Compare commits

...

676 Commits

Author SHA1 Message Date
Nick Genovese
3fb30d5561 Merge pull request #1373 from PepperDash/matrix-routing-isonline
Multiple fixes
2025-12-31 15:19:31 -05:00
Andrew Welker
57cd77f019 fix: implement IKeyName for DspControlPoint 2025-12-31 14:04:04 -06:00
Andrew Welker
7f2bb078c8 fix: revert prop name to inUpPosition for screenlift messenger
- refactor volume interfaces into separate files
- IBasicVolumeControl implements IKeyName
2025-12-31 12:20:40 -06:00
Andrew Welker
316bb849b4 fix: update matrix routing inputs if endpoint online status changes 2025-12-30 16:58:36 -06:00
Andrew Welker
a983e2c87f fix: save config only when values change 2025-12-30 14:34:11 -06:00
Nick Genovese
babb9a77df fix: a few logging updates 2025-12-29 16:48:13 -06:00
Nick Genovese
7629921113 fix: a few logging updates 2025-12-29 15:34:14 -06:00
Andrew Welker
3878c85a7a fix: use correct property name for isInUpPosition 2025-12-29 13:36:47 -06:00
Andrew Welker
7ad8218af0 fix: fusion controller now sets only associated room custom values 2025-12-29 13:03:47 -06:00
Andrew Welker
0c4aec14d1 fix: use .NET timers instead of CTimer 2025-12-29 11:53:52 -06:00
Andrew Welker
7d3f871460 Merge branch 'copilot/featureadd-raise-lower-time' into mc-connection-issues 2025-12-29 09:03:09 -06:00
Andrew Welker
78c9381108 fix: add clientId as qp for websocket for MC 2025-12-29 08:57:52 -06:00
Erik Meyer
a7ff2e8903 fix: move isInUpPosition to momvement complete method, remove conditionlal logic on raise/lower commands 2025-12-27 17:09:56 -06:00
copilot-swe-agent[bot]
ae0b2fe086 Refactor timer disposal and improve code readability
Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com>
2025-12-27 20:17:59 +00:00
copilot-swe-agent[bot]
bd11c827da Split movement time into separate raise/lower times and remove timing from latched mode
Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com>
2025-12-27 20:14:39 +00:00
copilot-swe-agent[bot]
7ea1efbabf Add raise/lower movement time configuration and banked command support
Co-authored-by: erikdred <88980320+erikdred@users.noreply.github.com>
2025-12-27 20:09:31 +00:00
copilot-swe-agent[bot]
9d6aaa2a0e Initial plan 2025-12-27 20:02:52 +00:00
Andrew Welker
53e7a30224 fix: handle threading issues for concurrent clients joining 2025-12-26 12:34:31 -06:00
Neil Dorin
39c1f60a4d Merge pull request #1370 from PepperDash/udp-eisc 2025-12-23 12:10:37 -05:00
Andrew Welker
2cc37a4e40 fix: ExecuteJoinAction now uses the correct Sig collections 2025-12-23 11:06:52 -06:00
Andrew Welker
076e5dfa9d chore: update comments, debug methods, and mark some things obsolete 2025-12-23 10:25:53 -06:00
Andrew Welker
092896bb25 fix: support for legacy UDP EISC 2025-12-23 09:57:49 -06:00
Neil Dorin
7c8f0586e6 Merge pull request #1369 from PepperDash/volume-messenger-issue 2025-12-16 17:15:46 -05:00
Andrew Welker
c5b0872a4c fix: DeviceVolumeMessenger only sends rawValue when device implements it 2025-12-16 15:41:33 -06:00
Andrew Welker
a7b88ec38d Merge pull request #1368 from PepperDash/generic-sink
generic sink
2025-12-16 11:09:40 -05:00
Andrew Welker
210b398a13 fix: implement PR Review suggestions 2025-12-16 10:04:01 -06:00
Nick Genovese
bce1e3610e fix: copy dictionaries
- fixed multiple enumeration exception
2025-12-10 16:58:41 -06:00
Andrew Welker
6a33e7c99d fix: initialize current sources dictionaries 2025-12-05 16:26:08 -06:00
Andrew Welker
2048e3f65d fix: GenericSink implements ICurrentSources 2025-12-05 16:26:02 -06:00
Neil Dorin
81df2738de Merge pull request #1363 from PepperDash/feature/add-on-off-dsp-preset-keys-to-room-config
feat: Add on/off dsp keys to EssentialsAvRoomPropertiesConfig
2025-11-26 18:00:18 -05:00
Neil Dorin
08fbec416f Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:45 -07:00
Neil Dorin
7594b22096 Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:39 -07:00
Neil Dorin
d1babf6b9b Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:31 -07:00
Neil Dorin
2187c9fb0d Update src/PepperDash.Essentials.Core/Devices/SourceListItem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:22 -07:00
Neil Dorin
a5e6059160 Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:14 -07:00
Neil Dorin
9ef4aedcce Update src/PepperDash.Essentials.Core/Room/Config/EssentialsRoomConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 15:53:07 -07:00
Neil Dorin
f7c7160bf0 feat: Add on/off dsp keys to EssentialsAvRoomPropertiesConfig
clean up XML comments and improve property definitions in EssentialsRoomConfig
2025-11-26 15:48:14 -07:00
Neil Dorin
dbf5740841 Merge pull request #1362 from PepperDash/feature/add-IHasFeedback-to-IEssentialsRoomFusionController
fix: ensure proper disposal of help request timeout timer and improve…
2025-11-26 13:28:55 -05:00
Neil Dorin
c07e099a79 feat: add logging for help request timeout events in IEssentialsRoomFusionController 2025-11-26 11:10:21 -07:00
Neil Dorin
06cb508f3a Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 11:09:07 -07:00
Neil Dorin
e93b5b34cc Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-26 11:09:02 -07:00
Neil Dorin
4f5d4ef87a fix: ensure proper disposal of help request timeout timer and improve logging 2025-11-26 11:02:31 -07:00
Andrew Welker
636da8cc8c Merge pull request #1361 from PepperDash/feature/add-IHasFeedback-to-IEssentialsRoomFusionController
feat: add help request timeout functionality to IEssentialsRoomFusion…
2025-11-26 12:34:48 -05:00
Neil Dorin
5de1e2d7bb feat: add help request timeout functionality to IEssentialsRoomFusionController 2025-11-26 10:26:41 -07:00
Andrew Welker
03bbb84894 Merge pull request #1359 from PepperDash/feature/add-IHasFeedback-to-IEssentialsRoomFusionController
feat: implement IHasFeedback interface in IEssentialsRoomFusionContro…
2025-11-25 13:37:31 -05:00
Neil Dorin
d17394cdd7 feat: add logging to ExecuteSwitch method in GenericSink 2025-11-25 11:06:48 -07:00
Neil Dorin
8467afde38 Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-24 17:23:38 -07:00
Neil Dorin
5c016fb4b8 feat: implement IHasFeedback interface in IEssentialsRoomFusionController 2025-11-24 17:14:34 -07:00
Neil Dorin
2fbc32947c Merge pull request #1357 from PepperDash/url-parsing
fix: check for multiple URL patterns for both template & system URLS
2025-11-18 14:14:16 -05:00
Andrew Welker
c06b57a5f9 fix: check for multiple URL patterns for both template & system URLS 2025-11-18 12:30:54 -06:00
Neil Dorin
6d64fffc50 Merge pull request #1356 from PepperDash/mc-subscription-logging
Multiple Fixes
2025-11-14 15:44:58 -05:00
Andrew Welker
0c4ebdaf1d fix: change how subscription state is logged to reduce traffic to console 2025-11-13 09:56:29 -06:00
Andrew Welker
2c49fb9321 fix: parse current Portal URLS for system and template UUIDs correctly 2025-11-12 16:58:23 -06:00
Neil Dorin
c55de61da9 fix: enhance COM port registration logging and update GenericSink class for input switching 2025-11-05 14:43:42 -07:00
Andrew Welker
42444ede0a Merge pull request #1354 from PepperDash/comport-controller-updates
fix: improve logging for COM port registration and configuration
2025-11-04 10:55:44 -05:00
jkdevito
3d50f5f5ac fix: improve logging for COM port registration and configuration 2025-11-04 09:37:14 -06:00
Nick Genovese
11d62aebe1 Merge pull request #1353 from PepperDash/mc-subscription-concurrency
fix: make subscriber functionality thread-safe
2025-11-03 17:26:31 -04:00
Andrew Welker
edc10a9c2a fix: make subscriberIds private & check for add failure 2025-11-03 15:19:17 -06:00
Andrew Welker
9be5823956 Merge branch 'main' into mc-subscription-concurrency 2025-11-03 15:18:45 -06:00
Andrew Welker
35371dde22 fix: make subscriber functionality thread-safe 2025-11-03 15:13:48 -06:00
Jason DeVito
d3ceb4d7e7 Merge pull request #1352 from PepperDash/theme-saving
fix: use correct overload for PostStatusMessage
2025-11-03 15:08:28 -06:00
Andrew Welker
a782b57100 fix: use correct overload for PostStatusMessage 2025-11-03 14:00:51 -06:00
Neil Dorin
1360de599f Merge pull request #1351 from PepperDash/stream-debugging
fix: centralize debug printing into extension methods
2025-11-03 12:19:05 -05:00
Andrew Welker
fd95f5fed1 docs: update XML docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-03 11:11:00 -06:00
Andrew Welker
9426dff5df fix: copilot suggestions from PR review 2025-11-03 11:02:39 -06:00
Andrew Welker
0d083e63c6 Merge commit '314570d6c3d78c7a92a362f3ec3a4a06bdbebd28' into stream-debugging 2025-11-03 10:56:53 -06:00
Andrew Welker
6ed7c96ec7 fix: centralize debug printing into extension methods
Stream debugging now uses CrestronConsole instead of debug methods, so that the debug statements will be printed regardless of console debug level. This also means that comm debug statements will NOT be in the Crestron Error log or in the files created by the logging system
2025-11-03 10:53:21 -06:00
Andrew Welker
314570d6c3 fix: change number of retained files to 7 instead of 30 for processors 2025-10-31 13:11:50 -05:00
Andrew Welker
ff609dfecd fix: add config option to turn echo off for SSH
In addition, removed CTimer in favor of System.Threading.Timers Timer in the SSH class, and modified the class to be thread-safe.
2025-10-31 09:01:54 -05:00
Andrew Welker
666be5ae59 Merge pull request #1345 from PepperDash/mc-touchpanel-key
mc touchpanel key
2025-10-30 17:11:15 -04:00
Andrew Welker
bfc9b7e7fa fix: logging & ternary changes 2025-10-30 16:07:26 -05:00
Andrew Welker
b02e952765 Merge remote-tracking branch 'origin/com102-comspec' into mc-touchpanel-key 2025-10-30 15:58:36 -05:00
Andrew Welker
72861a5097 Merge remote-tracking branch 'origin/feature/fusion-help-request' into mc-touchpanel-key 2025-10-30 15:58:26 -05:00
Andrew Welker
44ed067f4d chore: update local build version 2025-10-30 15:57:37 -05:00
Andrew Welker
faa2169baf fix: send correct URL to panel 2025-10-30 15:57:28 -05:00
Andrew Welker
fd40b0c6d1 fix: send all status messages with ClientId 2025-10-30 15:57:15 -05:00
Andrew Welker
afcddad1cc fix: remove async on task, as it's unnecessary 2025-10-30 15:56:50 -05:00
Andrew Welker
c4cf8f13e9 fix: register panel in post phase rather than activation cycle 2025-10-30 15:56:28 -05:00
Neil Dorin
c852f87a27 refactor: Comment out logging statements in help request handling 2025-10-30 14:33:58 -06:00
Neil Dorin
071174fa7d feat: Implement help request status tracking in Fusion system 2025-10-30 14:13:02 -06:00
Neil Dorin
da0f28a10c feat: Enhance IEssentialsRoomFusionController with additional properties and logging 2025-10-29 16:47:18 -06:00
Andrew Welker
0538a304ed Merge branch 'main' into mc-touchpanel-key 2025-10-29 15:13:53 -05:00
Andrew Welker
705c5db237 Merge pull request #1347 from PepperDash/json-console
fix: print json response to console using \r\n instead of \n
2025-10-29 16:12:49 -04:00
Neil Dorin
2e95f5337e feat: Add IEssentialsRoomFusionController and related configurations
- Introduced IEssentialsRoomFusionControllerFactory for creating Fusion Room Controller devices.
- Added IEssentialsRoomFusionControllerPropertiesConfig to define configuration properties for the Fusion Room Controller.
- Updated IFusionHelpRequest interface to include methods for cancelling and toggling help requests.
- Refactored RoomOnToDefaultSourceWhenOccupied to use IEssentialsRoomFusionController instead of EssentialsHuddleSpaceFusionSystemControllerBase.
- Modified EssentialsRoomBase to check for IEssentialsRoomFusionController in occupancy status provider.
2025-10-28 16:49:29 -06:00
jkdevito
ba576180a7 refactor: remove unused using directives in ComPortController 2025-10-28 09:05:56 -05:00
jkdevito
92c9db8237 refactor: improve logging messages in ComPort registration and configuration 2025-10-28 09:04:15 -05:00
jkdevito
c4d064965f refactor: enhance ComPortController with additional event documentation and logging improvements 2025-10-28 08:40:29 -05:00
Neil Dorin
f27965ac29 feat: Add help request functionality to Fusion system controller
Introduce `IFusionHelpRequest` interface for managing help requests, including `HelpRequestResponseFeedback` property and `SendHelpRequest` method. Enhance `EssentialsHuddleSpaceFusionSystemControllerBase` with new properties and methods to support help request tracking and management. Improve code organization and documentation throughout the affected files.
2025-10-27 17:35:38 -06:00
jkdevito
3ce282e2db refactor: comment out debug logging for ComPort registration 2025-10-27 17:33:06 -05:00
jkdevito
32a332a6e2 refactor: update TODO comment for Port registration verification 2025-10-27 17:32:40 -05:00
jkdevito
151a90c9be refactor: streamline ComPort registration and configuration logic with enhanced logging 2025-10-27 17:13:43 -05:00
jkdevito
89e893b3b7 fix: improve formatting and logging in ComPortController 2025-10-27 16:46:09 -05:00
jkdevito
d01eb03890 feat: enhance ComPortController with detailed logging on configuration changes 2025-10-27 14:06:20 -05:00
Andrew Welker
16c92afabb fix: print json response to console using \r\n instead of \n 2025-10-27 09:21:51 -05:00
Andrew Welker
dff5d2d32e Merge pull request #1346 from PepperDash/feature/add-infinetEx-comm-method 2025-10-23 18:50:06 -04:00
Neil Dorin
317bde3814 fix: add InfinetEx to eControlMethods enum 2025-10-23 15:22:46 -06:00
Andrew Welker
8bab3dc966 fix: send touchpanelKey message with all room combiner checks 2025-10-23 11:21:17 -05:00
Andrew Welker
514ac850ca fix: send touchpanel key correctly 2025-10-23 10:01:05 -05:00
Andrew Welker
44432f7a41 fix: send touchpanel key to client when client joins direct server 2025-10-23 09:54:03 -05:00
Andrew Welker
99253b30c2 fix: send touchpanel key to client when client joins 2025-10-23 09:49:45 -05:00
Andrew Welker
bf248fe33e Merge pull request #1343 from PepperDash/device-interface-system
device interface system
2025-10-23 10:40:56 -04:00
cdenig
2f44040e4f Merge pull request #1344 from PepperDash/feature/allow-config-tool-v2-structure
Feature/allow config tool v2 structure
2025-10-23 10:15:03 -04:00
Neil Dorin
10399a1be8 Update src/PepperDash.Core/Config/PortalConfigReader.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 13:54:08 -06:00
Neil Dorin
5409db193c Update src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 13:53:23 -06:00
Neil Dorin
f1ce54a524 Merge branch 'main' into feature/allow-config-tool-v2-structure 2025-10-22 13:48:22 -06:00
Andrew Welker
7c72a0d905 fix: send device interface list on client join
In order to avoid updating the MC Edge API to send device interfaces, one of the responses to the clientJoined message will now be a message with the type `/system/deviceInterfaces`. This has a corresponding handler in the core library to put the interfaces in the correct location.
2025-10-21 18:19:09 -05:00
Andrew Welker
5d5e78629e chore: update local build version to 2.18.2-local 2025-10-21 18:17:25 -05:00
Neil Dorin
dab5484d6e Merge pull request #1342 from PepperDash/wsdebug-persistence
Unique Client IDs
2025-10-15 15:16:46 -04:00
Andrew Welker
5c35a3be45 fix: catch exceptions in handlers directly
Previously, any exceptions that were occuring in a hander's action were being swalled due to being off on another thread. Now, those exceptions are caught and printed out.
2025-10-15 14:03:17 -05:00
Andrew Welker
6cb98e12fa fix: use correct collection for program stop 2025-10-15 14:02:06 -05:00
Andrew Welker
608601990b docs: fix Copilot comments 2025-10-15 12:54:14 -05:00
Andrew Welker
3e0f318f7f fix: update log methods to all be consistent 2025-10-15 12:36:42 -05:00
Andrew Welker
98d0cc8fdc docs: add missing XML comments for Mobile Control Project 2025-10-15 12:26:57 -05:00
Andrew Welker
c557c6cdd6 fix: mobileinfo & CWS info call report the correct data 2025-10-15 11:49:06 -05:00
Andrew Welker
8525134ae7 fix: direct server clients now have unique client IDs
Using the generated security token as an ID was presenting problems with duplicate connections using the same ID and trying to figure out where to send the messages. Now, the clients have a unique ID that's an increasing integer that restarts at 1 when the program restarts.
2025-10-15 09:50:12 -05:00
Andrew Welker
1197b15a33 Merge pull request #1340 from PepperDash/display-bridging
Display bridging, Docs, Appdebug
2025-10-10 10:21:35 -04:00
Andrew Welker
ea6a7568fc Merge remote-tracking branch 'origin/feature/add-IKeyed-to-interfaces' into display-bridging 2025-10-09 16:08:17 -05:00
Andrew Welker
3a2a059ce1 fix: appdebug now has option for setting all sink levels 2025-10-09 16:07:25 -05:00
Andrew Welker
f9d9df9d5c docs: XML comments for Devices.Common 2025-10-09 13:18:36 -05:00
Neil Dorin
c284c4275f feat: Enhance UDPServer initialization in Connect method
Updated the `Connect` method in `GenericUdpServer.cs` to include error handling for hostname parsing. The method now attempts to create a `UDPServer` instance with specified parameters and falls back to default initialization if an error occurs. This improves flexibility and robustness in server setup.
2025-10-09 11:31:38 -06:00
Neil Dorin
0418f8a7cc fix: Fix typos and enhance item selection handling
Corrected parameter name in GenericUdpServer constructor.
Added new action for item selection in ISelectableItemsMessenger
with error handling for missing or invalid keys.
Updated SetItems method to improve clarity and ensure proper
clearing and re-adding of item actions.
2025-10-09 09:40:46 -06:00
Andrew Welker
a5d409e93a fix: analog input check uese correct comparison
input selection coming from SIMPL is 1-based, not 0-based. Comparison to input port array length needs to be <= instead of <
2025-10-09 08:36:26 -05:00
Andrew Welker
59944c0e2f refactor: split up classes to separate files 2025-10-09 08:33:22 -05:00
Neil Dorin
419177ccd5 fix: Add item management and update handling in messenger
Introduces a new private field `_itemKeys` to store item keys. Adds the `SetItems` method to manage item actions and update events, ensuring proper registration and cleanup. The `SendFullStatus` method is now invoked from a dedicated event handler `LocalItem_ItemUpdated`, improving the handling of item updates.
2025-10-08 12:24:17 -06:00
Neil Dorin
bd01e2bacc fix: Add KeyName class and update camera messaging
This commit introduces a new `KeyName` class implementing the `IKeyName` interface, enhancing the representation of camera data. The `CameraController_CameraSelected` and `SendFullStatus` methods are updated to utilize `KeyName` instances for selected and listed cameras, improving data encapsulation and consistency in the `IHasCamerasWithControlsStateMessage`. Additionally, new using directives for logging and core functionalities are added.
2025-10-07 17:10:11 -06:00
Neil Dorin
2928c5cf94 feat: Enhance camera capabilities and messaging structure
- Introduced `ICameraCapabilities` interface and `CameraCapabilities` class for defining camera features like pan, tilt, zoom, and focus.
- Modified `IHasCameras` interface to include a list of `IHasCameraControls` objects for improved camera management.
- Refactored `CameraBaseMessenger` to be generic, enhancing flexibility and type safety.
- Updated `SendCameraFullMessageObject` to include detailed camera capabilities in status messages.
- Added `CameraStateMessage` class to encapsulate camera state, including control support and capabilities.
- Updated `IHasCamerasWithControlMessenger` to use `IKeyName` for camera list and selected camera properties, improving type consistency.
- Enhanced `MobileControlSystemController` to manage devices implementing `IHasCameraControls`, creating appropriate messengers for different device types.
2025-10-07 15:37:31 -06:00
Neil Dorin
82b5dc96c1 feat: Deprecate IHasCamerasMessenger; introduce new controls
Mark IHasCamerasMessenger as obsolete and replace it with
IHasCamerasWithControlMessenger, which adds functionality
for devices with camera controls. A new state message class,
IHasCamerasWithControlsStateMessage, has been added to
encapsulate camera state. Update MobileControlSystemController
to use the new messenger implementation.
2025-10-07 12:00:01 -06:00
Neil Dorin
37cea8a11c fix: Refactor camera control interfaces and event arguments
Significantly restructure camera control interfaces and event arguments.
Removed obsolete interfaces like `IHasCameras` and `CameraSelectedEventArgs`,
and introduced generic event argument classes for improved type safety.
Added `IHasCamerasWithControls` for better management of camera controls.
Corrected the `IHasCameraMuteWithUnmuteReqeust` interface name.
Reintroduced the `eCameraControlMode` enum to define camera control modes.
These changes enhance the organization, clarity, and functionality of the camera control system.
2025-10-07 11:52:07 -06:00
Neil Dorin
a3f0901fa0 Merge pull request #1335 from PepperDash/mc-messenger-improvements
feat: unique status requests for messengers
2025-09-30 14:33:07 -06:00
Neil Dorin
f91f435768 fix: Add null check for CurrentScenario in MobileControlSystem
This change introduces a check for `_roomCombiner.CurrentScenario` being `null`. When it is `null`, a `MobileControlMessage` is created with the type `/system/roomKey`, `clientId`, and `roomKey`, which is then sent to the client. This improves the handling of scenarios without a current scenario by ensuring relevant room key information is communicated.
2025-09-30 12:11:25 -06:00
Andrew Welker
5120b2b574 Merge branch 'main' into mc-messenger-improvements 2025-09-29 11:02:39 -05:00
Andrew Welker
7c90027578 feat: set direct server debug level via console command
The `mobilewsdebug` console command can now be used to set the internal websocket logging level for both the API client and the direct server at the same time.
2025-09-26 21:47:06 -05:00
Andrew Welker
bb694b4200 feat: enable subscription logic for messengers
In order to help control traffic over the websocket, a subscription feature has been added:
* A config option, `enableMessengerSubscriptions` has been added
* When true, the MessengerBase class will assume that any message sent using the `PostStatusMessage` that has a valid client ID wants to send any subsequent unsolicited updates to that same client
* The client's ID will be added a list of subscribed client IDs
* Any subsequent messages sent using the `PostStatusMessage` methods that have a null clientId will ONLY be sent to subscribed clients
* When a client disconnects, it will be removed from the list of subscribed clients

This should cut down drastically on the traffic to the UI, especially when combined with requesting partial status updates from a device rather than the entire state.
2025-09-26 21:31:54 -05:00
Andrew Welker
087d0a1149 chore: move classes and interfaces to individual files 2025-09-26 21:21:01 -05:00
Andrew Welker
4747c16b02 build: delete all local build clzs on build 2025-09-26 21:17:28 -05:00
Andrew Welker
aa4d241dde Merge pull request #1337 from PepperDash/Ibasicvolumecontrols-messenger
fix: modify volume messenger to start with IBasicVolumeControls
2025-09-26 10:40:44 -04:00
Andrew Welker
9fc5586531 fix: use IBasicVolumeControls to build DeviceVolumeMessenger 2025-09-25 15:29:35 -05:00
Andrew Welker
d33fd56529 feat: add method to force panel reload
Other refactorings for factory method and log statements
2025-09-25 14:47:11 -05:00
Neil Dorin
8fc4d21f02 fix: Add DeviceInterfaceSupport property to JoinResponse
This commit introduces a new property, `DeviceInterfaceSupport`,
to the `JoinResponse` class in the `PepperDash.Essentials.WebSocketServer`
namespace. This property is a dictionary that maps strings to
`DeviceInterfaceInfo` objects, enhancing support for device interfaces.
A summary comment has also been added for clarity.
2025-09-25 11:50:42 -06:00
Neil Dorin
6d93662b31 Merge pull request #1338 from PepperDash/io-updates 2025-09-25 09:59:36 -06:00
Andrew Welker
11b190e76f docs: put XML comments on correct thing 2025-09-25 10:51:30 -05:00
Andrew Welker
df03a71cbc chore: fix errors in log statements
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-25 09:50:55 -05:00
Andrew Welker
72e67a1c4c fix: implement IHasFeedback for all IO devices and modify logging 2025-09-25 09:47:44 -05:00
Andrew Welker
f8728b6825 docs: update XML comments for EssentialsDeviceFactory 2025-09-25 09:46:55 -05:00
Andrew Welker
d9ef8a2289 docs: add XML comments for IAnalogInput 2025-09-25 09:46:03 -05:00
Andrew Welker
278408a3bc docs: reword XML comments for IHasFeedback Interface 2025-09-25 09:45:42 -05:00
Andrew Welker
fd70377c7f fix: log errors and disconnects for UI Clients 2025-09-25 08:51:11 -05:00
Neil Dorin
06341b14f3 feat: Adds device interface support info to joinroom response in MC websocket server.
Enhance MessengerBase and WebSocketServer functionality

Updated MessengerBase with new methods for action management and message posting, along with improved documentation. Introduced DeviceMessageBase for better message representation.

Enhanced MobileControlWebsocketServer to support device interfaces, adding DeviceInterfaceSupport to JoinResponse and a new DeviceInterfaceInfo class for detailed device information.
2025-09-24 14:49:41 -06:00
Andrew Welker
d7f9c74b2f fix: modify volume messenger to start with IBasicVolumeControls 2025-09-23 13:39:03 -05:00
Andrew Welker
5921b5dbb0 Merge pull request #1336 from PepperDash/hotfix/check-for-cs-lan-on-mctp
Hotfix/check for cs lan on mctp
2025-09-23 13:58:15 -04:00
Neil Dorin
ba0de5128f Update src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-23 11:56:57 -06:00
Andrew Welker
b0a090062f Merge branch 'hotfix/check-for-cs-lan-on-mctp' into mc-messenger-improvements 2025-09-23 11:25:41 -05:00
Andrew Welker
9c0cab8218 fix: use id parameter and fix some formatting 2025-09-23 11:09:44 -05:00
Andrew Welker
c0af637108 fix: use correct property names 2025-09-23 11:07:24 -05:00
Andrew Welker
9c9eaea928 feat: unique status requests for messengers
UI Applications can now request status for specific feature sets instead of full status for a device. This will hopefully cut down on the traffic and messages required to get the data for the UI.
2025-09-23 10:55:16 -05:00
Neil Dorin
9de94bd65f fix: Update v2 config detection criteria
Changed the logic for identifying v2 configuration files. The check now looks for the presence of a "versions" node instead of the absence of "system" or "template" nodes, reflecting an update in the configuration file structure.
2025-09-22 15:05:06 -06:00
Neil Dorin
ff46fb8f29 feat: Add versioning support to EssentialsConfig
Introduce `Versions` property in `EssentialsConfig` to hold version information.
Add `VersionData` class for Essentials and package versions, and `NugetVersion` class for individual package details.
Retain and document `SystemTemplateConfigs` class.
2025-09-22 14:55:58 -06:00
Neil Dorin
d9243def30 feat: Adds ability to read configs generated from v2 config tool that are pre-merged don't have system or template objects
Refactor config handling and improve documentation

- Updated `PortalConfigReader.cs` to use constants for configuration keys, enhancing maintainability and readability. Improved error logging with `Debug.LogError`.
- Modified `ConfigReader.cs` to handle v2 configuration format, streamlining the loading process and avoiding redundant parsing.
- Added XML documentation comments to properties in `EssentialsConfig.cs`, improving code documentation. Initialized `Rooms` property in the constructor.
- Enhanced `SystemTemplateConfigs` class with XML documentation for better clarity on its properties.
2025-09-22 14:22:57 -06:00
Neil Dorin
258699fbcd fix: Enhance AppUrl change logging
Updated logging to include the new AppUrl value when it changes. This provides better context in the logs, making it easier to track the specific URL that was set.
2025-09-18 18:23:12 -06:00
Neil Dorin
738504e9fc fix: Add error handling for network parameter retrieval
Introduced a try-catch block to handle exceptions when fetching the Crestron Ethernet adapter's ID, subnet mask, and IP address. Added logging for cases where the processor lacks a CS LAN. Also included a new using directive for Serilog.Events.
2025-09-18 14:48:21 -06:00
erikdred
cae1bbd6e6 Merge pull request #1332 from PepperDash/feature/add-hide-property-to-room-combine-scenario
feat: Add HideInUi property to room combiner classes
2025-09-17 18:38:21 -04:00
Neil Dorin
dea4407e3e Update src/PepperDash.Essentials.Core/Room/Combining/IEssentialsRoomCombiner.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-17 16:36:33 -06:00
Neil Dorin
ab08a546f7 feat: Add HideInUi property to room combiner classes
- Introduced `HideInUi` property in `EssentialsRoomCombinerPropertiesConfig` to control UI visibility.
- Added `HideInUi` interface property in `IEssentialsRoomCombiner`.
- Implemented `HideInUi` in `RoomCombinationScenario`, along with new `Key` and `Name` properties for improved data representation.
2025-09-17 15:25:23 -06:00
Neil Dorin
ef98d47792 Merge pull request #1331 from PepperDash/dev-list-fix 2025-09-17 08:36:04 -06:00
Andrew Welker
c05976ee60 fix: modify formatting and sorting for devfb response 2025-09-17 09:28:04 -05:00
Andrew Welker
4ca1031bef docs: fix CS1587 errors 2025-09-17 08:52:44 -05:00
Andrew Welker
6d61c4525e fix: use ConsoleCommandResponse for device feedbacks 2025-09-17 08:52:29 -05:00
Andrew Welker
3db274ace5 fix: add line endings for devlist console command 2025-09-17 08:51:45 -05:00
Neil Dorin
f0f708294c Merge pull request #1329 from PepperDash/devlist-fix
fix: print devlist output using ConsoleCommandResponse
2025-09-10 09:58:13 -06:00
Andrew Welker
a00d186c62 fix: print devlist output using ConsoleCommandResponse 2025-09-10 10:45:55 -05:00
Neil Dorin
51da668dfd Merge pull request #1327 from PepperDash/cs-lan-mc-panel
fix: add config property for devices on CS LAN
2025-09-05 16:20:42 -06:00
Andrew Welker
d2b7400039 fix: INvxNetworkPortInformation inherits from IKeyed 2025-09-05 15:55:31 -05:00
Andrew Welker
2424838b7f chore: remove unused using
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-02 12:56:14 -05:00
Andrew Welker
9556edc064 fix: add config property for devices on CS LAN 2025-09-02 12:54:51 -05:00
Sumanth Rayancha
f9088691fd Merge pull request #1322 from PepperDash/release
Release
2025-08-26 10:16:18 -04:00
Andrew Welker
e40b6a8b4c Merge pull request #1320 from PepperDash/feature/extract-html-assets
feat: add html assets extraction from zip files in ControlSystem
2025-08-26 09:58:02 -04:00
Erik Meyer
c3b39a87da fix: enhance zip extraction to prevent directory traversal attacks 2025-08-22 15:15:02 -04:00
erikdred
06dc0e947e fix: check for null when getting directory
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-22 09:12:20 -04:00
aknous
147997f460 Merge pull request #1321 from PepperDash/IBasicVideoMuteWithFeedbackMessenger
Adds IBasicVideoMuteWithFeedbackMessenger
2025-08-21 14:26:10 -04:00
aknous
49abec5eea Update src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-21 13:23:43 -04:00
aknous
6830efe42a fix: fixes CameraBaseMessenger hold timer for PTZ controls, adds storePreset messenger 2025-08-19 18:34:16 -04:00
Neil Dorin
d013068a0c Update src/PepperDash.Essentials/ControlSystem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 14:54:11 -06:00
Neil Dorin
52916d29f4 Update src/PepperDash.Essentials/ControlSystem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-19 14:53:06 -06:00
Erik Meyer
19e8489166 feat: add html assets extraction from zip files in ControlSystem 2025-08-19 15:48:43 -04:00
Neil Dorin
fe33443b25 fix: Update IP handling in MobileControlTouchpanelController
Updates logic to handle setting the URL sent to the CH5 wrapper app to use the CS LAN IP based on the actual IpInformationChange event on the panel itself.

- Added `._PepperDash.Essentials.4Series.sln` to .gitignore.
- Introduced new using directives for Regex and Crestron libraries.
- Added `csIpAddress` and `csSubnetMask` fields to store device IP info.
- Modified constructor to retrieve and assign current IP and subnet mask.
- Updated `Panel.IpInformationChange` event handler for logging and URL setting.
- Created `GetUrlWithCorrectIp` method to determine the correct URL based on IP.
- Refactored `SetAppUrl` to utilize the new URL method.
- Commented out old IP determination logic in `MobileControlWebsocketServer.cs` as it was moved to the touchpanel controller.
2025-08-19 13:28:45 -06:00
aknous
8cf195b262 feat: adds IBasicVideoMuteWithFeedbackMessenger 2025-08-19 11:37:39 -04:00
Neil Dorin
40406b797d Merge pull request #1314 from PepperDash/streaming-device-properties 2025-08-15 11:52:20 -06:00
Andrew Welker
65bc408ebf fix: add StreamUrl to baseStreamingDeviceProperties 2025-08-15 12:41:21 -05:00
Neil Dorin
9d49fb8357 Merge pull request #1313 from PepperDash/temp-to-dev
Temp to dev
2025-08-15 09:34:41 -06:00
Neil Dorin
fb7797dac7 Merge pull request #1312 from PepperDash/comm-bridge-add 2025-08-15 09:02:23 -06:00
Andrew Welker
574f5f6dc9 chore: remove unused using directives in CommFactory.cs 2025-08-15 09:51:17 -05:00
Andrew Welker
e49c69a12f feat: add CommBridge class and enhance EssentialsBridgeableDevice with new constructors 2025-08-15 09:48:30 -05:00
Sumanth Rayancha
7889cba196 Merge pull request #1307 from PepperDash/feature/assets-folder
feat: add LoadAssets method to manage asset loading and configuration…
2025-08-13 12:08:14 -04:00
Nick Genovese
033aa1f3dd feat: enhance asset extraction and configuration file handling in ControlSystem 2025-08-12 12:17:58 -04:00
Neil Dorin
9b6c2d80ea Merge pull request #1308 from PepperDash/current-sources 2025-08-11 22:53:11 -06:00
Andrew Welker
a0fc731701 chore: apply copilot suggestions
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-11 17:55:02 -05:00
Andrew Welker
f2d0dca7b8 fix: make appdebug case-insensitive
when commands like `appdebug verbose` are used rather than `appdebug 2`, the command would fail because Verbose != verbose. The enum conversion is now case-insensitive.
2025-08-11 17:50:50 -05:00
Andrew Welker
ab4e85d081 fix: use different methods for extensions
The logging extension methods now use the appropriate methods from
the debug class. Previously some messages were not getting handled correctly, and it was causing issues with log statements.
2025-08-11 17:48:46 -05:00
Andrew Welker
47017da527 fix: use correct property names 2025-08-11 17:47:14 -05:00
Andrew Welker
4f0d464ba4 Merge pull request #1306 from PepperDash/fix/remove-unsued-method-in-cs
Fix/remove unsued method in cs
2025-08-11 15:30:04 -05:00
Nick Genovese
0107422507 refactor: remove unused assembly resolution logic from ControlSystem 2025-08-11 16:24:44 -04:00
Nick Genovese
1a366790e7 feat: add LoadAssets method to manage asset loading and configuration file cleanup 2025-08-11 16:23:31 -04:00
Andrew Welker
97448f4f0f Merge branch 'main' into current-sources 2025-08-06 09:01:40 -05:00
Andrew Welker
cf3ece4237 fix: use cr-lf for line endings 2025-08-06 09:00:45 -05:00
Andrew Welker
bb4b2f88b6 Merge pull request #1304 from PepperDash/temp-to-dev
Temp to dev
2025-08-05 16:39:38 -05:00
aknous
808e8042a7 Merge pull request #1305 from PepperDash/default-debug-levels
fix: set default debug levels if not found
2025-08-04 14:47:31 -04:00
Andrew Welker
0bc4388bfd Merge branch 'default-debug-levels' into current-sources 2025-08-04 13:31:26 -05:00
Andrew Welker
dbc132c0da fix: set default debug levels if not found 2025-08-04 13:31:17 -05:00
Andrew Welker
5bb0ab2626 fix: base config properties for use with streaming devices 2025-08-01 21:17:35 -05:00
Andrew Welker
27bf36c58c fix: modify how current sources dictionary gets updated 2025-08-01 09:22:31 -05:00
Andrew Welker
ce886aea63 chore: update local build version 2025-08-01 09:22:31 -05:00
Andrew Welker
ef920bf54c Merge branch 'main' into current-sources 2025-07-31 13:27:43 -05:00
Andrew Welker
88466818ce Merge pull request #1303 from PepperDash/debug-fixes
fix: use correct overload for logging at levels
2025-07-31 13:03:14 -05:00
Andrew Welker
0871a902e1 fix: use correct overload for logging at levels
Some overloads that had the first argument as an Exception were calling the _logger.Write method with a null where the message template should have been, causing messages logged with that overload to be swallowed and not logged.
2025-07-31 12:50:41 -05:00
Andrew Welker
a031424752 fix: add destination & source keys to routelist 2025-07-30 11:20:54 -05:00
Andrew Welker
fd1ba345aa fix: remove StringEnumConverter 2025-07-29 23:01:13 -05:00
Andrew Welker
e03874a7a9 fix: add messenger and event to ICurrentSources 2025-07-29 22:26:07 -05:00
aknous
2efab4f196 Merge pull request #1301 from PepperDash/mc-tp-ip-fix
fix: only adjust IP if processor is a CS processor
2025-07-28 12:57:24 -04:00
Andrew Welker
a41aba1904 Merge pull request #1300 from PepperDash/temp-to-dev
Temp to dev
2025-07-28 11:54:11 -05:00
Andrew Welker
d0ca6721f5 fix: only adjust IP if processor is a CS processor 2025-07-28 11:48:00 -05:00
Andrew Welker
c732eb48f2 Merge pull request #1299 from PepperDash/factory-updates
Multiple Updates
2025-07-25 10:38:15 -05:00
Andrew Welker
efe70208d3 fix: check for null assembly name 2025-07-25 10:32:43 -05:00
Andrew Welker
615f640ebb fix: use continue instead of return
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-25 10:27:50 -05:00
Andrew Welker
ee6f9416a3 chore: remove unused configSnippet 2025-07-25 10:24:32 -05:00
Andrew Welker
4fc6ecbd0b style: switch to auto property for attributes 2025-07-25 09:56:39 -05:00
Andrew Welker
58bcc3315d fix: add changes from code review 2025-07-25 09:51:04 -05:00
Andrew Welker
08cc84a8e8 fix: apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-25 09:28:53 -05:00
Andrew Welker
226014fee0 fix: apply suggestions from code review
- remove commented out debug statements
- null check for description attribute

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-25 09:27:33 -05:00
Andrew Welker
dc7f99e176 fix: mark FeedbackBase default constructor as obsolete
There are situations now where feedbacks in the feedback collection
can be used to update things on UIs. If the feedback doesn't have a key, it can't
be used for this purpose.
2025-07-25 09:18:22 -05:00
Andrew Welker
f0af9f8d19 fix: mark IHasMultipleDisplays and associated enum as obsolete
Fixes #1219
2025-07-25 09:14:45 -05:00
Andrew Welker
31143f56df fix: modify how exceptions are printed to reduce noise
When an exception occurs during the connect method,
only the exception message will be printed at the Error log level.
The entire stack trace will be printed when at the Verbose level.

fixes #1273
2025-07-25 09:13:42 -05:00
Andrew Welker
43989b9588 chore: move interfaces to their own files 2025-07-25 09:12:09 -05:00
Andrew Welker
8db559f197 feat: factory updates & refactoring
This commit introduces significant updates to the device factory system, enhancing the way devices are created and managed within the PepperDash Essentials framework.
The changes include:
- New attributes for device configuration and description.
- Refactoring of the device manager and essentials device classes to support new factory methods.
- modified factory classes for essentials devices, plugin development devices, and processor extension devices.
- The device factory interface has been updated to include a factory method for creating devices.
- Added a wrapper for the device factory to streamline device creation.
- Updated plugin loader to accommodate the new device factory structure.

Fixes #1065
Fixed #1277
2025-07-25 09:05:40 -05:00
Andrew Welker
86f20da116 Merge pull request #1291 from PepperDash/copilot/fix-1290
docs: Add comprehensive XML documentation to all public members
2025-07-25 08:02:37 -05:00
Andrew Welker
0674dbda37 Merge branch 'main' into copilot/fix-1290 2025-07-25 07:48:49 -05:00
Andrew Welker
592607f3c8 Merge pull request #1296 from PepperDash/feature/add-IHasCamerasMessenger 2025-07-24 18:53:05 -05:00
Neil Dorin
ea0a779f8b Merge branch 'feature/add-IHasCamerasMessenger' of https://github.com/PepperDash/Essentials into feature/add-IHasCamerasMessenger 2025-07-24 16:40:06 -06:00
Neil Dorin
86e4d2f7fb feat: Update SendFullStatus to target specific clients
Modified the `SendFullStatus` method to accept a `string clientId` parameter, allowing it to send status messages to specific clients. Updated the action for `"/fullStatus"` to pass the client ID and adjusted the `PostStatusMessage` call accordingly.
2025-07-24 16:39:28 -06:00
Neil Dorin
0069233e13 Update src/PepperDash.Essentials.Devices.Common/Cameras/CameraControl.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-24 16:16:05 -06:00
Neil Dorin
4048efb07e Merge branch 'main' into feature/add-IHasCamerasMessenger 2025-07-24 16:03:41 -06:00
Andrew Welker
b12cdbc75c docs: apply suggestions from copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-23 09:08:10 -05:00
Neil Dorin
b47f1d6b77 Merge pull request #1293 from PepperDash/to-dev 2025-07-23 07:53:44 -06:00
Andrew Welker
1dbac7d1c8 Merge pull request #1292 from PepperDash/portkey-add
feat: add destination and source port key properties for advanced routing
2025-07-22 15:26:44 -05:00
Neil Dorin
799d4c127c Update src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 14:02:01 -06:00
Andrew Welker
a6cd9a0571 feat: add destination and source port key properties for advanced routing 2025-07-22 14:56:28 -05:00
copilot-swe-agent[bot]
80da4ad98f docs: fix duplicate and malformed XML documentation tags
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-22 17:40:17 +00:00
Andrew Welker
b283ed34b4 docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:08:28 -05:00
Andrew Welker
899f13eadb docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:08:10 -05:00
Andrew Welker
fc1e29565e docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:03:33 -05:00
Andrew Welker
f9a74567d2 docs: remove duplicates
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:03:18 -05:00
Andrew Welker
53b1e5d142 docs: remove duplicates
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:03:03 -05:00
Andrew Welker
78e9ea8070 docs: duplicate tags
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:02:38 -05:00
Andrew Welker
df201558a5 docs: update
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 12:02:24 -05:00
Andrew Welker
130c874684 docs: fix documentation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:37:08 -05:00
Andrew Welker
aee40ffe14 docs: fix duplicate XML tags
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:36:43 -05:00
Andrew Welker
3ffad13abf docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:30:04 -05:00
Andrew Welker
5ee7aaa991 docs: revert to old comment
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:29:25 -05:00
Andrew Welker
4fa8433e73 docs: change wording
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:19:10 -05:00
Andrew Welker
5fe99518a0 docs: update formatting
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:18:50 -05:00
Andrew Welker
5a2a2129e6 docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:13:33 -05:00
Andrew Welker
4fbfda62d6 docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:13:21 -05:00
Andrew Welker
b8ab54cbe0 docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:12:55 -05:00
Andrew Welker
f6f1619bc2 docs: remove duplicate
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:12:38 -05:00
Andrew Welker
41fd4d6adc docs: revert to original documentation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:12:25 -05:00
Andrew Welker
5b73f8fbd2 docs: remove duplicate docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:12:03 -05:00
Andrew Welker
c70a8edc24 docs: remove duplicate documentation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 11:11:33 -05:00
copilot-swe-agent[bot]
7987eb8f9b docs: complete XML documentation for all projects with inheritdoc tags
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-22 15:53:01 +00:00
copilot-swe-agent[bot]
260677a37f docs: add XML documentation to PepperDash.Core project
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-22 15:48:23 +00:00
copilot-swe-agent[bot]
eeb0e84dc7 docs: enable XML documentation generation and add initial documentation
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-22 15:44:52 +00:00
copilot-swe-agent[bot]
d282487da6 Initial plan 2025-07-22 15:35:41 +00:00
Andrew Welker
da30424657 Merge pull request #1289 from PepperDash/meter-feedback-interface
meter feedback interface
2025-07-21 15:20:29 -05:00
Andrew Welker
311452beac fix: use correct namespaces
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:30:11 -05:00
Andrew Welker
789113008e docs: update comments
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:29:11 -05:00
Andrew Welker
660836bd5a docs: remove spaces
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:28:59 -05:00
Andrew Welker
97b2ffed9c docs: fix comment
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:28:37 -05:00
Andrew Welker
2bbefa062d docs: fix comments
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:28:10 -05:00
Andrew Welker
3421b2f28c Merge branch 'main' into meter-feedback-interface 2025-07-17 12:34:16 -05:00
Andrew Welker
82889e9794 Merge pull request #1288 from PepperDash/mc-touchpanel-cs
Backwards Compatibility issues
2025-07-17 12:33:01 -05:00
Andrew Welker
1dcd4e328c fix: Destination support for USB 2025-07-17 12:32:26 -05:00
Andrew Welker
e76369726d docs: XML comments for DestinationListItem 2025-07-17 12:25:52 -05:00
Andrew Welker
2bf0f2092b fix: use new interface in direct server 2025-07-17 12:16:32 -05:00
Neil Dorin
f8455d4110 feat: Refactor IHasCamerasMessenger constructor parameters
Updated the constructor of `IHasCamerasMessenger` to reorder parameters, placing `IHasCameras cameraController` last. Added logic in `MobileControlSystemController.cs` to instantiate and add `IHasCamerasMessenger` for devices implementing the `IHasCameras` interface.
2025-07-17 11:14:32 -06:00
Andrew Welker
c1eccfd790 fix: refactor interfaces for backwards compatibility 2025-07-17 12:13:08 -05:00
aknous
f1a89161bc Merge pull request #1287 from PepperDash/mc-touchpanel-cs
fix: use Control Subnet IP if MC TP devices are on the CS Lan
2025-07-17 11:17:03 -04:00
Andrew Welker
e59c50d0aa refactor: use tryParse for IP Address parsing
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-17 10:15:19 -05:00
Andrew Welker
9d313d8c7c fix: use Control Subnet IP if MC TP devices are on the CS Lan 2025-07-17 09:54:08 -05:00
Andrew Welker
9813673b66 feat: ICurrentSources interface to allow for tracking breakaway routing 2025-07-17 09:15:25 -05:00
Neil Dorin
f006ed0076 feat: Add camera control interfaces and messenger classes
This commit introduces new interfaces in `CameraControl.cs` for various camera functionalities, including muting, panning, tilting, zooming, and auto modes. The `IHasCameras` interface is expanded to manage camera lists and selections, with the addition of `CameraSelectedEventArgs` for event handling.

In `CameraBaseMessenger.cs`, a new `IHasCamerasMessenger` class is created to facilitate communication for devices implementing the `IHasCameras` interface, along with the `IHasCamerasStateMessage` class to represent the state of camera devices. These changes enhance the overall camera control capabilities and improve interaction management within the application.
2025-07-16 16:54:57 -06:00
Andrew Welker
ddbcc13c50 fix: add property for sync device association 2025-07-16 10:41:46 -05:00
Andrew Welker
2a70fc678e fix: add IStateFeedback interface 2025-07-11 13:13:52 -05:00
Andrew Welker
056614cba1 fix: add IMeterFeedback interface 2025-07-09 14:32:01 -05:00
Neil Dorin
5ff587a8c9 Merge pull request #1285 from PepperDash/feature/add-isMic-support
feat: Update .gitignore and enhance routing-related classes
2025-07-08 10:55:05 -06:00
Neil Dorin
26c1baa1f8 Merge pull request #1284 from PepperDash/network-port 2025-07-07 09:19:42 -06:00
Andrew Welker
2b15c2a56f docs: remove extra space
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-07 10:17:37 -05:00
Andrew Welker
a076d531bc chore: remove BOM
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-07 10:17:08 -05:00
Andrew Welker
5e880f0111 chore: add missing brace 2025-07-07 10:06:23 -05:00
Andrew Welker
8f1fb86d37 fix: add NVX network port info interface 2025-07-07 09:49:01 -05:00
Andrew Welker
fda4a5a816 Merge pull request #1282 from PepperDash/main
Update Dev
2025-07-04 10:45:35 -05:00
Neil Dorin
471d5b701b Merge pull request #1281 from PepperDash/combiner-auto-mode-enable 2025-06-27 08:48:11 -06:00
Andrew Welker
96ac266d24 fix: room combiner messenger sends disableAutoMode property 2025-06-27 10:45:42 -04:00
Andrew Welker
c1809459a6 chore: format property name correctly for JSON 2025-06-27 10:37:49 -04:00
Andrew Welker
1a9e1087de fix: add property to disable auto mode 2025-06-27 10:36:34 -04:00
Neil Dorin
8d55615837 Merge pull request #1280 from PepperDash/device-info-messenger 2025-06-26 16:08:49 -06:00
Andrew Welker
19e799f11d fix: debounce device info events
In some cases, multiple device info update events are triggering,
causing the queue to be flooded with multiple unneccessary messages containing the same info.
This clogs the queue and makes it harder for UIs to come online when Essentials restarts,
especially in systems with a lot of NVX devices.

The events are now debounced. If there are no new messages for 1 second, then the MC message
is sent out.
2025-06-26 17:12:36 -04:00
Neil Dorin
a3c1c444b7 Merge pull request #1279 from PepperDash/device-status-console 2025-06-26 12:27:43 -06:00
Andrew Welker
9f70e3c721 Merge pull request #1276 from PepperDash/temp-to-dev
Temp to dev
2025-06-26 14:16:36 -04:00
Andrew Welker
c9b3205736 fix: return --- if the device was created without a name 2025-06-26 14:14:04 -04:00
Andrew Welker
253b2cddaf fix: print device key & name instead of type 2025-06-26 13:54:24 -04:00
Andrew Welker
d96edfa8d0 fix: end devcommstatus with cr-lf instead of just -lf 2025-06-26 13:50:42 -04:00
Neil Dorin
2fa297a204 feat: Update .gitignore and enhance routing-related classes
- Updated `.gitignore` to include additional files and directories.
- Added summary comments and new properties in `LevelControlListItem.cs` for better clarity and functionality.
- Enhanced documentation in `SourceListItem.cs` and introduced new properties, including `Destinations` and a `ToString` method.
- Introduced `SourceRouteListItem` class with routing properties and expanded `eSourceListItemDestinationTypes` enum.
- Added `IRoutingSinkWithInputPort` interface in `IRoutingSink.cs` to support input port functionality.
2025-06-26 10:10:09 -06:00
aknous
95c1c01396 Merge pull request #1278 from PepperDash/feature/add-interfaces
Feature/add interfaces
2025-06-18 15:22:33 -04:00
Neil Dorin
9c94806e4f Update src/PepperDash.Essentials.Devices.Common/Codec/Cisco/IPresenterTrack.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-18 13:21:32 -06:00
Neil Dorin
183879f1c4 feat: Add ApplyLayout method to IHasScreensWithLayouts
Introduced a new method `ApplyLayout` in the `IHasScreensWithLayouts` interface. This method enables the application of a specific layout to a screen using the provided screen ID and layout index. XML documentation has been added to clarify its purpose and parameters.
2025-06-18 11:11:12 -06:00
Neil Dorin
f3159738ce feat: Enhance layout and window configuration classes
Added `LayoutType` and `Windows` properties to the `LayoutInfo` class. Introduced a new `WindowConfig` class with `Label` and `Input` properties to represent window configurations within a layout.
2025-06-17 20:33:25 -06:00
Neil Dorin
2c5cae9f41 fix: Rename PresenterTrackMode to ePresenterTrackMode
Updated the enum name to reflect new naming conventions and potentially broader categorization within the codebase.
2025-06-17 19:18:30 -06:00
Neil Dorin
7178d8e284 featr: Add PresenterTrackMode enum to IPresenterTrack.cs
Introduces a new enumeration `PresenterTrackMode` that defines four tracking modes for the Cisco codec's Presenter Track feature: `Off`, `Follow`, `Background`, and `Persistent`. Each mode includes a summary comment for clarity.
2025-06-17 19:17:34 -06:00
Neil Dorin
af98a92f8c (force-patch): generate new build to test updated workflow nuget push issues 2025-06-17 19:01:13 -06:00
Neil Dorin
0a6896910d feat: Add screen/layout management and codec tracking features
Introduced new interfaces and classes for screen and layout management, including `IHasScreensWithLayouts`, `ScreenInfo`, and `LayoutInfo`. Enhanced `IPresenterTrack` and `ISpeakerTrack` interfaces with additional properties and methods for managing presenter and speaker tracking. Added `IHasCodecRoomPresets` interface for room preset management and updated `CodecRoomPreset` class with a new constructor.
2025-06-17 18:35:36 -06:00
Neil Dorin
9b1dd099f6 feat: Add IPresenterTrack and ISpeakerTrack interfaces
Introduced two new interfaces, `IPresenterTrack` and `ISpeakerTrack`, in the `PepperDash.Essentials.Devices.Common.Codec.Cisco` namespace. These interfaces provide properties and methods for managing presenter and speaker tracking functionalities in Cisco codecs, including availability, status feedback, and control methods.
2025-06-17 16:47:09 -06:00
aknous
3f5269de2f Merge pull request #1275 from PepperDash/mobile-control-direct-cs
Access MC from CS LAN
2025-06-06 17:32:15 -04:00
Andrew Welker
60f1adcd35 docs: fix spelling error
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-06 15:02:42 -05:00
Andrew Welker
12c8660015 feat: return correct config for CS processors
When MC is running on a processor with a control subnet and a user
attempts to access the UI via the control subnet, the return _config.local.json
needs to reflect the CS LAN IP address for the processor rather than the LAN IP Address.

In order to accomplish this, there will now be 2 config files written to disk on startup:
1. the original file, /user/programX/mcUserApp/_local-config/_config.local.json
2. A file with the correct CS IP Address /user/programX/mcUserApp/_local-config/_config.cs.json

When a user requests the _config.local.json, the processor will compare the remote IP address
with the CS LAN IP Address and determine if they're in the same subnet, the assumption being
that if the IP Addresses are in the same subnet, then the app or end user is on the CS LAN and needs the CS config.
If the addresses are in the same subnet, then the contents of the _config.cs.json will be returned.
2025-06-06 14:58:26 -05:00
Andrew Welker
ec6aeb17f6 Merge pull request #1271 from PepperDash/temp-to-dev
Temp to dev
2025-05-19 16:52:45 -05:00
Neil Dorin
e7c3fcbbd9 Merge pull request #1270 from PepperDash/dsp-base-fix
dsp base fix
2025-05-14 12:04:41 -06:00
Andrew Welker
0c7ec82529 fix: initialize dictionaries
fix #1167
2025-05-14 12:25:22 -05:00
Andrew Welker
feb99ecbb6 fix: use correct join for preset select 2025-05-14 12:25:21 -05:00
Neil Dorin
91dc655103 Merge pull request #1269 from PepperDash/camera-preset-fix 2025-05-14 09:42:11 -06:00
Andrew Welker
bf31fb10eb fix: use correct join for preset select 2025-05-14 10:39:42 -05:00
equinoy
d78b9ea313 Merge pull request #1267 from PepperDash/DGE-1000
feat: add support for mcdge1000 type in MobileControlTouchpanelContro…
2025-05-09 16:16:49 -04:00
equinoy
15172a5509 feat: add support for mcdge1000 type in MobileControlTouchpanelController 2025-05-09 16:06:33 -04:00
Neil Dorin
5e21bad596 Merge pull request #1266 from PepperDash/temp-to-dev 2025-05-02 12:23:51 -06:00
Neil Dorin
a4de9f2241 Merge pull request #1265 from PepperDash/routing-clear-fix 2025-05-02 11:30:35 -06:00
Andrew Welker
13cd84b73d docs: add xml comments for Essentials routing 2025-05-02 12:27:16 -05:00
Andrew Welker
81a01b7960 fix: add correct interfaces for feedback 2025-05-02 12:16:59 -05:00
Andrew Welker
d9dc70bea2 fix: add ClearRoute method 2025-05-02 11:23:20 -05:00
Neil Dorin
2368f0c8cc Merge pull request #1262 from PepperDash/temp-to-dev 2025-04-28 11:37:01 -06:00
Neil Dorin
4a77955987 Merge pull request #1261 from PepperDash/add-docs-pages 2025-04-28 11:06:23 -06:00
Andrew Welker
a7e4d1b7c1 docs: correct spelling
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-28 11:59:28 -05:00
Andrew Welker
6295e10c08 docs: correct spelling
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-28 11:24:34 -05:00
Andrew Welker
b9553f486a docs: use correct capitalization for SSH
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-28 11:24:13 -05:00
Andrew Welker
e08f250ee8 docs; correct spelling
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-28 11:23:06 -05:00
Andrew Welker
e38ac6bab5 docs: correct spelling
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-28 11:22:51 -05:00
Andrew Welker
aaee3d1317 ci: set docs branch to main only 2025-04-28 11:18:58 -05:00
Andrew Welker
c40acca47a docs: use correct path for CI 2025-04-28 11:04:36 -05:00
Andrew Welker
b128199d25 docs: refactor docs layout and add logos 2025-04-28 10:59:08 -05:00
Andrew Welker
cc9492938b docs: first pass at using docfx 2025-04-25 21:39:45 -05:00
Neil Dorin
be58a0bc29 Merge pull request #1260 from PepperDash/temp-to-dev 2025-04-24 09:32:32 -06:00
AECohn
e8276c4165 Merge pull request #1259 from PepperDash/routing-updates
fix: check for `SecondaryAudio | Video`
2025-04-24 11:13:05 -04:00
Andrew Welker
817b5eccb5 fix: explicit flag checks for clarity
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-24 10:08:29 -05:00
Andrew Welker
702443f953 fix: check for SecondaryAudio | Video
In some scenarios when working with NVX, a route is intended to use the
secondary audio (NAX) path instead of the primary audio path. A route
that's both `Video` and `SecondaryAudio` should be considered a
dual-path route instead of a single path.
2025-04-24 09:55:22 -05:00
Neil Dorin
8406f69e0d Merge pull request #1257 from PepperDash/temp-to-dev 2025-04-18 11:46:18 -06:00
Andrew Welker
1306247c32 Merge pull request #1256 from PepperDash/lighting-interface-fix
fix: ILightingScenesDynamic inherits from ILightingScenes
2025-04-18 12:44:12 -05:00
Andrew Welker
d3d7b400ae Merge branch 'selectable-items-interfaces' into lighting-interface-fix 2025-04-18 12:29:36 -05:00
Andrew Welker
9db980ead1 fix: ILightingScenesDynamic inherits from ILightingScenes 2025-04-18 11:36:05 -05:00
Andrew Welker
c9c3a74f2f fix: add ISelectableItems<TKey,TValue> interface 2025-04-17 09:19:34 -05:00
Andrew Welker
116d83394a Merge pull request #1255 from PepperDash/temp-to-dev
Temp to dev
2025-04-14 11:15:27 -05:00
Neil Dorin
d99095e8ce Merge pull request #1254 from PepperDash/lighting-scene-updates 2025-04-14 09:53:18 -06:00
Andrew Welker
60e705ea8b refactor: change accessiblity & naming for ILightingScenesMessenger 2025-04-14 10:49:27 -05:00
Andrew Welker
e86ab8fa8b fix: update lighting interfaces & messenger
Added the `ILightingScenesDynamic` interface to add an event for devices that support retrieving scene data from the lighting system at runtime. Also added the `sortOrder` property for the `LightingScene` type to allow for control over the sort order of scenes on the UI
2025-04-14 10:45:43 -05:00
Neil Dorin
9b8e452eb4 Merge pull request #1251 from PepperDash/temp-to-dev 2025-04-11 14:00:02 -06:00
Neil Dorin
d8c7e3cfc7 Merge pull request #1250 from PepperDash/generic-comm-monitor-issues 2025-04-11 12:49:39 -06:00
Andrew Welker
eeb106c489 fix: use semaphore for thread safety in Start method 2025-04-11 13:45:04 -05:00
Andrew Welker
c1d62ea5d4 chore: update local build version to 2.4.0-local 2025-04-11 13:44:26 -05:00
Neil Dorin
9148cfd819 Merge pull request #1249 from PepperDash/generic-comm-monitor-issues 2025-04-11 12:32:12 -06:00
Andrew Welker
60550caf99 fix: add lock for threadsafety
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-11 13:26:01 -05:00
Neil Dorin
c9d86bd5dd Merge pull request #1248 from PepperDash/temp-to-dev 2025-04-11 11:55:35 -06:00
Andrew Welker
59baa74dd7 fix: multiple messages no longer sent
Due to how the `BeginPolling` method was written and being called, there
were situations where multiple PollTimers were created, causing there to
be multiple messages sent to the end point for each poll cycle.
2025-04-11 12:13:26 -05:00
Neil Dorin
bf31bf9e93 Merge pull request #1247 from PepperDash/messenger-fixes
ISelectableItemsMessenger & logging updates
2025-04-09 12:33:04 -06:00
Andrew Welker
ee8776cfb1 chore: remove unnecessary constructor overload 2025-04-09 12:04:32 -05:00
Andrew Welker
0b59990532 fix: add IHasInputsMessenger
In order to match up with how existing front-end apps are expecting to
recieve data for devices that implement the `IHasInputs<T>` interface,
there is now an IHasInputsMessengers that is implemented for devices
that implement `IHasInputs<string>`, `IHasInputs<int>` or
`IHasInputs<byte>` interfaces.
2025-04-09 11:48:05 -05:00
Andrew Welker
8d3fd343f1 fix: remove extraneous param for message logging 2025-04-09 10:53:42 -05:00
Andrew Welker
372274d9fa fix: add constructor that takes IHasInputs<T>
In order to satisfy the requirements for the `MessengerBase` class, the
`ISelectableItemsMessenger` class needs to take an `IHasInputs<T>`, as that is
the device that implements `IKeyName`. We may want to consider adding a
`IHasInputsMessenger` specifically for those devices that implement that
interface vs the `ISelectableItemsMessenger`.
2025-04-09 10:49:31 -05:00
Neil Dorin
b2b257020f Merge pull request #1246 from PepperDash/temp-to-dev 2025-04-08 13:48:10 -06:00
Neil Dorin
403c03491c Merge pull request #1245 from PepperDash/fix-add-client
Update `mobileadduiclient` command & `CrestronGenericBaseDevice.CustomActivate` method
2025-04-08 12:47:08 -06:00
Andrew Welker
3770c2a47d fix: call the EssentialsDevice custom activate
`CrestronGenericBaseDevice` was NOT calling `base.CustomActivate()` in
it's `CustomActivate` override, causing the
`CreateMobileControlMessengers` method to not be called as expected when
plugin devices were inheriting from `CrestronGenericBaseDevice` or
`CrestronGenericBridgeableBaseDevice`.
2025-04-08 13:35:58 -05:00
Andrew Welker
5f4a1f768e fix: check for grant code in mobileadduiclient
If the grant code was not provided, the `mobileadduiclient` console
command would fail silently. The command now checks that the correct
number of arguments was provided and prints an error to the console of
one is missing
2025-04-08 13:32:50 -05:00
Neil Dorin
6f58e18d14 Merge pull request #1244 from PepperDash/temp-to-dev
Temp to dev
2025-04-04 10:30:10 -06:00
Neil Dorin
7eed7866f1 Merge pull request #1243 from PepperDash/routing-fixes
Routing fixes
2025-04-04 09:50:27 -06:00
Andrew Welker
c5403f33c5 fix: add previous condition back to the flag check
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-04 10:49:04 -05:00
Andrew Welker
c9f10ecb90 build: delete clz files on build 2025-04-04 08:09:42 -05:00
Andrew Welker
ef2da21c2a chore: remove old InRoomPc class
class has moved to Devices.Common library
2025-04-04 08:09:32 -05:00
Andrew Welker
b0920746d1 feat: check base folder for cplz and ir files
In an effort to make things easier for devs and end users, the plugin loading logic will now find .cplz files that are in the `Global.FilePathPrefix` base folder (/user/program{programNumber}/` on an appliance, `/user/` on a server). The same applies for IR files. This should make it so that individual plugin cplzs can be loaded via the VC-4 web interface.
2025-04-04 08:09:24 -05:00
Andrew Welker
b531d724ff fix: add OverrideType property to TieLineConfig
The TielineConfig had no property for the `type` property from an Essentials configuration file to be deserialized into. This has been corrected so that the `type` property in a Tieline JSON configuration is now respected and used to build the tieline.
2025-04-04 00:04:53 -05:00
Andrew Welker
1b17d92ee0 fix: Essentials Routing now checks for SecondaryAudio if necessary
To support NVX Routing, checking for the SecondaryAudio routing type is necessary to find the correct path through the system for audio.
2025-04-04 00:03:21 -05:00
Neil Dorin
60fc0298ec Merge pull request #1242 from PepperDash/temp-to-dev
Temp to dev
2025-04-02 11:14:45 -06:00
Neil Dorin
2c0739df4b Merge pull request #1241 from PepperDash/release
Release
2025-04-02 11:12:48 -06:00
Andrew Welker
b77fc3647a Merge pull request #1240 from PepperDash/codec-messenger-issues
Codec messenger serialization issues
2025-04-02 12:05:17 -05:00
Andrew Welker
1fe8993db3 fix: ignore CameraBase routing port list
The `RoutingPortCollection` type appears to not be currently
serializable. If a class that contains this collection is going to be
serialized, the collection should have the `JsonIgnore` attribute added.
If the list is needed, use a conversion object and convert it to a
regular list.
2025-04-02 11:56:13 -05:00
Andrew Welker
f735f7377d fix: bring modifications made to plugin over 2025-04-02 10:00:33 -05:00
Neil Dorin
4e43565c1a Merge pull request #1239 from PepperDash/mc-fixes 2025-04-02 08:16:02 -06:00
Andrew Welker
97e157b5b6 fix: bring modifications made to plugin over 2025-04-02 08:39:08 -05:00
Andrew Welker
157ef3397f Merge pull request #1238 from PepperDash/webview-eventhandler 2025-04-02 07:49:37 -05:00
aknous
16c39b5201 feat: adds event handler to IHasWebView 2025-04-01 23:54:49 -04:00
Neil Dorin
8f278f4ec2 Merge pull request #1236 from PepperDash/temp-to-dev
Temp to dev
2025-04-01 11:35:03 -06:00
Jason DeVito
3411fe0cf3 Merge pull request #1235 from PepperDash/fix-volume-add-again
fix: move current volume action creation after power registrations
2025-04-01 10:09:54 -05:00
Andrew Welker
6713ea53f2 fix: move current volume action creation after power registrations 2025-04-01 09:44:18 -05:00
Neil Dorin
a81f92481c Merge pull request #1234 from PepperDash/temp-to-dev 2025-04-01 08:07:55 -06:00
Andrew Welker
607f6084de Merge pull request #1233 from PepperDash/release
Release
2025-04-01 09:03:46 -05:00
Andrew Welker
1625cbe6de Merge pull request #1232 from PepperDash/feature/IHasOsd
Add IHasWebview interface
2025-04-01 08:56:01 -05:00
Neil Dorin
63e6829a03 Merge pull request #1231 from PepperDash/temp-to-dev 2025-03-31 12:28:30 -06:00
Neil Dorin
d3719ee505 Merge pull request #1230 from PepperDash/room-loading-protections 2025-03-31 11:58:14 -06:00
Andrew Welker
041059210c fix: add null check for room before adding to DevManager 2025-03-31 12:53:44 -05:00
Neil Dorin
1a45ef5d0f Merge pull request #1229 from PepperDash/temp-to-dev 2025-03-28 11:14:10 -06:00
Andrew Welker
65a0743dac Merge pull request #1228 from PepperDash/add-routes
fix: add routes to get routing ports & all defined routes
2025-03-28 10:50:04 -05:00
Andrew Welker
32e090648c fix: add missing parameter for GetRoutesHandler 2025-03-28 10:41:21 -05:00
Andrew Welker
bb045ba06b fix: add base URL to routes response 2025-03-28 10:32:16 -05:00
Andrew Welker
474b2eb647 fix: add routes to get routing ports & all defined routes 2025-03-28 10:25:17 -05:00
Andrew Welker
e6cf2794bc Merge pull request #1227 from PepperDash/temp-to-dev 2025-03-26 14:29:27 -05:00
Andrew Welker
09dd8f0bcd Merge pull request #1226 from PepperDash/release
2.1.0
2025-03-26 13:28:43 -05:00
Andrew Welker
99c6163c0e Merge pull request #1225 from PepperDash/feature/move-portal-core
Bring Portal Core and Mobile Control into Essentials
2025-03-26 10:14:36 -05:00
Andrew Welker
90aa4a5d62 feat: remove IHasInputs<T,R> interface
This has been replaced by the `IHasInputs<T>` interface and was marked to be removed in the 2.0.0 release.
2025-03-26 08:55:55 -05:00
Andrew Welker
8b3eda1d18 refactor: make messenger constructors more consistent
Some constructors for messengers were taking Device rather than the specific type they needed.
2025-03-26 08:55:07 -05:00
Andrew Welker
6c710dd209 chore(force-patch): increment patch version 2025-03-26 00:12:37 -05:00
Andrew Welker
7629836732 feat: add overloads for specific levels
In an effort to make it easier to use the logging mechanism, I added 4 overloaded methods for each level to allow for logging exceptions at any level, not just error.

I also added overloads for each level to the extensions so that an exception can be logged at any level, not just error.
2025-03-26 00:01:06 -05:00
Andrew Welker
a5bc79c469 chore: update some logging methods 2025-03-25 23:58:51 -05:00
Andrew Welker
26116d0495 feat: move PD Core into Essentials 2025-03-25 23:22:18 -05:00
Andrew Welker
cdafaf7586 build(force-patch): remove cplz generation
CPLZs are not required for using Essentials as a dependency, and just
adds noise that's not required.
2025-03-25 22:54:47 -05:00
Andrew Welker
222c2f6fe2 chore: more miscellaneous cleanup 2025-03-25 22:52:32 -05:00
Andrew Welker
19d0bc73c8 fix: remove LightingBase from core 2025-03-25 22:50:31 -05:00
Andrew Welker
277771d154 chore: miscellaeneous cleanup 2025-03-25 22:46:41 -05:00
Andrew Welker
789111cb9a style: run code cleanup & apply VS suggestions 2025-03-25 22:14:15 -05:00
Andrew Welker
4d98191fa7 chore: remove obsolete log methods 2025-03-25 21:55:37 -05:00
Andrew Welker
f6f731b470 chore: remove 4SERIES compiler directive 2025-03-25 10:21:17 -05:00
Andrew Welker
8316ee22b6 build(force-patch): change names of MC packages
In order to prevent eventual version collision and confusion, and to
allow for deprecation of existing packages, the names of the MC packages
are now reflective of the fact that they are no longer
pluginsbuild(force-patch): change names of MC packages

In order to prevent eventual version collision and confusion, and to
allow for deprecation of existing packages, the names of the MC packages
are now reflective of the fact that they are no longer plugins.
2025-03-25 00:20:56 -05:00
Andrew Welker
aebc694da7 build(force-patch): use version in directory.build.props 2025-03-25 00:16:23 -05:00
Andrew Welker
fe2cd573e5 feat: remove DisplayBase from Core 2025-03-25 00:10:22 -05:00
Andrew Welker
af0855cea3 build(force-patch): publish mc messenger package 2025-03-24 23:36:17 -05:00
Andrew Welker
5ac9efb1fb Merge branch 'development' into feature/move-mc 2025-03-24 22:43:59 -05:00
Andrew Welker
7318dbb04e ci(force-patch): add skip package check prop 2025-03-24 22:39:52 -05:00
Andrew Welker
f6fdc14059 feat: move MC into Essentials
In order to solve some dependency issues that keep cropping up, MC
should be moved back into the Essentials repo and loaded automatically
on startup. This will allow for all plugins that use the MC Messengers
library to use the same version without fear of overwriting a dll due to
loading of plugin libraries.
2025-03-24 22:28:27 -05:00
Andrew Welker
7046205e57 Merge pull request #1224 from PepperDash/feature/inclusive-poll 2025-03-24 14:50:26 -05:00
Jonathan Arndt
d7499662de fix: revise 4-series-caller.yml to include bypassPackageCheck bool. 2025-03-24 11:01:12 -07:00
Jonathan Arndt
fdb04286d6 fix: correct event subscription logic in GenericCommunicationMonitor and poll inclusively. 2025-03-23 16:36:41 -07:00
aknous
2c28b57806 fix: removes public access modifier from WebviewIsVisible prop 2025-03-20 00:42:30 -04:00
aknous
8762f84548 feat: adds WebviewVisible property to IHasWebView 2025-03-20 00:40:39 -04:00
aknous
b0a68f38f5 feat: adds bypassPackageCheck to github workflow 2025-03-18 13:18:07 -04:00
aknous
46887579e4 feat: simplifies IHasOsd ShowWebView method signature 2025-03-18 12:35:13 -04:00
Neil Dorin
3d91723ab0 Merge pull request #1221 from PepperDash/test-ci
Cleanup leftover files
2025-03-12 12:55:42 -06:00
Andrew Welker
4dcb3946c5 chore: remove unnecessary files 2025-03-12 13:54:13 -05:00
Andrew Welker
a288fc5890 fix: increment minor version 2025-03-12 13:41:21 -05:00
Andrew Welker
9ef8698387 feat: increment minor version 2025-03-12 13:38:52 -05:00
Andrew Welker
daccf9eb77 chore(force-patch): increment patch version 2025-03-12 13:37:13 -05:00
Andrew Welker
b095d926d2 Merge branch 'development' into feature-2.0.0/IHasOsd 2025-03-12 13:29:00 -05:00
Neil Dorin
6333443ecb Merge pull request #1220 from PepperDash/get-beta-build 2025-03-12 12:27:31 -06:00
Andrew Welker
51414e7d26 chore(force-patch): increment patch version 2025-03-12 13:23:40 -05:00
aknous
7cebf861cc build(force-patch): force rebuild 2025-03-12 14:14:11 -04:00
aknous
2e0b003118 build(force-patch): force build 2025-03-12 13:31:54 -04:00
aknous
bada7e3a25 feat: renames interface 2025-03-12 11:00:44 -04:00
aknous
67a7422cd0 feat: adds IHasOSD interface 2025-03-11 23:33:59 -04:00
Andrew Welker
3222c73282 Merge pull request #1218 from PepperDash/update-dev
Update dev
2025-03-07 11:43:16 -06:00
Andrew Welker
5df7b57c43 Merge pull request #1217 from PepperDash/release
Essentials 2.0.0
2025-03-07 11:25:39 -06:00
Andrew Welker
7631c7a8a1 build(force-patch): update PD Core to 2.0.1 2025-03-06 13:53:41 -06:00
Andrew Welker
16e2529378 feat: update to PD Core 2.0.0 2025-03-06 10:04:19 -06:00
Andrew Welker
8f485071c6 chore(force-patch): increment patch version 2025-03-05 18:51:59 -06:00
Andrew Welker
4eabd82878 build(force-update): fix build targets to handle files correctly 2025-03-05 18:50:40 -06:00
Andrew Welker
edfe77cd02 build(force-patch): update PD Core & Crestron libraries 2025-03-05 17:37:48 -06:00
Andrew Welker
fd593d7117 Merge pull request #1216 from PepperDash/release-2.0.0
Merge 2.0.0 stuff into development
2025-03-05 13:51:28 -06:00
Andrew Welker
a141b365ab fix: add missing using for System.Text 2025-03-05 13:40:38 -06:00
Andrew Welker
68ddd76ba5 chore!: update version to 2.0.0
BREAKING CHANGE: remove support for .NET 6 for Essentials 2.0.0
2025-03-05 13:33:49 -06:00
Andrew Welker
9330ebeae2 chore: remove references to Essentials Template UI 2025-03-05 13:32:21 -06:00
Andrew Welker
b398e73024 chore!: remove .NET 6 support for v2.0.0
BREAKING_CHANGE: remove .NET 6 support for v2.0.0
2025-03-05 13:26:50 -06:00
Andrew Welker
7a2b2eece6 ci(force-patch): use same workflow as PD Core for building 2025-03-05 13:24:28 -06:00
Andrew Welker
543176bb08 Merge branch 'development' into release-2.0.0 2025-03-05 13:07:51 -06:00
Jason T Alborough
237fff5398 Merge pull request #1214 from PepperDash/feature-2.0.0/concurrent-routing-issues
Fix issues with concurrent routing actions
2025-02-20 13:19:56 -05:00
Andrew Welker
b2eab21fbd Merge pull request #1211 from PepperDash/hotfix-2.0.0/room-combiner-syncronous-events
fix: improve error handling and await device actions in RoomCombinati…
2025-02-19 17:20:59 -06:00
Andrew Welker
f2545fb1cf Merge pull request #1212 from PepperDash/feature-2.0.0/versiport-room-combiner
Enable using Versiports with Room Combiner
2025-02-19 17:20:40 -06:00
Andrew Welker
27072e3475 fix: #1213 remove key from GenericSoftCodec routing port keys 2025-02-19 17:10:18 -06:00
Andrew Welker
e4755ed9df Merge branch 'feature-2.0.0/concurrent-routing-issues' of https://github.com/PepperDash/Essentials into feature-2.0.0/concurrent-routing-issues 2025-02-19 17:00:45 -06:00
Andrew Welker
316867caf8 chore: update to PD Core 2.0.0-alpha-451
Catch the `SshOperationTimeoutException` and handle it differently.
2025-02-19 16:49:22 -06:00
jtalborough
d8fd774324 Merge branch 'hotfix-2.0.0/room-combiner-syncronous-events' into feature-2.0.0/concurrent-routing-issues 2025-02-19 15:11:46 -05:00
Andrew Welker
e0058d8cfe chore: update PD Core to 2.0.0-alpha-450
Updated to Renci for SSH
2025-02-19 13:24:52 -06:00
Andrew Welker
a055d06bc6 chore: add some logging for route queue processing 2025-02-19 11:18:00 -06:00
Andrew Welker
66cb592c70 chore: add missing curly brace 2025-02-19 11:10:35 -06:00
Andrew Welker
d53a5607e2 chore: reorg methos in routing Extensions class 2025-02-19 11:07:41 -06:00
Andrew Welker
34f59f1410 fix: move ReleaseRoute & RunRouteRequests to use a queue 2025-02-19 11:06:56 -06:00
Andrew Welker
261779d4c4 fix: actually log exception correctly 2025-02-18 14:04:30 -06:00
Andrew Welker
30d5e2b081 fix: add more exception handling
Also add null check for `AddRouteDescriptor` method
2025-02-18 14:00:53 -06:00
Andrew Welker
5516ed16c3 fix: use null instead of empty string to make it clearer what's happening 2025-02-18 12:02:52 -06:00
Andrew Welker
8108b9dfdb fix: attempt to get better logging for exception
Fixed some issues with log messages that were not formatted correctly for Serilog.
2025-02-18 11:59:26 -06:00
Nick Genovese
fb4f1482c7 fix: small null check in the release and make route method 2025-01-31 19:33:31 -05:00
Nick Genovese
54dcb5de08 feat: implement IPartitionStateProvider to Generic VersaPortInput 2025-01-30 15:21:12 -05:00
jtalborough
4ef481375c fix: improve error handling and await device actions in RoomCombinationScenario 2025-01-28 09:00:21 -05:00
Nick Genovese
d8a88b2a07 Merge branch 'development-2.0.0' into feature-2.0.0/emergencyOSD 2025-01-10 07:31:35 -05:00
Andrew Welker
cc724ddf19 Merge pull request #1210 from PepperDash/hotfix-2.0.0/release-routes-clears-the-routes
Hotfix 2.0.0/release routes clears the routes
2024-12-30 13:33:54 -06:00
Nick Genovese
e29e800d9d fix: now pushes the tag whenever not rc 2024-12-10 07:52:43 -05:00
Nick Genovese
134e8ba02e fix: remove null route when releasing route 2024-12-10 07:44:10 -05:00
Neil Dorin
a83ba444d3 Merge pull request #1209 from PepperDash/feature-2.0.0/more-cooldown-fixes 2024-11-22 08:34:13 -07:00
Andrew Welker
f4c5e6fbeb fix: remove event sub for route request
When route requests made during a destination's cooldown cycle were handled, the event subscription was *NOT* being removed, resulting in the request being run on *EVERY* subsequent cooldown complete event.
2024-11-22 09:14:07 -06:00
Andrew Welker
35d7994cc8 Merge pull request #1208 from PepperDash/feature-2.0.0/cooldown-exception
fix: add try/catch for routing cooldown handler
2024-11-20 15:52:24 -06:00
Andrew Welker
c3e9d654c9 fix: add try/catch for routing cooldown handler
Fixed log statement to handle when a value is null
2024-11-20 15:47:33 -06:00
Andrew Knous
f68b1e9e49 feat: cleans up code, adds versiport comment, changes ShowEmergencyMessage arg name to "url" 2024-11-19 09:19:48 -08:00
Andrew Knous
cd81b8af73 feat: adds roomKey to ShowEmergencyMessage 2024-11-14 17:00:21 -05:00
Andrew Knous
cd52c245a6 feat: adds emergency OSD support 2024-11-14 16:23:31 -05:00
Neil Dorin
0b60f53d0e feat: Adds IEssentialsRoomEmergency interface and implements on contact closure device to provide state 2024-11-13 12:15:25 -07:00
Andrew Welker
ffed2dea8a Merge pull request #1206 from PepperDash/feature-2.0.0/catv-presets
docs: adds debug statement to print preset count
2024-10-31 09:54:47 -05:00
Andrew Welker
936dce2046 Merge pull request #1204 from PepperDash/feature-2.0.0/fix-version-info
feat: adds sdi in/out port names
2024-10-31 09:53:46 -05:00
Andrew Welker
b33704eabe Merge pull request #1203 from PepperDash/feature-2.0.0/bridge-issues
fix: joins in join maps get added correctly to a bridge
2024-10-30 13:27:25 -05:00
Andrew Welker
aca6fe9af5 chore: remove extraneous call 2024-10-30 13:20:43 -05:00
Andrew Welker
332faaa9cc fix: joins in join maps get added correctly to a bridge
When Essentials moved to using `System.Reflection` instead of the Crestron classes, there were some leftover `GetType` calls that were no longer necessary. These extra calls were preventing things from getting the correct type.

Join Map printing was also fixed to print out in an actual readable fashion.
2024-10-30 13:18:36 -05:00
Nick Genovese
4449077a39 Merge pull request #1202 from PepperDash/feature-2.0.0/display-feedback-fix
Set CurrentSourceKey correctly & in the correct order
2024-10-30 11:32:29 -04:00
Andrew Welker
86ba9e0f16 fix: set currentSourceKey & currentSource in order 2024-10-30 10:19:55 -05:00
Andrew Welker
db2d8a213d fix: get order of source & source key correct 2024-10-29 10:59:26 -05:00
Andrew Welker
590e16298c fix: use correct key for destination CurrentSourceInfoKey 2024-10-28 16:41:30 -05:00
Andrew Welker
1a11e9019c Merge pull request #1201 from PepperDash/feature-2.0.0/routing-feedback-manager-nullref
Feature 2.0.0/routing feedback manager nullref
2024-10-25 08:33:46 -05:00
Nick Genovese
0e16dff90c fix: checks the routing output port for null 2024-10-24 20:52:24 -04:00
Andrew Welker
d11827bc7b fix: remove verbose logging for feedback manager 2024-10-18 15:04:08 -05:00
Andrew Welker
631dd2b00d fix: check for nulls in SwitchingDevice property 2024-10-18 11:09:15 -05:00
Andrew Welker
639cd2abfb Merge pull request #1197 from PepperDash/feature-2.0.0/add-cpz-to-builds
Feature 2.0.0/add cpz to builds
2024-10-18 10:46:05 -05:00
Andrew Welker
f04f70495f fix: check for nulls in route switch descriptors 2024-10-18 10:43:53 -05:00
Neil Dorin
fa38e8a9a8 feat: Adds mechanism to track initialization status of EssentialsDevice as well as an event on DeviceManager to notify when all devices initialized. Room combiner now waits for all initialize before setting current scenario. 2024-10-04 10:33:09 -06:00
Neil Dorin
f351c036ed Merge branch 'feature-2.0.0/temp-humidity-interfaces' into feature-2.0.0/generic-sink-fix 2024-09-26 15:04:34 -06:00
Neil Dorin
0a7da79356 fix: Allows both BytesReceived and TextReceived to both fire on the ComPortController 2024-09-26 15:03:31 -06:00
Andrew Welker
82ebf45921 fix: use correct class for generic sink factory 2024-09-26 08:44:27 -05:00
Neil Dorin
d0dbe986f3 feat: Adds ITemperatureSensor and IHumiditySensor interfaces 2024-09-13 13:17:51 -06:00
Neil Dorin
aa503f3b29 feat: Allows for the ItemKey to be left undefined or empty and the ParentDeviceKey to be used on it's own instead. 2024-08-28 13:16:27 -06:00
Neil Dorin
90251d92df fix: adds condition to handle legacy and current portal URL structures and adds null check for getting list types in basic config helper methods 2024-08-26 12:47:31 -06:00
jtalborough
778af651d0 feature: adds pack for release builds 2024-08-05 10:40:33 -04:00
jtalborough
2e31719a84 fix: artifact path 2024-08-05 10:39:04 -04:00
jtalborough
a96fb21898 feature: add dotnet pack 2024-08-05 09:52:57 -04:00
jtalborough
3f45372e1e feature: add cpz test 2024-08-02 15:55:38 -04:00
jtalborough
60dbaf547d fix: artifact path 2024-08-02 11:00:07 -04:00
Andrew Welker
941e537d53 Merge pull request #1196 from PepperDash/feature-2.0.0/routing-cooldown-fixes
Feature 2.0.0/routing cooldown fixes
2024-08-01 15:36:36 -04:00
Andrew Welker
f7c5e18af8 fix: add input port matching to route descriptors 2024-07-26 11:19:43 -05:00
Andrew Welker
ab73bbf979 fix: remove call to remove routerequest
The routerequest is being removed if the route is successfully made after the display has cooled down. This was an extraneous removal
2024-07-26 06:53:18 -05:00
Andrew Welker
1fb1947158 fix: ReleaseRoute callse ExecuteSwitch with null for input selector
Most devices that implement IRouting will now need to handle the possiblity of a null as the input selector, with the idea being that a null input selector should clear the route to whatever device is selected as the input selector. This may change to a typed value in the future.
2024-07-26 06:51:45 -05:00
Andrew Welker
e374f7b50f fix: add some options to destination type enum 2024-07-26 06:48:23 -05:00
Andrew Welker
7a263a644a fix: devcommstatus response now prints correctly 2024-07-26 06:47:48 -05:00
Andrew Welker
97bd30e9c9 fix: add async/await patterns for activation/deactivation 2024-07-26 06:47:13 -05:00
Neil Dorin
63c653b21f docs: adds debug statement to print preset count 2024-07-24 10:39:52 -06:00
Andrew Welker
c56841d95b fix: attempt to catch a null ref happening in the Route Descriptor 2024-07-22 11:19:34 -05:00
Andrew Welker
64d6df70b0 fix: deactivate all rooms on startup
As part of the Essentials startup process, ALL rooms are activated, meaning there are unnecessary rooms activated. Deactiving them all prior to determining a combination scenario helps keep unecessary activity from happening.
2024-07-22 11:19:08 -05:00
Andrew Welker
d970d806c9 fix: add mutex to prevent multiple scenarios from running at once 2024-07-19 13:09:47 -05:00
Andrew Welker
5a9b876d80 fix: remove route request once the request has been handled 2024-07-18 13:46:40 -05:00
Andrew Welker
fb60683af6 feat: add async method for devjson 2024-07-18 13:44:49 -05:00
Andrew Welker
e7d7b8638e Merge pull request #1195 from PepperDash/feature/video-wall-mode
Feature/video wall mode
2024-07-12 09:20:25 -04:00
Colin Denig
0eb2436fa0 fix: videowall mode raw fb for dm-rmc-4kz-scaler-c 2024-07-11 15:42:25 -04:00
Colin Denig
192991cc65 fix(wip): DmRmc4kzScalerC VideoWallModeFb not working
- reverted PepperDashCode ref for Essentials_DM to unversioned and latest release
- fixed Scaler_OutputChange function params
- added instantiation of VideoWallModeRawFeedback
- feedback event trigger causing null ref exception
TODO:
-[] trace nullref exception cause
-[] Link Fb to Api
2024-07-10 16:45:02 -04:00
mhengeli
421f87db5e feat: add video wall mode command to dm-rmc-4kz-scaler-c 2024-07-09 20:06:49 -04:00
Andrew Welker
b63996b9e6 fix: set partition state to match physical partitions when changing from manual to auto mode 2024-07-08 13:22:18 -05:00
Andrew Welker
bc217a2008 chore: add logging for current scenario searching 2024-07-08 08:44:45 -05:00
Andrew Welker
fec6b0d385 fix: change source for PartitionPresent depending on mode
Added `FireUpdate` calls for feedback on mode change in order to get correct values when changing modes.
2024-07-08 08:41:03 -05:00
Neil Dorin
f3b4c0aa02 fix: resolves issue with incorrect partition state feedback in test mode 2024-06-28 13:56:42 -06:00
Neil Dorin
a3351812cd fix: fixes for room combination in manual/auto mode with actual crestron partition sensors 2024-06-28 13:20:19 -06:00
Neil Dorin
71815eff17 fix: updates mode for partition sensors when mode of room combiner changes 2024-06-28 12:57:06 -06:00
Neil Dorin
c7f4bf1fb2 fix: fixes backing values for eLevelControlType 2024-06-28 12:42:43 -06:00
Neil Dorin
bec3ab8e73 fix: Adds missing initializer for CameraLists 2024-06-28 11:27:22 -06:00
Neil Dorin
c499d2a2eb fix: corrects spelling mistake 2024-06-28 09:01:59 -06:00
Neil Dorin
d9721a362e feat: adds method to set input source type and corresponding enum to ICiscoCodecCameraConfig 2024-06-28 08:55:18 -06:00
Neil Dorin
5fb6f3e117 fix: Fixes namespace for CameraListItem 2024-06-25 22:20:13 -06:00
Neil Dorin
c2fb44a662 feat: adds CameraListKey to EssentialsAvRoomPropertiesConfig 2024-06-25 21:22:49 -06:00
Neil Dorin
0f9bddf4dd fix: updates EssentialsRoomBase to add CameraListKey 2024-06-25 17:58:44 -06:00
Neil Dorin
ddc2491664 feat: updates to IEssentialsRoom for CameraListKey and adds helper method to get camera list from config 2024-06-25 17:15:45 -06:00
Neil Dorin
9a6209f50a feat: Adds basic CamerLists config structure and CameraListItems class 2024-06-25 16:08:44 -06:00
Andrew Welker
49852d25e8 Merge pull request #1194 from PepperDash/feature-2.0.0/routing-extension-changes
Feature 2.0.0/routing extension changes
2024-06-24 16:01:37 -04:00
Andrew Welker
ba4ca20936 fix: add tieline type to output for console & CWS 2024-06-24 14:57:07 -05:00
Andrew Welker
c50726f813 feat: separate audio & video route descriptors
Also cleaned up logging some
2024-06-24 14:54:37 -05:00
Neil Dorin
0aafe8a62e fix: add message to indicate no handler registered when debugging stream data 2024-06-24 13:45:07 -06:00
Neil Dorin
71005940ac fix: catches last condition for destinationPort match 2024-06-21 15:00:17 -06:00
Neil Dorin
e5e79316a6 fix: fixes conditions for specific port and device matches 2024-06-21 14:53:03 -06:00
mhengeli
eb388d28db feat: add scaler mode for videowall set rmc 4kz scaler c 2024-06-21 15:37:28 -04:00
Neil Dorin
5aa1f85df5 feat: adds try catch to devjson execution thread and adds port names for sdi in/out 2024-06-20 11:57:32 -06:00
Andrew Welker
7bac65002d fix: correct issues with method calls 2024-06-19 15:01:25 -05:00
Andrew Welker
ed0141a536 fix: change ports and what's used where 2024-06-19 14:37:04 -05:00
Andrew Welker
25ebcdfb5d feat: update routing methods
Routing methods will now take a source port and destination port. This should solve the issue where a device could have multiple input ports defined in tielines and allow Essentials routing to find a path correctly.
2024-06-19 14:09:59 -05:00
Neil Dorin
b326ccf6c3 feat: adds sdi in/out port names 2024-06-19 13:08:55 -06:00
Neil Dorin
3a56e47c48 fix: updates IHasInputs to remove second generic that is unnecessary. 2024-06-13 10:59:05 -06:00
Neil Dorin
171bd6b1ec fix: removed DefaultDisplay from TwoWayDisplayBase 2024-06-06 12:15:30 -06:00
Neil Dorin
e61fd7777a fix: fixed typenames for mockDisplay 2024-06-06 12:09:35 -06:00
Neil Dorin
027bdd5bf4 Merge 'development-2.0.0' into 'feature-2.0.0/fix-version-info' 2024-05-29 12:10:34 -06:00
Neil Dorin
b876b8123d feat: changes access modifiers to public on SystemMonitorController methods 2024-05-28 14:25:34 -06:00
Neil Dorin
6e05653c6c fix: fixes name for PDCore dll 2024-05-28 14:25:05 -06:00
Neil Dorin
16d32bc720 Merge pull request #1191 from PepperDash/feature-2.0.0/fix-touchpanel-config
Add JSON Props & Control Config
2024-05-28 14:00:19 -06:00
Andrew Welker
0bef5d4b77 build: update PD Core 2024-05-28 14:57:35 -05:00
Andrew Welker
a3c1572a77 build: update PD Core 2024-05-28 14:43:39 -05:00
Andrew Welker
cc06c7bfd8 fix: add nullable props where necessary 2024-05-28 14:43:31 -05:00
Andrew Welker
00a7b25026 build: update PD Core 2024-05-28 13:29:03 -05:00
Andrew Welker
edc916c9d3 fix: add more json props 2024-05-28 13:28:45 -05:00
Andrew Welker
c755ecb16c fix: add correct casing to base touchpanel config 2024-05-28 11:26:25 -05:00
Neil Dorin
a8c36ba243 Merge pull request #1190 from PepperDash/feature-2.0.0/routing-updates
Updates after testing
2024-05-24 15:28:59 -06:00
Andrew Welker
f630d3f410 fix: add missing interfaces 2024-05-24 16:13:35 -05:00
Andrew Welker
35e0662b27 fix: only update CurrentInputPort if it has changed 2024-05-24 16:13:21 -05:00
Andrew Welker
effefc939c fix: minor Web API enhancements
* changed path for DevJson to include the device key instead of requiring it in the body
* Made the `GetRequestBody` method an extension method for the `HttpCwsRequest` class
2024-05-24 16:12:50 -05:00
Andrew Welker
5afdc2effa fix: remove Crestron.SimplSharp.Reflection
Ran into some odd exceptions loading on a VC-4 instance, and changing to System.Reflection solved them.
2024-05-24 16:11:20 -05:00
Neil Dorin
448cc273ec feat: Adds PepperDashCoreAssembly 2024-05-23 14:42:31 -06:00
Neil Dorin
0a2aaa693f feat: Replaces Crestron.SimplSharp.Reflection with System.Reflextion and updates the way essentials plugin versions are stored and retrieved 2024-05-23 14:11:42 -06:00
Neil Dorin
1ebee58ad6 Merge pull request #1189 from PepperDash/feature-2.0.0/routing-updates
Add routing feedback for Essentials Routing
2024-05-23 12:31:49 -06:00
Andrew Welker
3c5fe88e5a fix: correct namespaces to allow plugins to load correctly 2024-05-23 12:45:47 -05:00
Andrew Welker
8255328be1 build: update PD Core 2024-05-23 08:44:51 -05:00
Andrew Welker
4bf026601f feat: get it working 2024-05-23 08:41:19 -05:00
Andrew Welker
888f1b3888 chore: update PD Core version 2024-05-23 08:41:16 -05:00
Andrew Welker
19bd5723c8 feat: add RoutingFeedbackManager
RoutingFeedbackManager keeps track of updates from IRoutingSinkWotjFeedbacl devoces & IRoutingWithFeedback devices to allow for walking tieLines and finding a source.
2024-05-23 08:40:14 -05:00
Andrew Welker
e3e7add5b9 fix: correct build issues 2024-05-23 08:39:09 -05:00
Andrew Welker
dd66de0463 feat: implement feedback manager 2024-05-23 08:39:09 -05:00
Andrew Welker
3823943cd9 feat: add 2 new routing interfaces to allow for getting feedback for routing 2024-05-23 08:39:09 -05:00
Andrew Welker
577e111f26 feat: add properties to control when sources show 2024-05-23 08:38:56 -05:00
Andrew Welker
528fff569d refactor: fix namespaces after move 2024-05-23 08:38:56 -05:00
Andrew Welker
06a6b1caa2 refactor: move routing interfaces into their own files 2024-05-23 08:38:50 -05:00
Neil Dorin
621d848418 feat: adds deviceKey property to LevelControlListItem to synthesize device key 2024-05-22 14:53:01 -06:00
Neil Dorin
2f9038a501 fix: adds initializer for dictionaries 2024-05-21 22:15:50 -06:00
Neil Dorin
983b18d25a fix: fixes type of AudioControlPointLists 2024-05-21 17:28:05 -06:00
Neil Dorin
048004d441 fix: updates IEssentialsRoom and EssentialsRoomBase for missed changes 2024-05-21 17:14:13 -06:00
Neil Dorin
e7e448f02c fix: Switches from LevelControlListKey to AudioControlPointListKey 2024-05-21 17:04:15 -06:00
Neil Dorin
2e61d8d709 fix: Changes LevelControlLists to AudioControlPointLists and modified IHasDspPresets 2024-05-21 16:49:13 -06:00
Neil Dorin
d8d2c5b340 build(force-patch): Updates PD.Core version to support LevelControlList property merge in config 2024-05-16 23:07:13 -06:00
Neil Dorin
7e736ae519 fix: initializes LevelControlLists in config 2024-05-16 22:44:57 -06:00
Neil Dorin
0067e11d3d fix: Adds new property to EssentialsRoomBase and implements consistent default key if no key set 2024-05-16 20:57:38 -06:00
Neil Dorin
7942c91f73 feat: Adds LevelControlListKey to IEssentialsRoom 2024-05-16 20:49:11 -06:00
Neil Dorin
e1638762a1 feat: Adds helper method for getting LevelControlList by key 2024-05-16 20:30:22 -06:00
Neil Dorin
3566400379 fix: Adds LevelControlListKey to EssentialsRoomPropertiesConfig 2024-05-16 20:24:45 -06:00
Neil Dorin
dde85f39a2 fix: combine enum values 2024-05-16 20:20:11 -06:00
Neil Dorin
735433f660 fix: adds missing flags decorator to enum 2024-05-16 17:26:35 -06:00
Neil Dorin
734149960b fix: Adds missing StringEnumConverter 2024-05-16 17:23:38 -06:00
Neil Dorin
cb16f2a505 feat: Adds LevelControlLists to BasicConfig and LevelControlListItem class 2024-05-16 17:17:59 -06:00
Neil Dorin
a2b7a39082 Merge pull request #1188 from PepperDash/hotfix/add-hdcp-control-to-airmedia
Hotfix/add hdcp control to airmedia
2024-05-16 15:31:35 -06:00
Neil Dorin
428f9f34bd Merge pull request #1187 from PepperDash/hotfix/add-hdcp-control-to-airmedia
main <- hotfix/add-hdcp-control-to-airmedia
2024-05-16 11:29:07 -06:00
Jason T Alborough
78e49871c6 Merge branch 'main' into hotfix/add-hdcp-control-to-airmedia 2024-05-16 12:18:08 -04:00
Joshua_Gutenplan
a95d44e405 Merge pull request #1186 from PepperDash/feature-2.0.0/logging-updates
Update PD Core & Crestron Packages
2024-05-15 22:45:04 -07:00
Andrew Welker
12e81af9e6 Merge pull request #1183 from PepperDash/hotfix/bridge-warning-fix
Hotfix/bridge warning fix
2024-05-16 00:38:44 -05:00
Andrew Welker
5820c9d282 fix: set error log level to to verbose on VC-4
Update catch log methods to use updated LogMessage method
2024-05-16 00:38:02 -05:00
Andrew Welker
4d19ecde00 build: update PD Core and dependencies 2024-05-16 00:32:02 -05:00
Andrew Welker
826b7fd6d5 fix: add factory for mock display in Devices.Common 2024-05-15 15:30:54 -05:00
Neil Dorin
cb9eb5dafa Merge remote-tracking branch 'origin/feature-2.0.0/video-codec-interface-uiextensions' into feature-2.0.0/room-combiner-updates 2024-05-14 16:44:30 -06:00
Neil Dorin
eb955aa014 build: updates version of PD.Core --force-patch 2024-05-14 15:32:23 -06:00
Joshua_Gutenplan
2d9ffca78e Merge branch 'feature-2.0.0/room-combiner-updates' into feature-2.0.0/video-codec-interface-uiextensions 2024-05-10 21:36:53 -07:00
Neil Dorin
98f1a09c25 feat: Adds IHasCiscoNavigatorTouchpanel interface 2024-05-10 14:49:37 -06:00
Neil Dorin
a11ad421f0 fix: better implmentation of input select 2024-05-10 13:16:59 -06:00
Neil Dorin
8878ff7ddd chore: renames property to Keys 2024-05-09 16:09:16 -06:00
Neil Dorin
7e4b5f984f feat: Adds IHasAccessoryDevices 2024-05-09 16:07:52 -06:00
Neil Dorin
64ab315142 fix: various updates for room combining from testing 2024-05-09 15:16:35 -06:00
Neil Dorin
c47a93f4d0 Merge remote-tracking branch 'origin/feature-2.0.0/add-screenLift-controls' into feature-2.0.0/room-combiner-updates 2024-05-09 15:15:50 -06:00
Joshua_Gutenplan
5a55a701d6 fix: remove codec interfaces that are only needed in cisco epi now 2024-05-09 12:10:42 -07:00
Andrew Knous
01862ab9aa feat: moves mockdisplay factory from PepperDash.Essentials.Core to PepperDash.Essential.Devices.Common 2024-05-09 13:48:59 -04:00
Andrew Knous
8ec6fa785e feat: adds IProjectorScreenLiftControl and ScreenLiftController 2024-05-09 13:47:46 -04:00
Joshua_Gutenplan
e37c675da1 Merge remote-tracking branch 'origin/feature-2.0.0/room-combiner-updates' into feature-2.0.0/video-codec-interface-uiextensions 2024-05-06 12:55:16 -07:00
Neil Dorin
3ee8cb7ea3 feat: updates IHasInput to remove requirement for SetInputs method (unnecessary) 2024-05-03 13:34:22 -06:00
Neil Dorin
2b6f79b68f feat: updates to Room combiner for use with mobile control 2024-05-02 17:27:34 -06:00
Neil Dorin
65369606a4 feat: updates to room combiner interfaces 2024-05-02 15:00:17 -06:00
Joshua_Gutenplan
e9954b3081 fix: add GetRoomMessenger to IMobileControl 2024-05-02 12:07:00 -07:00
Neil Dorin
bc67a4382b Merge pull request #1185 from PepperDash/feature-2.0.0/tech-password-interface
Feature 2.0.0/tech password interface
2024-05-02 09:55:41 -06:00
Neil Dorin
544a7a2d73 Merge branch 'main' into hotfix/bridge-warning-fix 2024-05-02 09:46:45 -06:00
Neil Dorin
1d843c6c89 Merge pull request #1184 from PepperDash/hotfix/fix-ghidra-specific-verbiage
fix: ghidra specific verbiage
2024-05-02 09:46:23 -06:00
Neil Dorin
c72db72e7e Merge branch 'main' into feature/bridge-warning-fix 2024-04-26 09:58:02 -06:00
AECohn
9e588f4da5 fix: ghidra specific verbiage 2024-04-25 10:42:40 -04:00
Aviv Cohn
4c466b425c fix: Change console message to include "use eiscApiAdvanced" 2024-04-25 10:24:09 -04:00
Jason T Alborough
d0aed1c1c5 Merge pull request #1173 from PepperDash/release/1.16.0
Release/1.16.0
2024-04-23 11:17:09 -04:00
Jason T Alborough
bf966121f9 Merge branch 'main' into release/1.16.0 2024-04-01 13:20:55 -04:00
Andrew Welker
ecadb439b2 Merge pull request #1176 from PepperDash/hotfix/ssh-fix
fix: Update PepperDashCore version to 1.3.3-hotfix-390
2024-03-21 09:59:17 -05:00
jtalborough
8c1553a026 fix: Update PepperDashCore version to 1.3.3-hotfix-390 2024-02-26 13:22:35 -05:00
Andrew Welker
9755724342 Merge pull request #1164 from PepperDash/feature/hdps-dm-event-updates
Feature/hdps dm event updates
2024-02-02 08:53:29 -06:00
Andrew Welker
4d25c420e5 Merge pull request #1161 from PepperDash/hotfix/video-codec-base
fix: clear selected causes thread abort in 3 series
2024-02-02 08:52:47 -06:00
Andrew Welker
3190dacdf8 Merge branch 'development' into feature/hdps-dm-event-updates 2024-02-02 08:34:30 -06:00
Andrew Welker
44add9aac6 Merge branch 'main' into hotfix/video-codec-base 2024-02-02 08:33:47 -06:00
Andrew Welker
5bb6405874 Merge pull request #1169 from PepperDash/hotfix/latest-dbs
Hotfix/latest dbs
2024-02-02 08:33:23 -06:00
Andrew Welker
fed3d7e13a Merge branch 'development' into hotfix/latest-dbs 2024-01-25 14:44:35 -06:00
Andrew Welker
af848b9ca4 Merge pull request #1168 from PepperDash/hotfix/latest-dbs 2024-01-25 14:03:48 -06:00
jtalborough
b52c13d8e8 fix: Update PepperDashCore package version to 1.3.2 2024-01-25 13:50:44 -05:00
jtalborough
26f9118154 chore: update PepperDashCore package version 2024-01-25 13:15:12 -05:00
Neil Dorin
bba3c347c6 Merge pull request #1166 from PepperDash/feature/add-cen-io-com-x02
feat: add cen-io-com102 and cen-io-com202 support
2023-12-19 14:45:02 -07:00
jkdevito
be96adcc06 feat: add cen-io-com102 and cen-io-com202 support 2023-12-19 10:50:55 -06:00
Jason DeVito
b245016420 fix: updated dminputchange event debug message 2023-12-14 17:02:14 -06:00
Jason DeVito
19f2c6aa79 fix: update dm event handlers, adds debug statements 2023-12-14 16:32:22 -06:00
Jason DeVito
533ca05ac2 feat: adds additional dm input/output event cases to event handlers 2023-12-14 12:43:46 -06:00
Jason DeVito
9c7777fbaa Merge pull request #1163 from PepperDash/feature/hdpsxxxx-volume-control
Feature/hdpsxxxx volume control
2023-12-13 16:54:59 -06:00
Jason DeVito
5530c91b75 refactor: HdPsXxxOutputAudioController & HdPsXxxAnalogAuxMixerController
- Updated audio controllers to implement `Volume(Feedback).ShortValue`
per documentation for set and get
- Changed `VolumeLevel` set to private set
- Removed `VolumeLevel` and `IsMuted` set from constructors
2023-12-13 16:01:15 -06:00
Jason DeVito
67e0378806 fix: cleanup debug statements 2023-12-13 12:41:45 -06:00
Jason DeVito
1c5aca03d2 fix: resolves hdPsXxx audio controllers scaleWithLimits exception 2023-12-13 12:36:45 -06:00
Nick Genovese
6f5fa2c3b8 fix: clear selected causes thread abort in 3 series
- the old method was using _directoryCode, _directoryTrilist, _directoryJoinmap which were never initialized
2023-12-12 15:29:41 -05:00
Neil Dorin
3d760cbedc feat: adds support for hdcp input control and feedback on Airmedia devices 2023-11-14 16:28:32 -07:00
Jason DeVito
d713abf614 fix: update hd-ps audio controllers to resolve control issues 2023-11-09 23:36:54 -06:00
Jason DeVito
a64b5240ad feat: add output audio controller 2023-11-03 18:14:53 -05:00
Jason DeVito
c528fecb9a feat(wip): add HdPsXxx audio control 2023-11-02 17:52:27 -05:00
jkdevito
355e9cde12 chore(wip): save updates 2023-11-02 11:55:48 -05:00
jkdevito
1df8d3f617 feat: create branch to add volume control 2023-11-02 11:54:44 -05:00
Neil Dorin
2f1caff815 Merge pull request #1154 from PepperDash/hotfix/mpc3-button-list
fix: list available buttons on startup
2023-10-27 12:53:52 -06:00
Jason DeVito
4da2f25c3d fix: list available buttons on startup 2023-10-27 12:14:07 -05:00
Andrew Welker
d6334538c0 Merge pull request #1142 from PepperDash/feature/ir-bridge-map
Add default IR join map using Crestron IR File naming conventions
2023-10-25 20:38:39 -06:00
750 changed files with 61735 additions and 10087 deletions

13
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.4",
"commands": [
"csharpier"
],
"rollForward": false
}
}
}

View File

@@ -0,0 +1,22 @@
name: Build PepperDash Essentials
on:
push:
branches:
- '**'
jobs:
getVersion:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main
secrets: inherit
build-4Series:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main
secrets: inherit
needs: getVersion
if: needs.getVersion.outputs.newVersion == 'true'
with:
newVersion: ${{ needs.getVersion.outputs.newVersion }}
version: ${{ needs.getVersion.outputs.version }}
tag: ${{ needs.getVersion.outputs.tag }}
channel: ${{ needs.getVersion.outputs.channel }}
bypassPackageCheck: true

View File

@@ -1,94 +0,0 @@
name: Branch Build Using Docker
on:
push:
branches:
- feature-2.0.0/*
- hotfix-2.0.0/*
- release-2.0.0/*
- development-2.0.0
env:
# solution path doesn't need slashes unless there it is multiple folders deep
# solution name does not include extension. .sln is assumed
SOLUTION_PATH: .
SOLUTION_FILE: PepperDash.Essentials
# Do not edit this, we're just creating it here
VERSION: 0.0.0-buildtype-buildnumber
# Defaults to debug for build type
BUILD_TYPE: Debug
# Defaults to main as the release branch. Change as necessary
RELEASE_BRANCH: main
jobs:
Build_Project_4-Series:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Set Version Number
id: setVersion
shell: powershell
run: |
$latestVersion = [version]"2.0.0"
$newVersion = [version]$latestVersion
$phase = ""
$newVersionString = ""
switch -regex ($Env:GITHUB_REF) {
'^refs\/pull\/*.' {
$phase = 'beta';
$newVersionString = "{0}-{1}-{2}" -f $newVersion, $phase, $Env:GITHUB_RUN_NUMBER
}
'^refs\/heads\/hotfix-2.0.0\/*.' {
$phase = 'hotfix'
$newVersionString = "{0}-{1}-{2}" -f $newVersion, $phase, $Env:GITHUB_RUN_NUMBER
}
'^refs\/heads\/release-2.0.0\/*.' {
$splitRef = $Env:GITHUB_REF -split "/"
$version = [version]($splitRef[-1] -replace "v", "")
$phase = 'rc'
$newVersionString = "{0}-{1}-{2}" -f $version, $phase, $Env:GITHUB_RUN_NUMBER
}
'^refs\/heads\/feature-2.0.0\/*.' {
$phase = 'alpha'
$newVersionString = "{0}-{1}-{2}" -f $newVersion, $phase, $Env:GITHUB_RUN_NUMBER
}
'development-2.0.0' {
$phase = 'beta'
$newVersionString = "{0}-{1}-{2}" -f $newVersion, $phase, $Env:GITHUB_RUN_NUMBER
}
}
echo "version=$newVersionString" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Setup MS Build
uses: microsoft/setup-msbuild@v1.1
- name: restore Nuget Packages
run: nuget restore .\$($Env:SOLUTION_FILE).sln
# Build the solutions in the docker image
- name: Build Solution
run: msbuild .\$($Env:SOLUTION_FILE).sln /p:Platform="Any CPU" /p:Configuration="Debug" /p:Version="${{ steps.setVersion.outputs.version }}" -m
- name: Create tag for non-rc builds
if: contains(steps.setVersion.outputs.version, 'alpha')
run: |
git tag ${{ steps.setVersion.outputs.version }}
git push --tags origin
# Create the release on the source repo
- name: Create Release
id: create_release
# if: contains(steps.setVersion.outputs.version,'-rc-') ||
# contains(steps.setVersion.outputs.version,'-hotfix-') ||
# contains(steps.setVersion.outputs.version, '-beta-')
uses: ncipollo/release-action@v1
with:
artifacts: 'output\**\*.*(cpz|cplz)'
generateReleaseNotes: true
prerelease: ${{contains('debug', env.BUILD_TYPE)}}
tag: ${{ steps.setVersion.outputs.version }}
- name: Setup Nuget
run: |
nuget sources add -name github -source https://nuget.pkg.github.com/pepperdash/index.json -username pepperdash -password ${{ secrets.GITHUB_TOKEN }}
nuget setApiKey ${{ secrets.GITHUB_TOKEN }} -Source github
nuget setApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json
- name: Publish to Nuget
run: nuget push .\output\*.nupkg -Source https://api.nuget.org/v3/index.json
- name: Publish to Github Nuget
run: nuget push .\output\*.nupkg -Source github

View File

@@ -1,55 +0,0 @@
name: main Build using Docker
on:
release:
types:
- published
branches:
- main-2.0.0
env:
# solution path doesn't need slashes unless there it is multiple folders deep
# solution name does not include extension. .sln is assumed
SOLUTION_PATH: PepperDashEssentials
SOLUTION_FILE: PepperDashEssentials
# Do not edit this, we're just creating it here
VERSION: 0.0.0-buildtype-buildnumber
# Defaults to debug for build type
BUILD_TYPE: Release
# Defaults to main as the release branch. Change as necessary
RELEASE_BRANCH: main
jobs:
Build_Project:
runs-on: windows-2019
steps:
# First we checkout the source repo
- name: Checkout repo
uses: actions/checkout@v3
# Generate the appropriate version number
- name: Set Version Number
shell: powershell
id: setVersion
env:
TAG_NAME: ${{ github.event.release.tag_name }}
run: echo "VERSION=$($Env:TAG_NAME)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Setup MS Build
uses: microsoft/setup-msbuild@v1.1
- name: restore Nuget Packages
run: nuget restore .\$($Env:SOLUTION_FILE).sln
- name: Build Solution
run: msbuild .\$($Env:SOLUTION_FILE).sln /p:Platform="Any CPU" /p:Configuration="Debug" /p:Version="${{ steps.setVersion.outputs.version }}" -m
- name: Upload Release
id: create_release
uses: ncipollo/release-action@v1
with:
updateRelease: true
artifacts: 'output\**\*.*(cpz|cplz)'
tag: ${{ steps.setVersion.outputs.version }}
- name: Setup Nuget
run: |
nuget sources add -name github -source https://nuget.pkg.github.com/pepperdash/index.json -username pepperdash -password ${{ secrets.GITHUB_TOKEN }}
nuget setApiKey ${{ secrets.GITHUB_TOKEN }} -Source github
nuget setApiKey ${{ secrets.NUGET_API_KEY }} -Source https://api.nuget.org/v3/index.json
- name: Publish to Nuget
run: nuget push .\output\*.nupkg -Source https://api.nuget.org/v3/index.json
- name: Publish to Github Nuget
run: nuget push .\output\*.nupkg -Source github

45
.github/workflows/publish-docs.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Publish Docs
# Trigger the action on push to main
on:
push:
branches:
- main
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
actions: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
publish-docs:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dotnet Setup
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.x
- run: dotnet tool update -g docfx
- run: docfx ./docs/docfx.json
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: './docs/_site'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

6
.gitignore vendored
View File

@@ -391,3 +391,9 @@ FodyWeavers.xsd
essentials-framework/Essentials Interfaces/PepperDash_Essentials_Interfaces/PepperDash_Essentials_Interfaces.csproj
.DS_Store
/._PepperDash.Essentials.sln
.vscode/settings.json
_site/
api/
*.DS_Store
/._PepperDash.Essentials.4Series.sln
dotnet

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "Essentials-Template-UI"]
path = Essentials-Template-UI
url = https://github.com/PepperDash/Essentials-Template-UI.git

36
.releaserc.json Normal file
View File

@@ -0,0 +1,36 @@
{
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"releaseRules": [
{ "scope": "force-patch", "release": "patch" },
{ "scope": "no-release", "release": false }
]
}
],
"@semantic-release/release-notes-generator",
["@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/exec",
{
"verifyReleaseCmd": "echo \"newVersion=true\" >> $GITHUB_OUTPUT",
"publishCmd": "echo \"version=${nextRelease.version}\" >> $GITHUB_OUTPUT && echo \"tag=${nextRelease.gitTag}\" >> $GITHUB_OUTPUT && echo \"type=${nextRelease.type}\" >> $GITHUB_OUTPUT && echo \"channel=${nextRelease.channel}\" >> $GITHUB_OUTPUT"
}
]
],
"branches": [
"main",
{"name": "development", "prerelease": "beta", "channel": "beta"},
{"name": "release", "prerelease": "rc", "channel": "rc"},
{
"name": "replace-me-feature-branch",
"prerelease": "replace-me-prerelease",
"channel": "replace-me-prerelease"
}
]
}

9
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"recommendations": [
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csharp",
"ms-dotnettools.csdevkit",
"vivaxy.vscode-conventional-commits",
"mhutchie.git-graph"
]
}

View File

@@ -91,8 +91,8 @@ we receive and the availability of resources to evaluate contributions, we antic
project remains dynamic and relevant. This may affect our responsiveness and ability to accept pull requests
quickly. This does not mean we are ignoring them.
- Not all innovative ideas need to be accepted as pull requests into this GitHub project to be valuable to the community.
There may be times when we recommend that you just share your code for some enhancement to Ghidra from your own
repository. As we identify and recognize extensions that are of general interest to the reverse engineering community, we
There may be times when we recommend that you just share your code for some enhancement to Essentials from your own
repository. As we identify and recognize extensions that are of general interest to Essentials, we
may seek to incorporate them with our baseline.
## Legal

View File

@@ -0,0 +1,282 @@
# Crestron Library Usage Analysis - PepperDash Essentials
This document provides a comprehensive analysis of Crestron classes and interfaces used throughout the PepperDash Essentials framework, organized by namespace and library component.
## Executive Summary
The PepperDash Essentials framework extensively leverages Crestron SDK components across 100+ files, providing abstractions for:
- Control system hardware (processors, touchpanels, IO devices)
- Communication interfaces (Serial, TCP/IP, SSH, CEC, IR)
- Device management and routing
- User interface components and smart objects
- System monitoring and diagnostics
## 1. Core Crestron Libraries
### 1.1 Crestron.SimplSharp
**Primary Usage**: Foundational framework components, collections, and basic types.
**Key Files**:
- Multiple files across all projects use `Crestron.SimplSharp` namespaces
- Provides basic C# runtime support for Crestron processors
### 1.2 Crestron.SimplSharpPro
**Primary Usage**: Main hardware abstraction layer for Crestron devices.
**Key Classes Used**:
#### CrestronControlSystem
- **File**: `/src/PepperDash.Essentials/ControlSystem.cs`
- **Usage**: Base class for the main control system implementation
- **Implementation**: `public class ControlSystem : CrestronControlSystem, ILoadConfig`
#### Device (Base Class)
- **Files**: 50+ files inherit from or use this class
- **Key Implementations**:
- `/src/PepperDash.Core/Device.cs` - Core device abstraction
- `/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs` - Extended device base
- `/src/PepperDash.Essentials.Core/Room/Room.cs` - Room device implementation
- `/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs` - Processor device wrapper
#### BasicTriList
- **Files**: 30+ files use this class extensively
- **Primary Usage**: Touchpanel communication and SIMPL bridging
- **Key Files**:
- `/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs` - Extension methods for signal handling
- `/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs` - Bridge interface
- `/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs` - UI dialog implementation
#### BasicTriListWithSmartObject
- **Files**: Multiple touchpanel and UI files
- **Usage**: Enhanced touchpanel support with smart object integration
- **Key Files**:
- `/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs` - Interface definitions
- `/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferenceList/SubpageReferenceList.cs`
## 2. Communication Hardware
### 2.1 Serial Communication (ComPort)
**Primary Class**: `ComPort`
**Key Files**:
- `/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs`
- `/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs`
**Usage Pattern**:
```csharp
public class ComPortController : Device, IBasicCommunicationWithStreamDebugging
public static ComPort GetComPort(EssentialsControlPropertiesConfig config)
```
**Interface Support**: `IComPorts` - Used for devices that provide multiple COM ports
### 2.2 IR Communication (IROutputPort)
**Primary Class**: `IROutputPort`
**Key Files**:
- `/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs`
- `/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs`
- `/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs`
**Usage Pattern**:
```csharp
public class IrOutputPortController : Device
IROutputPort IrPort;
public IrOutputPortController(string key, IROutputPort port, string irDriverFilepath)
```
### 2.3 CEC Communication (ICec)
**Primary Interface**: `ICec`
**Key Files**:
- `/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs`
- `/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs`
**Usage Pattern**:
```csharp
public class CecPortController : Device, IBasicCommunicationWithStreamDebugging
public static ICec GetCecPort(ControlPropertiesConfig config)
```
## 3. Input/Output Hardware
### 3.1 Digital Input
**Primary Interface**: `IDigitalInput`
**Key Files**:
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericDigitalInputDevice.cs`
- `/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs`
**Usage Pattern**:
```csharp
public List<IDigitalInput> Inputs { get; private set; }
void AddInput(IDigitalInput input)
```
### 3.2 Versiport Support
**Key Files**:
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs`
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs`
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs`
**Usage**: Provides flexible I/O port configuration for various signal types
## 4. Touchpanel Hardware
### 4.1 MPC3 Touchpanel
**Primary Class**: `MPC3Basic`
**Key File**: `/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs`
**Usage Pattern**:
```csharp
public class Mpc3TouchpanelController : Device
readonly MPC3Basic _touchpanel;
_touchpanel = processor.ControllerTouchScreenSlotDevice as MPC3Basic;
```
### 4.2 TSW Series Support
**Evidence**: References found in messenger files and mobile control components
**Usage**: Integrated through mobile control messaging system for TSW touchpanel features
## 5. Timer and Threading
### 5.1 CTimer
**Primary Class**: `CTimer`
**Key File**: `/src/PepperDash.Core/PasswordManagement/PasswordManager.cs`
**Usage Pattern**:
```csharp
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started"));
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset"));
```
## 6. Networking and Communication
### 6.1 Ethernet Communication
**Libraries Used**:
- `Crestron.SimplSharpPro.EthernetCommunication`
- `Crestron.SimplSharp.Net.Utilities.EthernetHelper`
**Key Files**:
- `/src/PepperDash.Core/Comm/GenericTcpIpClient.cs`
- `/src/PepperDash.Core/Comm/GenericTcpIpServer.cs`
- `/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs`
- `/src/PepperDash.Core/Comm/GenericSshClient.cs`
- `/src/PepperDash.Core/Comm/GenericUdpServer.cs`
**Usage Pattern**:
```csharp
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
```
## 7. Device Management Libraries
### 7.1 DeviceSupport
**Library**: `Crestron.SimplSharpPro.DeviceSupport`
**Usage**: Core device support infrastructure used throughout the framework
### 7.2 DM (DigitalMedia)
**Library**: `Crestron.SimplSharpPro.DM`
**Usage**: Digital media routing and switching support
**Evidence**: Found in routing configuration and DM output card references
## 8. User Interface Libraries
### 8.1 UI Components
**Library**: `Crestron.SimplSharpPro.UI`
**Usage**: User interface elements and touchpanel controls
### 8.2 Smart Objects
**Key Files**:
- `/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs`
- `/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferenceList/SubpageReferenceList.cs`
**Usage**: Advanced UI components with dynamic content
## 9. System Monitoring and Diagnostics
### 9.1 Diagnostics
**Library**: `Crestron.SimplSharpPro.Diagnostics`
**Usage**: System health monitoring and performance tracking
### 9.2 System Information
**Key Files**:
- `/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs`
**Usage**: Provides system status, Ethernet information, and program details
## 10. Integration Patterns
### 10.1 SIMPL Bridging
**Pattern**: Extensive use of `BasicTriList` for SIMPL integration
**Files**: Bridge classes throughout the framework implement `LinkToApi` methods:
```csharp
public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge);
```
### 10.2 Device Factory Pattern
**Implementation**: Factory classes create hardware-specific implementations
**Example**: `CommFactory.cs` provides communication device creation
### 10.3 Extension Methods
**Pattern**: Extensive use of extension methods for Crestron classes
**Example**: `TriListExtensions.cs` adds 30+ extension methods to `BasicTriList`
## 11. Signal Processing
### 11.1 Signal Types
**Bool Signals**: Digital control and feedback
**UShort Signals**: Analog values and numeric data
**String Signals**: Text and configuration data
**Implementation**: Comprehensive signal handling in `TriListExtensions.cs`
## 12. Error Handling and Logging
**Pattern**: Consistent use of Crestron's Debug logging throughout
**Examples**:
```csharp
Debug.LogMessage(LogEventLevel.Information, "Device {0} is not a valid device", dc.PortDeviceKey);
Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?");
```
## 13. Threading and Synchronization
**Components**:
- CTimer for time-based operations
- Thread-safe collections and patterns
- Event-driven programming models
## Conclusion
The PepperDash Essentials framework demonstrates sophisticated integration with the Crestron ecosystem, leveraging:
- **Core Infrastructure**: CrestronControlSystem, Device base classes
- **Communication**: COM, IR, CEC, TCP/IP, SSH protocols
- **Hardware Abstraction**: Touchpanels, I/O devices, processors
- **User Interface**: Smart objects, signal processing, SIMPL bridging
- **System Services**: Monitoring, diagnostics, device management
This analysis shows that Essentials serves as a comprehensive middleware layer, abstracting Crestron hardware complexities while providing modern software development patterns and practices.
---
*Generated: [Current Date]*
*Framework Version: PepperDash Essentials (Based on codebase analysis)*

View File

@@ -0,0 +1,97 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Devices.Common", "src\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj", "{53E204B7-97DD-441D-A96C-721DF014DF82}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "src\PepperDash.Essentials\PepperDash.Essentials.csproj", "{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Control", "Mobile Control", "{B24989D7-32B5-48D5-9AE1-5F3B17D25206}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl", "src\PepperDash.Essentials.MobileControl\PepperDash.Essentials.MobileControl.csproj", "{F6D362DE-2256-44B1-927A-8CE4705D839A}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl.Messengers", "src\PepperDash.Essentials.MobileControl.Messengers\PepperDash.Essentials.MobileControl.Messengers.csproj", "{B438694F-8FF7-464A-9EC8-10427374471F}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Essentials", "Essentials", "{AD98B742-8D85-481C-A69D-D8D8ABED39EA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{53E204B7-97DD-441D-A96C-721DF014DF82} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
EndGlobalSection
EndGlobal

View File

@@ -1,44 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Devices.Common", "src\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj", "{53E204B7-97DD-441D-A96C-721DF014DF82}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "src\PepperDash.Essentials\PepperDash.Essentials.csproj", "{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
EndGlobalSection
EndGlobal

View File

@@ -60,4 +60,4 @@ For detailed documentation, see the [Wiki](https://github.com/PepperDash/Essenti
## How-To (Getting Started)
See [Getting Started](https://github.com/PepperDash/Essentials/wiki/Get-started#how-to-get-started)
See [Getting Started](https://github.com/PepperDash/Essentials/wiki/Get-started#how-to-get-started)

View File

@@ -1,47 +0,0 @@
devjson:1 {"deviceKey":"display-1-comMonitor","methodName":"PrintStatus"}
devjson:1 {"deviceKey":"display-1-com","methodName":"SimulateReceive", "params": ["\\x05\\x06taco\\xAA"]}
devjson:1 {"deviceKey":"display-1","methodName":"InputHdmi1", "params": []}
devjson:1 {"deviceKey":"display-1","methodName":"PowerOff", "params": []}
devjson:1 {"deviceKey":"timer","methodName":"Start" }
devjson:1 {"deviceKey":"timer","methodName":"Cancel" }
devjson:1 {"deviceKey":"timer","methodName":"Reset" }
devjson:1 {"deviceKey":"room1","methodName":"Shutdown" }
devjson:1 {"deviceKey":"mockVc-1", "methodName":"TestIncomingVideoCall", "params": ["123-456-7890"]}
devjson:1 {"deviceKey":"mockVc-1", "methodName":"TestIncomingAudioCall", "params": ["111-111-1111"]}
devjson:1 {"deviceKey":"mockVc-1", "methodName":"TestIncomingVideoCall", "params": ["444-444-4444"]}
devjson:1 {"deviceKey":"mockVc-1", "methodName":"ListCalls"}
devjson:1 {"deviceKey":"room1-emergency", "methodName":"RunEmergencyBehavior"}
devjson:1 {"deviceKey":"microphonePrivacyController-1", "methodName":"TogglePrivacyMute"}
devjson:1 {"deviceKey":"room1.InCallFeedback","methodName":"SetTestValue", "params": [ true ]}
devjson:1 {"deviceKey":"room1.InCallFeedback","methodName":"ClearTestValue", "params": []}
devjson:3 {"deviceKey":"room1.RoomOccupancy.RoomIsOccupiedFeedback","methodName":"SetTestValue", "params": [ true ]}
devjson:2 {"deviceKey":"codec-comms-ssh", "methodName":"SendText", "params": ["xcommand dial number: 10.11.50.211\r"]}
devjson:2 {"deviceKey":"codec-comms-ssh", "methodName":"Connect", "params": []}
devjson:1 {"deviceKey":"commBridge", "methodName":"ExecuteJoinAction", "params":[ 301, "digital", true ]}
devjson:2 {"deviceKey":"display01Comm-com", "methodName":"SendText", "params": [ "I'M GETTING TIRED OF THIS" ]}
devjson:10 {"deviceKey":"dmLink-ssh", "methodName":"Connect", "params": []}
devjson:2 {"deviceKey":"roomCombiner", "methodName":"SetRoomCombinationScenario", "params": ["combined"]}
devjson:2 {"deviceKey":"roomCombiner", "methodName":"SetRoomCombinationScenario", "params": ["divided"]}

58
docs/docfx.json Normal file
View File

@@ -0,0 +1,58 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json",
"metadata": [
{
"src": [
{
"src": "../",
"files": [
"src/**/*.csproj"
]
}
],
"properties": {
"TargetFramework": "net472"
},
"dest": "api",
"namespaceLayout": "nested",
"outputFormat": "apiPage"
}
],
"build": {
"content": [
{
"files": [
"docs/**/*.{md,yml}",
"api/**/*.{md,yml}",
"index.md",
"toc.yml"
],
"exclude": [
"_site/**",
".github/**"
]
}
],
"resource": [
{
"files": [
"docs/images/**"
]
}
],
"output": "_site",
"template": [
"default",
"modern"
],
"globalMetadata": {
"_appName": "PepperDash Essentials",
"_appTitle": "PepperDash Essentials",
"_enableSearch": true,
"_appLogoPath": "docs/images/favicon-32x32.png",
"_appFaviconPath": "docs/images/favicon.ico",
"_disableToc": false,
"pdf": false
}
}
}

41
docs/docs/Arch-1.md Normal file
View File

@@ -0,0 +1,41 @@
# Essentials architecture
## Device and DeviceManager
---
[YouTube Video - The Device Model in PepperDash Essentials](https://youtu.be/QF4vCQfOYGw)
***
A `Device` (`PepperDash.Core.Device`) is a logical construct. It may represent a piece of hardware, a port, a socket, a collection of other devices/ports/constructs that define an operation, or any unit of logic that should be created at startup and exist independent of other devices.
`DeviceManager` (`PepperDash.Essentials.Core.DeviceManager`) is the collection of all Devices. The collection of everything we control, and other business logic in a system. See the list below for what is typical in the device manager.
## Flat system design
In Essentials, most everything we do is focused in one layer: The Devices layer. This layer interacts with the physical Crestron and other hardware and logical constructs underneath, and is designed so that we rarely act directly on the often-inconsistent hardware layer. The `DeviceManager` is responsible for containing all of the devices in this layer.
Types of things in `DeviceManager`:
* Rooms
* Sources
* Codecs, DSPs, displays, routing hardware
* IR Ports, Com ports, SSh Clients, ...
* Occupancy sensors and relay-driven devices
* Logical devices that manage multiple devices and other business, like shade or lighting scene controllers
* Fusion connectors to rooms
A Device doesn't always represent a physical piece of hardware, but rather a logical construct that "does something" and is used by one or more other devices in the running program. For example, we create a room device, and its corresponding Fusion device, and that room has a Cisco codec device, with an attached SSh client device. All of these lie in a flat collection in the `DeviceManager`.
> The `DeviceManager` is nothing more than a modified collection of things, and technically those things don't have to be Devices, but must at least implement the `IKeyed` (`PepperDash.Core.IKeyed`) interface (simply so items can be looked up by their key.) Items in the `DeviceManager` that are Devices are run through additional steps of [activation](~/docs/Arch-activate.md#2-pre-activation) at startup. This collection of devices is all interrelated by their string keys.
In this flat design, we spin up devices, and then introduce them to their "coworkers and bosses" - the other devices and logical units that they will interact with - and get them all operating together to form a running unit. For example: A room configuration will contain a "VideoCodecKey" property and a "DefaultDisplayKey" property. The `DeviceManager` provides the room with the codec or displays having the appropriate keys. What the room does with those is dependent on its coding.
> In the default Essentials routing scheme, the routing system gets the various devices involved in given route from `DeviceManager`, as they are discovered along the defined tie-lines. This is all done at route-time, on the fly, using only device and port keys. As soon as the routing operation is done, the whole process is released from memory. This is extremely-loose coupling between objects.
This flat structure ensures that every device in a system exists in one place and may be shared and reused with relative ease. There is no hierarchy.
## Architecture drawing
![Architecture overview](~/docs/images/arch-overview.png)
Next: [Configurable lifecycle](~/docs/Arch-lifecycle.md)

158
docs/docs/Arch-activate.md Normal file
View File

@@ -0,0 +1,158 @@
# Essentials architecture: DeviceManager activation
## What is all this?
The Essentials system architecture is a loose collection of "things" - generally real or logical Devices - that all need to relate to each other. In the interest of keeping Essentials extensible and flexible, we use an non-ordered collection of objects that should only have references to each other in the least-binding way possible. Meaning: Devices should be designed to be able to function without related objects present, and when they are present they should only retain loose reference to those other objects for memory management and later deconstruction as Essentials grows into a real-time configurable environment.
In order to facilitate this loose coupling, Essentials devices go through five phases during the startup process: Construction; addition to `DeviceManager`; pre-activation; activation; post-activation. We will describe what is optimal behavior for each of the steps below:
### Classes Referenced
* `PepperDash.Core.Device`
* `PepperDash.Essentials.Core.EssentialsDevice`
* `PepperDash.Essentials.Core.DeviceManager`
* `PepperDash.Essentials.Core.Privacy.MicrophonePrivacyController`
## 1. Construction and addition to the DeviceManager
In general, a device's constructor should only be used to get the "framework" of the device in place. All devices are constructed in this stage. Rooms and fusion bridges included. Simple devices like IR driver devices, and devices with no controls can be completely spun up in this phase. All devices are added to the `DeviceManager` after they are constructed, but may not be fully functional.
## 2. Pre-activation
This stage is rarely used. It exists to allow an opportunity for any necessary logic to take place before the main activation phase.
## 3. Activation
This stage is the main phase of startup, and where most devices will get up and running, if they need additional startup behavior defined. The developer will code an optional overridden `CustomActivate()` method on the device class. This is where hardware ports may be set up; signals and feedbacks linked; UI drivers fired up; rooms linked to their displays and codec... With the exception of early-designed devices, most new Essentials classes do all of their startup here, rather than in their constructors.
Remember that in your `CustomActivate()` method, you cannot assume that a device you depend on is alive and running yet. It may be activating later. You _can_ depend on that device's existence, and link yourself to it, but it may not be functional yet. In general, there should be no conditions in any Essentials code that depend on device startup sequence and ordering. All rooms, devices, classes should be able to function without linked devices being alive, and respond appropriately when they do come to life. Any post-activation steps can be done in step four below - and should be avoided in general.
If the `CustomActivate()` method is long, consider breaking it up into many smaller methods. This will enhance exception handling and debugging when things go wrong, with more-detailed stack traces, and makes for easier-to-read code.
Note: It is best-practice in Essentials to not write arbitrarily-timed startup sequences to ensure that a "system" or room is functional. Rather, we encourage the developer to use various properties and conditions on devices to aggregate together "room is ready" statuses that can trigger further action. This ensures that all devices can be up and alive, allowing them to be debugged within a program that may otherwise be misbehaving - as well as not making users and expensive developers wait for code to start up!
```cs
public override bool CustomActivate()
{
Debug.Console(0, this, "Final activation. Setting up actions and feedbacks");
SetupFunctions();
SetupFeedbacks();
EISC.SigChange += EISC_SigChange;
...
}
```
## 4. Post-activation
This phase is used primarily to handle any logic in a device that might be dependent on another device, and we need to ensure that we have waited for the dependent device to be activated first. For example, if we look at the `MicrophonePrivacyController` class, this is a "virtual" device whose purpose is to control the mute state of microphones from one or more contact closure inputs as well as provide feedback via different colored LEDs as to the current mute state. This virtual-device doesn't actually represent any sort of physical hardware device, but rather relies on associating itself with other devices that represent digital inputs and relays as well as whatever device is responsible for preforming the actual muting of the microphones.
We can see in the example below that during the `CustomActivate()` phase, we define a post-activation action via a lambda in `AddPostActivationAction()` that will execute during the post-activation phase. The purpose here is to check the state of the microphone mute and set the state of the relays that control the LEDs accordingly. We need to do this as a post-activation action because we need to make sure that the devices PrivacyDevice, RedLedRelay and GreenLedRelay are fully activated before we can attempt to interact with them.
### **Example**
```cs
public override bool CustomActivate()
{
foreach (var i in Config.Inputs)
{
var input = DeviceManager.GetDeviceForKey(i.DeviceKey) as IDigitalInput;
if(input != null)
AddInput(input);
}
var greenLed = DeviceManager.GetDeviceForKey(Config.GreenLedRelay.DeviceKey) as GenericRelayDevice;
if (greenLed != null)
GreenLedRelay = greenLed;
else
Debug.Console(0, this, "Unable to add Green LED device");
var redLed = DeviceManager.GetDeviceForKey(Config.RedLedRelay.DeviceKey) as GenericRelayDevice;
if (redLed != null)
RedLedRelay = redLed;
else
Debug.Console(0, this, "Unable to add Red LED device");
AddPostActivationAction(() => {
CheckPrivacyMode();
PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange -= PrivacyModeIsOnFeedback_OutputChange;
PrivacyDevice.PrivacyModeIsOnFeedback.OutputChange += PrivacyModeIsOnFeedback_OutputChange;
});
initialized = true;
return base.CustomActivate();
}
void CheckPrivacyMode()
{
if (PrivacyDevice != null)
{
var privacyState = PrivacyDevice.PrivacyModeIsOnFeedback.BoolValue;
if (privacyState)
TurnOnRedLeds();
else
TurnOnGreenLeds();
}
}
```
## Activation exceptions
Each of the three activation phases operates in a try/catch block for each device. This way if one device has a fatal failure during activation, the failure will be logged and the system can continue to activate. This allows the developer to chase down multiple issues per load while testing, or to fix configuration omissions/errors as a group rather than one-at-a-time. A program can theoretically be fully-initialized and have many or all devices fail. We generally do not want to depend on exception handling to log device failures. Construction and activation code should have plenty of null checks, parameter validity checks, and debugging output to prevent exceptions from occurring. `String.IsEmptyOrNull(myString)` and `if(myObject == null)` are your friends. Invite them often.
## Interdependence
In any real-world system, devices and business logic need to talk to each other, otherwise, what's the point of all this coding? When creating your classes and configuration, it is best practice to _try_ not to "plug" one device into another during construction or activation. For example your touchpanel controller class has a `Display1` property that holds the display-1 object. Rather, it may be better to refer to the device as it is stored in the `DeviceManager` when it's needed using the static `DeviceManager.GetDeviceForKey(key)` method to get a reference to the device, which can be cast using various interfaces/class types, and then interacted with. This prevents objects from being referenced in places where the developer may later forget to dereference them, causing memory leak. This will become more important as Essentials becomes more able to be reconfigured at runtime.
As an example, [connection-based routing](~/docs/Connection-based-routing.md#essentials-connection-based-routing) uses these methods. When a route is requested, the collection of tielines and devices is searched for the devices and paths necessary to complete a route, but there are no devices or tie lines that are object-referenced in running code. It can all be torn down and reconfigured without any memory-management dereferencing, setting things to null.
## Device Initialization
Once the `DeviceManager` has completed the activation phase cycle for all devices, the devices themselves can be initialized. The `EssentialsDevice` class subscribes to the `DeviceManager.AllDevicesActivated` event and invokes the virtual `Initialize()` method on `Device` in a separate thread. This allows all devices to concurrently initialize in parallel threads.
The main task that should be undertaken in the `Initialize()` method for any 3rd party device class, it to begin communication with the device via its API. Ideally, no class that communicates with a 3rd party device outside the program should attempt to start communicating before this point.
### Example (from `PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.CiscoSparkCodec`)
```cs
public override void Initialize()
{
var socket = Communication as ISocketStatus;
if (socket != null)
{
socket.ConnectionChange += new EventHandler<GenericSocketStatusChageEventArgs>(socket_ConnectionChange);
}
Communication.Connect();
CommunicationMonitor.Start();
const string prefix = "xFeedback register ";
CliFeedbackRegistrationExpression =
prefix + "/Configuration" + Delimiter +
prefix + "/Status/Audio" + Delimiter +
prefix + "/Status/Call" + Delimiter +
prefix + "/Status/Conference/Presentation" + Delimiter +
prefix + "/Status/Cameras/SpeakerTrack" + Delimiter +
prefix + "/Status/RoomAnalytics" + Delimiter +
prefix + "/Status/RoomPreset" + Delimiter +
prefix + "/Status/Standby" + Delimiter +
prefix + "/Status/Video/Selfview" + Delimiter +
prefix + "/Status/Video/Layout" + Delimiter +
prefix + "/Status/Video/Input/MainVideoMute" + Delimiter +
prefix + "/Bookings" + Delimiter +
prefix + "/Event/CallDisconnect" + Delimiter +
prefix + "/Event/Bookings" + Delimiter +
prefix + "/Event/CameraPresetListUpdated" + Delimiter +
prefix + "/Event/UserInterface/Presentation/ExternalSource/Selected/SourceIdentifier" + Delimiter;
}
```
## The goal
Robust C#-based system code should not depend on "order" or "time" to get running. We do not need to manage the order of our startup in this environment. Our Room class may come alive before our DSP and or Codec, and the Room is responsible for handling things when those devices become available. The UI layer is responsible for blocking the UI or providing status when the Room's requirements are coming alive, or if something has gone away. We use events or `Feedbacks` to notify dependents that other devices/classes are ready or not, but we do not prevent continued construction/activation of the system when many of these events don't happen, or don't happen in a timely fashion. This removes the need for startup management, which is often prolonged and consumes _tons_ of developer/installer time. A fully-loaded Essentials system may go through activation in several seconds, with all devices concurrently getting themselves going, where legacy code may take 10 minutes.
When designing new Device-based classes, be it rooms, devices, port controllers, bridges, make them as independent as possible. They could exist alone in a program with no required partner objects, and just quietly exist without failing. We want the system to be fast and flexible, and keeping the interdependence between objects at a minimum improves this flexibility into the future.
Next: [More architecture](~/docs/Arch-topics.md)

View File

@@ -0,0 +1,9 @@
# Essentials Configurable System Lifecycle
The diagram below describes how Essentials gets a program up and running.
(The various activation phases are covered in more detail on the [next page](~/docs/Arch-activate.md))
![Lifecycle](~/docs/images/lifecycle.png)
Next: [Activation phases](~/docs/Arch-activate.md)

19
docs/docs/Arch-summary.md Normal file
View File

@@ -0,0 +1,19 @@
# Essentials architecture
## Summary
PepperDash Essentials is an open-source framework for control systems, built on Crestron's Simpl# Pro framework. It can be configured as a standalone program capable of running a wide variety of system designs and can also be used to augment other Crestron programs.
Essentials is a collection of C# libraries that can be used in many ways. It is a 100% configuration-driven framework that can be extended to add different workflows and behaviors, either through the addition of new device-types and classes, or via a plug-in mechanism. The framework is a collection of things that are all related and interconnected, but in general do not have strong dependencies on each other.
## Framework Libraries
The table below is a guide to understand the basic organization of code concepts within the various libraries that make up the architecture.
![Table](~/docs/images/arch-table.PNG)
The diagram below shows the reference dependencies that exist between the different component libraries that make up the Essentials Framework.
![Architecture drawing](~/docs/images/arch-high-level.png)
Next: [Architecture](~/docs/Arch-1.md)

156
docs/docs/Arch-topics.md Normal file
View File

@@ -0,0 +1,156 @@
# Configuration topics
Configuration is central to Essentials. On this page we will cover configuration-related topics, including the important concept of configure-first and some details about the config file process.
## Classes Referenced
- `PepperDash.Essentials.Core.Config.DeviceConfig`
## Configure-first development
## Framework Libraries
The table below is meant to serve as a guide to understand the basic organization of code concepts within the various libraries that make up the architecture.
_Todo, try a text-based table:_
![Table](~/docs/images/arch-table.PNG)
The diagram below shows the reference dependencies that exist between the different component libraries that make up the Essentials Framework.
![Architecture drawing](~/docs/images/arch-high-level.png)
### Architecture
#### Device and DeviceManager
A `Device` is a logical construct. It may represent a piece of hardware, a port, a socket, a collection of other devices/ports/constructs that define an operation, or any unit of logic that should be created at startup and exist independent of other devices.
`DeviceManager` is the collection of all Devices. The collection of everything we control on a system. **ADD SOME MORE HERE**
#### Flat system design
In Essentials, most everything we do is focused in one layer: The Devices layer. This layer interacts with the physical Crestron and other hardware and logical constructs underneath, and is designed so that we rarely act directly on the often-inconsistent hardware layer. The `DeviceManager` is responsible for containing all of the devices in this layer.
Types of devices:
- Rooms
- Sources
- Codecs, DSPs, displays, routing hardware
- IR Ports, Com ports, SSh Clients, ...
- Occupancy sensors and relay-driven devices
- Logical devices that manage multiple devices and other business, like shade or lighting scene controllers
- Fusion connectors to rooms
A Device doesn't always represent a physical piece of hardware, but rather a logical construct that "does something" and is used by one or more other devices in the running program. For example, we create a room device, and its corresponding Fusion device, and that room has a Cisco codec device, with an attached SSh client device. All of these lie in a flat collection in the `DeviceManager`.
> The `DeviceManager` is a modified collection of objects, and those objects don't have to inherit from Devices or EssentialsDevices, but must at least implement the `IKeyed` interface (so items can be looked up by their key.) Items in the `DeviceManager` that are Devices are run through additional steps of activation at startup. This collection of devices is all interrelated by their string keys.
In this flat design, we spin up devices, and then introduce them to their "coworkers and bosses" - the other devices and logical units that they will interact with - and get them all operating together to form a running unit. For example: A room configuration will contain a "VideoCodecKey" property and a "DefaultDisplayKey" property. The `DeviceManager` provides the room with the codec or displays having the appropriate keys. What the room does with those is dependent on its coding.
> In the default Essentials routing scheme, the routing system gets the various devices involved in given route from `DeviceManager`, as they are discovered along the defined tie-lines. This is all done at route-time, on the fly, using only device and port keys. As soon as the routing operation is done, the whole process is released from memory. This is extremely-loose coupling between objects.
This flat structure ensures that every device in a system exists in one place and may be shared and reused with relative ease. There is no hierarchy.
#### Architecture drawing
![Architecture overview](~/docs/images/arch-overview.png)
#### Essentials Configurable System Lifecycle
![Lifecycle](~/docs/images/lifecycle.png)
### Activation phases additional topics and examples (OTHER DOCS)
Concepts (link)
Room and touchpanel activation (link)
#### Configure first development
One of the primary concepts that has been adopted and must be adhered to when writing for Essentials framework is the concept of "configure first." The simple version is: Write what you need to do in the related configuration file (and configuration tool) first, then write the code that runs from that configuration. This ensures that the running code can actually be configured in the "flat" structure of devices and rooms that Essentials uses.
Often, code is written and tested first without consideration for configurability. Then, when a developer tries to make it configurable, they discover that the code as written doesnt support it without complicated configuration files. This creates spaghetti code in tools that are written to generate configurations and tends to create tighter coupling between objects than we desire. Later, a modified version of the original program is desired, but because the code was written in such a specific fashion, the code is hard to refactor and extend. This causes the configuration tool and configuration files to become even more convoluted. The modern versions of configuration tools that are starting to come out are modular and componentized. We want to ensure as much re-use of these modules as possible, with extensions and added features added on, rather than complete rewrites of existing code. In our running systems, we want to ensure as much flexibility in design as possible, eliminating multiple classes with similar code.
### Configuration reader process
At the heart of the Essentials framework is the configuration system. While not technically necessary for a system written with the Essentials framework, it is the preferred and, currently, the only way to build an Essentials system. The configuration file is JSON, and well-defined (but not well documented, yet). It is comprised of blocks:
- info (object) Contains metadata about the config file
- devices (array) Contains, well, the devices we intend to build and load
- rooms (array, typically only one) Contains the rooms we need
- sourceLists (object) Used by one or more rooms to represent list(s) of sources for those rooms
- tieLines (array) Used by the routing system to discover routing between sources and displays
In addition, a downloaded Portal config file will most likely be in a template/system form, meaning that the file contains two main objects, representing the template configuration and its system-level overrides. Other metadata, such as Portal UUIDs or URLs may be present.
At startup, the configuration file is read, and a ReadyEvent is fired. Upon being ready, that configuration is loaded by the ConfigReader.LoadConfig() method. The template and system are merged into a single configuration object, and that object is then deserialized into configuration wrapper classes that define most of the structure of the program to be built. (Custom configuration objects were built to allow for better type handling rather than using JToken methods to parse out error-prone property names.)
For example, a `DeviceConfig` object:
```cs
namespace PepperDash.Essentials.Core.Config
{
public class DeviceConfig
{
[JsonProperty("key")]
public string Key { get; set; }
[JsonProperty("uid")]
public int Uid { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("group")]
public string Group { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("properties")]
[JsonConverter(typeof(DevicePropertiesConverter))]
public JToken Properties { get; set; }
}
}
```
_Every_ `Device` present must adhere to those five properties plus a properties object. The properties object will have its own deserialization helpers, depending on what its structure is.
Once the ConfigReader has successfully read and deserialized the config file, then `ControlSystem.Load()` is called. This does the following in order:
1. Loads Devices
2. Loads TieLines
3. Loads Rooms
4. Loads LogoServer
5. Activation sequence
This ordering ensures that all devices are at least present before building tie lines and rooms. Rooms can be built without their required devices being present. In principle, this could break from the loosely-coupled goal we have described, but it is the clearest way to build the system in code. The goal is still to build a room class that doesn't have functional dependencies on devices that may not be ready for use.
In each device/room step, a device factory process is called. We call subsequent device factory methods in the various libraries that make up Essentials until one of them returns a functional device. This allows us to break up the factory process into individual libraries, and not have a huge list of types and build procedures. Here's part of the code:
```cs
// Try local factories first
var newDev = DeviceFactory.GetDevice(devConf);
if (newDev == null)
newDev = BridgeFactory.GetDevice(devConf);
// Then associated library factories
if (newDev == null)
newDev = PepperDash.Essentials.Core.DeviceFactory.GetDevice(devConf);
if (newDev == null)
newDev = PepperDash.Essentials.Devices.Common.DeviceFactory.GetDevice(devConf);
if (newDev == null)
newDev = PepperDash.Essentials.DM.DeviceFactory.GetDevice(devConf);
if (newDev == null)
newDev = PepperDash.Essentials.Devices.Displays.DisplayDeviceFactory.GetDevice(devConf);
```
In each respective factory, or device constructor, the configuration's properties object is either converted to a config object or read from using `JToken` methods. This builds the device which may be ready to go, or may require activation as described above.
A similar process is carried out for rooms, but as of now, the room types are so few that they are all handled in the `ControlSystem.LoadRooms()` method.
_This process will soon be enhanced by a plug-in mechanism that will drill into dynamically-loaded DLLs and load types from factories in those libraries. This is where custom essentials systems will grow from._
After those five steps, the system will be running and ready to use.

View File

@@ -0,0 +1,15 @@
# Bridging to Hardware and Network Resources
One of the most powerful features of Essentials is the ability to bridge SIMPL to and hardware resource to any piece of equipment instantiated inside of essentials. You can bridge directly to a comport on the processor just as easily as you can to the comport of an instantiated DM device. A simple change in the connection location of a display can be made with just a few keystrokes. This isn't restricted to comports either. Devices and direct connections can be linked to Digital Inputs, IR Ports, Relays, Comports, SSH, TCP/IP, UDP, and Cresnet resources.
## Examples
Follow the links below for examples of bridging to hardware and network resources.
**[GenericComm Bridging](~/docs/GenericComm.md)**
**[RelayOutput Bridging](~/docs/RelayOutput.md)**
**[Digital Input Bridging](~/docs/DigitalInput.md)**
**[Card Frame Bridging](~/docs/CardFrame.md)**

14
docs/docs/CardFrame.md Normal file
View File

@@ -0,0 +1,14 @@
{
"key": "cardCage1",
"uid": 1,
"name": "Internal Card Cage",
"type": "internalcardcage",
"group": "cardCage",
"properties": {
"cards": {
"1": "c3com3",
"2": "c3com3",
"3": ""
}
}
},

View File

@@ -0,0 +1,75 @@
# Unifying communication methods
In networked A/V systems, devices can use many different methods of communication: COM ports, TCP/IP sockets, Telnet, SSH. Generally, the data protocol and commands that are sent and received using any of these methods are the same, and it is not necessary for a device to know the details of the communication method it is using. A Samsung MDC protocol display in room 1 using RS232 speaks the same language as another Samsung MDC does in the next room using TCP/IP. For these, and most cases where the device doesn't need to know its communication method, we introduce the `IBasicCommunication` interface.
## Classes Referenced
* `PepperDash.Core.IBasicCommunication`
* `PepperDash.Core.ISocketStatus`
* `PepperDash.Core.GenericTcpIpClient`
* `PepperDash.Core.GenericSshClient`
* `PepperDash.Core.GenericSecureTcpIpClient`
* `PepperDash.Essentials.Core.ComPortController`
* `PepperDash.Essentials.Core.StatusMonitorBase`
## IBasicCommunication and ISocketStatus
All common communication controllers will implement the `IBasicCommunication` interface, which is an extension of `ICommunicationReceiver`. This defines receive events, connection state properties, and send methods. Devices that need to use COM port, TCP, SSh or other similar communication will require an `IBasicCommunication` type object to be injected at construction time.
```cs
/// <summary>
/// An incoming communication stream
/// </summary>
public interface ICommunicationReceiver : IKeyed
{
event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
bool IsConnected { get; }
void Connect();
void Disconnect();
}
/// <summary>
/// Represents a device that uses basic connection
/// </summary>
public interface IBasicCommunication : ICommunicationReceiver
{
void SendText(string text);
void SendBytes(byte[] bytes);
}
/// <summary>
/// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
/// GenericTcpIpClient
/// </summary>
public interface ISocketStatus : IBasicCommunication
{
event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
SocketStatus ClientStatus { get; }
}
```
### Developing devices with communication
Essentials uses dependency injection concepts in its start up phase. Simply, most devices use the same methods of communication, and are often communication-agnostic. During the build-from-configuration phase, the communication method device is instantiated, and then injected into the device that will use it. Since the communication device is of `IBasicCommunication`, the device controller receiving it knows that it can do things like listen for events, send text, or be notified when sockets change.
### Device Factory, Codec example
![Communication Device factory](~/docs/images/comm-device-factory.png)
The DeviceManager will contain two new devices after this: The Cisco codec, and the codec's `GenericSshClient`. This enables easier debugging of the client using console methods. Some devices like this codec will also have a `StatusMonitorBase` device, for Fusion and other reporting.
> `ComPortController` is `IBasicCommunication` as well, but methods like `Connect()` and `Disconnect()` do nothing on these types.
#### ISocketStatus
`PepperDash.Core.GenericTcpIpClient`, `GenericSshClient` and some other socket controllers implement `ISocketStatus`, which is an extension of `IBasicCommunication`. This interface reveals connection status properties and events.
```cs
public interface ISocketStatus : IBasicCommunication
{
event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
SocketStatus ClientStatus { get; }
}
```
Classes that are using socket-based comms will need to check if the communication is `ISocketStatus` and link up to the `ConnectionChange` event for connection handling.

View File

@@ -0,0 +1,291 @@
# Configuration Structure
---
[YouTube Video - Configuring PepperDash Essentials](https://youtu.be/EK8Ti9a1o7s)
***
The Essentials configuration structure is designed to allow minimum duplication of data across systems that share many similarities, which makes it ideally suited for applications where large numbers of duplicate room types must be deployed.
At a high level, the idea is to define a template of all of the common configuration shared by a group of systems of the same type. Then individual differences per system instance can be defined in a system block that either add data missing in the template, or override the default values set in the template.
## Top Level Object Structure (Double Config)
```cs
{
// This object is deserialized to type PepperDash.Essentials.Core.Config.EssentialsConfig
"system_url":"", // For Portal use only
"template_url":"", // For Portal use only
"template":{
// This object is deserialized to type PepperDash.Essentials.Core.EssentialsConfig
// For most manually generated configuration, only define data here. Leave system empty
},
"system":{
// This object is deserialized to type PepperDash.Essentials.Core.EssentialsConfig
// Any data here will be overlayed on top of the data in template. In the case of duplicate values
// the value in system will be overwrite any value in template
}
}
```
## Object Structure for `template` and `system` (`PepperDash.Essentials.Core.EssentialsConfig`)
``` js
{
"info": {
// This object is deserialized to type PepperDash.Essentials.Core.Config.InfoConfig
// Contains information about the system/configuration
},
"devices": [
// This object is deserialized to type List<PepperDash.Essentials.Core.Config.DeviceConfig>
// An array of devices
],
"rooms": [
// This object is deserialized to type List<PepperDash.Essentials.Core.Config.DeviceConfig>
// An array of rooms. These are not automatically deserialized
],
"tielines":[
// An array of tie lines that describe the connections between routing ports on devices
],
"sourceLists":{
// This object is deserialized to type Dictionary<string, Dictionary<string, PepperDash.Essentials.Core.SourceListItem>>
// An object that contains a collection
},
"joinMaps":{
// This object is deserialized to type Dictionary<string, string> where the value is a serialized class that inherits from JoinMapBase to be deserialized later
// Used to define custom join maps for bridging devices to SIMPL
}
}
```
## The Template and System Concept (Merging Configurations)
In order to understand how and why we use a double configuration concept, it's important to understand the relationship between a Template and a System in Portal.  A System represents a physical installed group of hardware(either currently or in the future), acting together usually as part of a single control system program.  A system MUST inherit from a Template.  A Template represents the common elements of one or more systems.
The idea being that configuration values that are common to all systems can be stored in the configuration for the template.  Then, any configuration values that are unique to a particular system cane be stored in the configuration of the System.  By "merging" the System configuration values over top of the Template configuration values, the resulting data contains all of the values that should be shared by each system that inherits from a common template, as well as the unique values for each individual system.
Below is an example of a double configuration containing both template and system properties.
```JSON
{
"template": {
"info": {
"name": "Template Name",
"description": "A 12 person conference room"
},
"devices": [
],
"rooms": [
]
},
"system": {
"info": {
"name": "System Name",
"myNewSystemProperty": "Some Value"
},
"devices": [
],
"rooms": [
]
}
}
```
Below is an example of the result of merging the above double configuration example into a single configuration.
```JSON
{
"info": {
"name": "System Name", // Since this property existed in both the template and system, the system value replaces the template value after the merge
"description": "A 12 person conference room", // This property existed only in the template and is unchanged after the merge
"myNewSystemProperty": "Some Value" // This property existed only in the system and is unchanged after the merge
},
"devices": [
],
"rooms": [
]
}
```
---
## Device Object Structure
The devices array is meant to hold a series of device objects.  The basic device object structure is defined below.
```JSON
{
"key": "someUniqueString", // *required* a unique string
"name": "A friendly Name", // *required* a friendly name meant for display to users
"type": "exampleType", // *required* the type identifier for this object.
"group": "exampleGroup", // *required* the group identifier for this object. This really equates to a category for the device,
// such as "lighting" or "displays" and may be deprecated in future in favor of "category"
"uid":0, // *required* a unique numeric identifier for each device
"properties": { // *required* an object where the configurable properties of the device are contained
"control": { // an object to contain all of the properties to connect to and control the device
"method": "ssh", // the control method used by this device
"tcpSshProperties": { // contains the necessary properties for the specified method
"address": "1.2.3.4",
"port": 22,
"username": "admin",
"password": "uncrackablepassword"
}
},
"someCustomProperty": "I Love Tacos!"
}
// Do NOT add any custom data at the top level of the device object. All custom data must be in the properties object.
}
```
Some additional details about specific properties that are important to note:
* "key": This value needs to be unique in the array of devices objects
* "uid": This value also needs to be unique for reasons related to configuration tools and template/system merging
* "type": Think of this as a way to identify what specific module you might associate with this device.  In Essentials, this value is used to determine what class will be instantiated for the device (ex. "necmpsx" or "samsungMdc" for two types of displays)
* "properties":  This object is used to store both specific and miscellaneous data about the device.
* Specific data, like that shown above in the "control" object has a pre-defined structure.
* Other data must be stored as objects or new properties inside the "properties" object such as "someCustomProperty" in the example above.
* Do NOT add any additional properties at the top level of the device object.  All custom data must be in the "properties" object.
## The Device Properties.Control Object
The control object inside properties has some reserved properties that are used by configuration tools and Essentials that require some caution.
```JSON
{
"properties": { // *required* an object where the configurable properties of the device are contained
"control": { // an object to contain all of the properties to connect to and control the device
// Example of the reserved properties for a socket based port (ssh, tcpIp, udp)
"method": "ssh", // the control method used by this device
"tcpSshProperties": { // contains the necessary properties for the specified method
"address": "1.2.3.4", // IP Address or hostname
"port": 22,
"username": "admin",
"password": "uncrackablepassword",
"autoReconnect": true, // If true, the client will attempt to re-connect if the connection is broken externally
"AutoReconnectIntervalMs": 2000 // The time between re-connection attempts
},
// Example of the reserved properties for a Com port
"method": "com",
"controlPortNumber": 1, // The number of the com port on the device specified by controlPortDevKey
"controlPortDevKey": "processor", // The key of the device where the com port is located
"comParams": { // This object contains all of the com spec properties for the com port
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 9600,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
}
}
}
}
```
---
## Device Merging
The following examples illustrate how the device key and uid properties affect how devices are merged together in a double configuration scenario.  In order for a template device and a system device to merge, they must have the same key and uid values
```JSON
{
"template": {
"info": {
"name": "Template Name",
"description": "A 12 person conference room"
},
"devices": [
{ // This is the template device
"key": "display-1",
"name": "Display",
"type": "samsungMdc",
"group": "displays",
"uid":0,
"properties": {
"control": {
"method": "ssh",
"tcpSshProperties": {
"address": "", // Note that at the template level we won't know the actual IP address so this value is left empty
"port": 22,
"username": "admin",
"password": "uncrackablepassword"
}
}
}
}
],
"rooms": [
]
},
"system": {
"info": {
"name": "System Name",
"myNewSystemProperty": "Some Value"
},
"devices": [
{ // This is the system device
"key": "display-1",
"uid":0,
"properties": {
"control": {
"tcpSshProperties": {
"address": "10.10.10.10" // Note that the actual IP address is specified at the system level
}
}
}
}
],
"rooms": [
]
}
}
```
Below is an example of the result of merging the above double configuration example into a single configuration.  
```JSON
{
"info": {
"name": "System Name",
"description": "A 12 person conference room",
"myNewSystemProperty": "Some Value"
},
"devices": [
{
"key": "display-1",
"name": "Display",
"type": "samsungMdc",
"group": "displays",
"uid":0,
"properties": {
"control": {
"method": "ssh",
"tcpSshProperties": {
"address": "10.10.10.10", // Note that the merged device object inherits all of the template
// properties and overwrites the template address property with the system value
"port": 22,
"username": "admin",
"password": "uncrackablepassword"
}
}
}
}
],
"rooms": [
]
}
```

View File

@@ -0,0 +1,81 @@
# Essentials connection-based routing
## TL;DR
Routing is defined by a connection graph or a wiring diagram. Routeable devices are sources, midpoints, or destinations. Devices are connected by tie lines. Tie lines represent the cables connecting devices, and are of type audio, video or both. Routes are made by telling a destination to get an audio/video/combined route from a source.
## Summary
Essentials routing is described by defining a graph of connections between devices in a system, typically in configuration. The audio, video and combination connections are like a wiring diagram. This graph is a collection of devices and tie lines, each tie line connecting a source device, source output port, destination device and destination input port. Tie lines are logically represented as a collection.
When routes are to be executed, Essentials will use this connection graph to decide on routes from source to destination. A method call is made on a destination, which says “destination, find a way for source xyz to get to you.” An algorithm analyzes the tie lines, instantly walking backwards from the destination, down every connection until it finds a complete path from the source. If a connected path is found, the algorithm then walks forward through all midpoints to the destination, executing switches as required until the full route is complete. The developer or configurer only needs to say “destination, get source xyz” and Essentials figures out how, regardless of what devices lie in between.
### Classes Referenced
* `PepperDash.Essentials.Core.Routing.IRoutingSource`
* `PepperDash.Essentials.Core.Routing.IRoutingOutputs`
* `PepperDash.Essentials.Core.Routing.IRoutingInputs`
* `PepperDash.Essentials.Core.Routing.IRoutingInputsOutputs`
* `PepperDash.Essentials.Core.Routing.IRoutingSinkNoSwitching`
* `PepperDash.Essentials.Core.Routing.IRoutingSinkWithSwitching`
## Example system, a simple presentation system
The diagram below shows the connections in a simple presentation system, with a few variations in connection paths. Example routes will be described following the diagram.
Each visible line between ports on devices represents a tie line. A tie line connects an output port on one device to an input port on another device, for example: an HDMI port on a document camera to an HDMI input on a matrix switcher. A tie line may be audio, video or both. It is essentially a logical representation of a physical cable in a system. This diagram has 12 tie lines, and those tie lines are defined in the tieLines array in configuration.
![Routing system diagram](~/docs/images/routing-system-diagram.png)
Lets go through some examples of routing, using pseudo-code:
1. Method call: “Projector 1, show Doc cam.” Routing will walk backwards through DM-RMC-3 and DM-8x8 iterating through all “wired up” ports until it finds a path back to the Doc cam. Routing will then step back through all devices in the discovered chain, switching routes on those that are switchable: Doc cam: no switching; DM 8x8: route input 3 to output 3; DM-RMC-3: no switching; Projector 1: Select input HDMI In. Route is complete.
2. Method call: “Projector 2, show Laptop, video-only.” Routing will walk backwards through DM-RMC-4, DM 8x8, DM-TX-1, iterating through all connected ports until it finds a connection to the laptop. Routing then steps back through all devices, switching video where it can: Laptop: No switching; DM-TX-1: Select HDMI in; DM 8x8: Route input 5 to output 4; DM-RMC-4: No switching; Projector 2: Select HDMI input. Route is complete.
3. Method call: “Amplifier, connect Laptop audio.” Again walking backwards to Laptop, as in #2 above. Switching will take place on DM-TX-1, DM 8x8, audio-only.
4. Very simple call: “Lobby display, show signage controller.” Routing will walk back on HDMI input 1 and immediately find the signage controller. It then does a switch to HDMI 1 on the display.
All four of the above could be logically combined in a series of calls to define a possible “scene” in a room: Put Document camera on Projector 1, put Laptop on Projector 2 and the audio, put Signage on the Lobby display. They key takeaway is that the developer doesnt need to define what is involved in making a certain route. The person configuring the system defines how its wired up, and the code only needs to tell a given destination to get a source, likely through configuration as well.
All of the above routes can be defined in source list routing tables, covered elsewhere (**make link)**.
---
### Definitions
#### Ports
Ports are logical representations of the input and output ports on a device.
#### Source
A source is a device at the beginning of a signal chain. For example, a set-top box, or a camera. Source devices typically have only output ports.
Source devices in Essentials must implement `IRoutingOutputs` or `IRoutingSource`
#### Midpoint
A midpoint is a device in the middle of the signal chain. Typically a switcher, matrix or otherwise. Examples: DM chassis; DM-TX; DM-RMC; A video codec. These devices will have input and output ports.
Midpoint devices must implement `IRoutingInputsOutputs`. Midpoints with switching must implement `IRouting`.
#### Sink
A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkNoSwitching` or `IRoutingSinkWithSwitching`.
#### Tie-line
A tie-line is a logical representation of a physical cable connection between two devices. It has five properties that define how the tie-line connects two devices. A configuration snippet for a single tie line connecting HDMI output 1 on a Cisco RoomKit to HDMI input 1 on a display, carrying both audio and video, is shown below.
```json
{
"sourceKey": "ciscoSparkPlusCodec-1",
"sourcePort": "HdmiOut1",
"destinationKey": "display-1",
"destinationPort": "HdmiIn1",
"type": "audioVideo"
}
```
### Interfaces
Todo: Define Interfaces IRouting, IRoutingOutputs, IRoutingInputs

274
docs/docs/Debugging.md Normal file
View File

@@ -0,0 +1,274 @@
# Methods of Debugging
1. You can use Visual Studio step debugging
- Pros:
- Detailed real time debugging into with breakpoints and object inspection
- Cons:
- Doesn't really work remotely
- On processors with Control Subnet, you must be connected to the CS interface to use step debugging. Often not practical or possible.
- No logging
- Using breakpoints stops the program and can interrupt system usage
- Requires the running application to be built in debug mode, not release mode
2. You can use the Debug class features build into the PepperDash.Core library.
- Pros:
- Can be easily enabled from console
- Allows for setting the level of verbosity
- Works when troubleshooting remotely and doesn't require a connection to the CS interface of the processor.
- Allows for logging to the Crestron error log or a custom log stored on removable media
- Works regardless of build type setting (debug/release)
- Can easily identify which class instance is generating console messages
- Can use console commands to view the state of public properties on devices
- Can use console commands to call methods on devices
- Doesn't stop the program
- Cons:
- No detailed object inspection in real time
- Only prints console statements already in code
- When enabled at the highest level of verbosity, it can produce a significant amount of data in console. Can be hard to find messages easily.
- No current mechanism to filter messages by device. (can be filtered by 3rd party tools easily, though)
- Not very effective in debugging applications running on the VC-4 platform as only log messages get printed to the Syslog
## How to use the PepperDash.Core Debug Class
The majority of interaction is done via console, preferably via an SSH session through Crestron Toolbox, PuTTy or any other suitable application.
In code, the most useful method is `Debug.Console()` which has several overloads. All variations take an integer value for the level (0-2) as the first argument. Level 0 will ALWAYS print. Level 1 is for typical debug messages and level 2 is for verbose debugging. In cases where the overloads that accept a `Debug.ErrorLogLevel` parameter are used, the message will ALWAYS be logged, but will only print to console if the current debug level is the same or higher than the level set in the `Debug.Console()` statement.
All statements printed to console are prefixed by a timestamp which can be greatly helpful in debugging order of operations.
```cs
// The most basic use, sets the level (0) and the message to print.
Debug.Console(0, "Hello World");
// prints: [timestamp]App 1:Hello World
// The string parameter has a built in string.Format() that takes params object[] items
string world = "World";
Debug.Console(0, "Hello {0}", world);
// prints: [timestamp]App 1:Hello World
// This overload takes an IKeyed as the second parameter and the resulting statement will
// print the Key of the device in console to help identify the class instance the message
// originated from
Debug.Console(0, this, "Hello World");
// prints: [timestamp]App 1:[deviceKey]Hello World
// Each of the above overloads has a corresponding variant that takes an argument to indicate
// the level of error to log the message at as well as printing to console
Debug.Console(0, Debug.ErrorLogLevel.Notice, "Hello World");
// prints: [timestamp]App 1:Hello World
```
## Console Commands
### General Console Commands
Below are is a non-exhaustive list of some of the Essentials specific console commands that allow interaction with the application at runtime.
### `help user`
Will print the available console commands for each program slot. Console commands can be added and removed dynamically by Essentials and may vary by the version of Essentials that is running. This is the best place to start to determine the available commands registered for each instance of Essentials running on a processor.
### `reportversions:[slot]`
Will print the running versions of all .dll libraries. Useful for determining the exact build version of the Essentials application and all plugins
### `gettypes:[slot] [searchString(optional)]`
The `searchString` value is an optional parameter to filter the results.
Will print all of the valid `type` values registered in the `DeviceFactory` for the running Essentials application. This helps when generating config structure and defining devices. Device types added by plugins will also be shown.
### `showconfig:[slot]`
Will print out the merged config object
### `donotloadonnextboot:[slot] [true/false]`
When the value is set to true, Essentials will pause when starting up, to allow for a developer to attach to the running process from an IDE for purposes of step debugging. Once attached, issuing the command `go:[slot]` will cause the configuration file to be read and the program to initialize. This value gets set to false when the `go` command is issues.
### DeviceManager Console Commands
The following console commands all perform actions on devices that have been registered with the `PepperDash.Essentials.Core.DeviceManager` static class
### `Appdebug:[slot][0-2]`
Gets or sets the current debug level where 0 is the lowest setting and 2 is the most verbose
### `getjoinmap:[slot] [bridgeKey][deviceKey (optional)]
For use with SIMPL Bridging. Prints the join map for the specified bridge. If a device key is specified, only the joins for that device will be printed.
Example:
```sh
RMC3>appdebug:1 // Gets current level
RMC3>AppDebug level = 0
RMC3>appdebug:1 1 // Sets level to 1 (all messages level 1 or lower will print)
RMC3>[Application 1], Debug level set to 1
```
### `Devlist:[slot]`
Gets the current list of devices from `DeviceManager`
Prints in the form [deviceKey] deviceName
Example:
```sh
// Get the list of devices for program 1
RMC3>devlist:1
RMC3>[16:34:05.819]App 1:28 Devices registered with Device Mangager:
[16:34:05.834]App 1: [cec-1] Tx 5 cec 1
[16:34:05.835]App 1: [cec-1-cec]
[16:34:05.835]App 1: [cec-5] Rmc 1 cec 1
[16:34:05.836]App 1: [cec-5-cec]
[16:34:05.836]App 1: [cec-6] Dm Chassis In 1 cec 1
[16:34:05.837]App 1: [cec-6-cec]
[16:34:05.837]App 1: [cec-7] Dm Chassis Out 1 cec 1
[16:34:05.838]App 1: [cec-7-cec]
[16:34:05.838]App 1: [comm-1] Generic comm 1
[16:34:05.838]App 1: [comm-1-com]
[16:34:05.839]App 1: [comm-2] Rmc comm 1
[16:34:05.839]App 1: [comm-2-com]
[16:34:05.840]App 1: [comm-3] Rmc comm 2
[16:34:05.840]App 1: [comm-3-com]
[16:34:05.841]App 1: [dmMd8x8-1] DM-MD8x8 Chassis 1
[16:34:05.842]App 1: [dmRmc100C-1] DM-RMC-100-C Out 3
[16:34:05.843]App 1: [dmRmc200C-1] DM-RMC-200-C Out 2
[16:34:05.843]App 1: [dmRmc4kScalerC-1] DM-RMC-4K-SCALER-C Out 1
[16:34:05.844]App 1: [dmTx201C-1] DM-TX-201C 1
[16:34:05.845]App 1: [eisc-1A]
[16:34:05.845]App 1: [gls-odt-1] GLS-ODT-CN 1
[16:34:05.846]App 1: [gls-oir-1] GLS-OIR-CN 1
[16:34:05.846]App 1: [processor]
[16:34:05.847]App 1: [ssh-1] Generic SSH 1
[16:34:05.847]App 1: [ssh-1-ssh]
[16:34:05.848]App 1: [systemMonitor]
[16:34:05.848]App 1: [tcp-1] Generic TCP 1
[16:34:05.849]App 1: [tcp-1-tcp]
```
### `Setdevicestreamdebug:[slot][devicekey][both/rx/tx/off]`
Enables debug for communication on a single device
Example:
```sh
PRO3>setdevicestreamdebug:1 lights-1-com both
[13:13:57.000]App 1:[lights-1-com] Sending 4 characters of text: 'test'
PRO3>setdevicestreamdebug:1 lights-1-com off
```
### `Devprops:[slot][devicekey]`
Gets the list of public properties on the device with the corresponding `deviceKey`
Example:
```sh
// Get the properties on the device with Key 'cec-1-cec'
// This device happens to be a CEC port on a DM-TX-201-C's HDMI input
RMC3>devprops:1 cec-1-cec
[
{
"Name": "IsConnected",
"Type": "Boolean",
"Value": "True",
"CanRead": true,
"CanWrite": false
},
{
"Name": "Key",
"Type": "String",
"Value": "cec-1-cec",
"CanRead": true,
"CanWrite": true
},
{
"Name": "Name",
"Type": "String",
"Value": "",
"CanRead": true,
"CanWrite": true
},
{
"Name": "Enabled",
"Type": "Boolean",
"Value": "False",
"CanRead": true,
"CanWrite": true
}
]
RMC3>
```
### `Devmethods:[slot][devicekey]`
Gets the list of public methods available on the device
Example:
```sh
// Get the methods on the device with Key 'cec-1-cec'
RMC3>devmethods:1 cec-1-cec
[
{
"Name": "SendText",
"Params": [
{
"Name": "text",
"Type": "String"
}
]
},
{
"Name": "SendBytes",
"Params": [
{
"Name": "bytes",
"Type": "Byte[]"
}
]
},
{
"Name": "SimulateReceive",
"Params": [
{
"Name": "s",
"Type": "String"
}
]
},
//... Response abbreviated for clarity ...
]
RMC3>
```
### `Devjson:[slot][json formatted object {"devicekey", "methodname", "params"}]`
Used in conjunction with devmethods, this command allows any of the public methods to be called from console and the appropriate arguments can be passed in to the method via a JSON object.
This command is most useful for testing without access to hardware as it allows both simulated input and output for a device.
Example:
```sh
// This command will call the SendText(string text) method on the
// device with the Key 'cec-1-cec' and pass in "hello world" as the
// argument parameter. On this particular device, it would cause
// the string to be sent via the CEC Transmit
RMC3>devjson:1 {"deviceKey":"cec-1-cec", "methodName":"SendText", "params": ["hello world\r"]}
// This command will call SimulateReceive(string text) on the device with Key 'cec-1-cec'
// This would simulate receiving data on the CEC port of the DM-TX-201-C's HDMI input
RMC3>devjson:1 {"deviceKey":"cec-1-cec", "methodName":"SimulateReceive", "params": ["hello citizen of Earth\r"]}
```
For additional examples, see this [file](https://github.com/PepperDash/Essentials/blob/main/devjson%20commands.json).

171
docs/docs/DigitalInput.md Normal file
View File

@@ -0,0 +1,171 @@
# DigitalInput
Digital Inputs can be bridged directly to SIMPL from any device that is both inlcuded within essentials and has a relay.
Consider the following example.
```JSON
{
"template": {
"roomInfo": [
{}
],
"devices": [
{
"key": "processor",
"uid": 0,
"type": "pro3",
"name": "pro3",
"group": "processor",
"supportedConfigModes": [
"compliance",
"essentials"
],
"supportedSystemTypes": [
"hudType",
"presType",
"vtcType",
"custom"
],
"supportsCompliance": true,
"properties": {}
},
{
"key": "DigitalInput-1",
"uid": 3,
"name": "Digital Input 1",
"group": "api",
"type": "digitalInput",
"properties": {
"portDeviceKey" : "processor",
"portNumber" : 1,
"disablePullUpResistor" : true
}
},
{
"key": "DigitalInput-2",
"uid": 3,
"name": "Digital Input 2",
"group": "api",
"type": "digitalInput",
"properties": {
"portDeviceKey" : "processor",
"portNumber" : 2,
"disablePullUpResistor" : true
}
},
{
"key": "deviceBridge",
"uid": 4,
"name": "BridgeToDevices",
"group": "api",
"type": "eiscapiadv",
"properties": {
"control": {
"tcpSshProperties": {
"address": "127.0.0.2",
"port": 0
},
"ipid": "03",
"method": "ipidTcp"
},
"devices": [
{
"deviceKey": "DigitalInput-1",
"joinStart": 1
},
{
"deviceKey": "DigitalInput-2",
"joinStart": 2
}
]
}
}
]
}
}
```
## RelayOutput Configuration Explanation
This configuration is meant for a Pro3 device, and instantiates two relay ports and links them to an eisc bridge to another processor slot on ipid 3. Let's break down the ```DigitalInput-1``` device.
```JSON
{
"key": "DigitalInput-1",
"uid": 3,
"name": "Digital Input 1",
"group": "api",
"type": "digitalInput",
"properties": {
"portDeviceKey" : "processor",
"portNumber" : 1,
"disablePullUpResistor" : true
}
}
```
**```Key```**
The Key is a unique identifier for essentials. The key allows the device to be linked to other devices also defined by key. All Keys MUST be unique, as every device is added to a globally-accessible dictionary. If you have accidentally utilized the same key twice, Essentials will notify you during startup that there is an issue with the device.
**```Uid```**
The Uid is reserved for use with an PepperDash internal config generation tool, and is not useful to Essentials in any way.
**```Name```**
The Name a friendly name assigned to the device. Many devices pass this data to the bridge for utilization in SIMPL.
**```Group```**
Utilized in certain Essentials devices. In this case, the value is unimportant.
**```Type```**
The Type is the identifier for a specific type of device in Essentials. A list of all valid types can be reported by using the consolecommand ```gettypes``` in Essentials. In this case, the type is ```digitalInput```. This type is valid for any instance of a Relay Output.
**```Properties```**
These are the properties essential to the instantiation of the identified type.
### Properties
There are two properties relevant to the instantiation of a relay device.
**```portDeviceKey```**
This property maps to the ```key``` of the device upon which the relay resides.
**```portNumber```**
This property maps to the number of the relay on the device you have mapped the relay device to. Even if the device has only a single relay, ```portNumber``` must be defined.
**```disablePullUpResistor```**
This is a boolean value, therefore it is a case-sensitive ```true``` or ```false``` utilized to determine if the pullup resistor on the digital input will be disabled or not.
### The JoinMap
The joinmap for a ```digitalInput``` device is comprised of a single digital join.
```cs
namespace PepperDash.Essentials.Core.Bridges
{
public class IDigitalInputJoinMap : JoinMapBaseAdvanced
{
[JoinName("InputState")]
public JoinDataComplete InputState = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Room Email Url", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
public IDigitalInputJoinMap(uint joinStart)
: base(joinStart, typeof(IDigitalInputJoinMap))
{
}
}
}
```
```InputState``` is a digital join that represents the feedback for the associated Digital Input Device. Its join is set to 1.

View File

@@ -0,0 +1,128 @@
# Feedback classes
***
* [YouTube Video - Using Feedbacks in PepperDash Essentials](https://youtu.be/5GQVRKbD9Rk)
***
The various Feedback classes are like "signals". They can enable various events, and are designed to be used where we need small data events to be sent without requiring custom handlers.
## Why Feedbacks?
We have been writing "code" in an environment, Simpl, for years and have taken for granted the power that signals in that environment give us. With the release of the ability to develop in C#, we have been handed a massive set of tools to potentially make our lives better, but because of the age and limited scope of the .NET 3.5 Compact Framework, many of the things that have been very easy to do in the past have become challenging or bulky to write. Crestron classes have things called "Sigs", which are a less-functional version of the signal that we used in Simpl, but we have no ability to use our own Sigs around our own classes. This forces us to break out of the constraints and mindset of Simpl programming, but simultaneously keeps us partially bound to the "old way" of doing things.
Signals as we have known them since Simpl came around are great. They allow a certain type of functional programming to be built, where things operate in solutions, and we are given a whole set of behaviors that we don't really have to think about: Something goes high, the next thing responds, something else happens, etc. With our older C# framework, it is most straightforward (and least-flexible) to take Sig transitions and handle them using very-flat and bulky coding techniques: Switch/case blocks, if/else blocks, slow dictionaries... In the Essentials environment (and in many other frameworks) these methods quickly reveal their flaws.
Enter the Feedback. We want to define simple events that can be attached to various things - TP Sigs, EISC, event handlers - and maintain their own state. This simplifies the interface to various device classes, and allows us to define functional, simple classes with well-defined means of connecting them together.
### Feedbacks are similar to signals
Feedbacks can:
- Fire an event (OutputChange)
- Be linked to one or more matching Crestron Sigs and update those Sigs
- May contain complex computations to define the output value
- Be put into test mode and have their value function overridden
A Feedback is defined on a class using a C# construct called a `Func`. A `Func` is a small operation that returns a single value and is typically written in a lambda. The operation/expression in the `Func` is calculated when FireUpdate() is called on the Feedback. The result is then available for all objects listening to this Feedback.
[Func documentation (MSDN)](<https://msdn.microsoft.com/en-us/library/bb534960(v=vs.110).aspx>)
#### Creating Feedbacks
The following `IntFeedback` returns the value of the `_VolumeLevel` field in this display class:
```cs
public class MyDisplay
{
public IntFeedback VolumeLevelFeedback { get; private set; }
...
public MyDisplay(...)
{
VolumeLevelFeedback = new IntFeedback(() => { return _VolumeLevel; });
...
```
This BoolFeedback, adapted from the DmTx201Controller class, defines the `Func` first, and then creates the BoolFeedback using that `Func`. The value returned is true if the input is the digital-HDMI connection, and the TX hardware's VideoAttributes.HdcpActiveFeedback is true as well.
```cs
public class MyTx
{
public BoolFeedback HdcpActiveFeedback { get; private set; }
Func HdcpActiveFeedbackFunc = () =>
ActualVideoInput == DmTx200Base.eSourceSelection.Digital
&& tx.HdmiInput.VideoAttributes.HdcpActiveFeedback.BoolValue,
...
public MyTx(...)
{
HdcpActiveFeedback = new BoolFeedback(HdcpActiveFeedbackFunc);
...
```
#### Triggering Feedback
In your classes, when you need to update the objects listening to a Feedback, you will call MyFeedback.FireUpdate() inside your class. This will trigger the evaluation of the Func value, update any linked Sigs, and fire the OutputChange event.
```cs
int _VolumeLevel;
void ComDataChanged(string data) // volume=77
{
if(data.StartsWith("volume="))
{
_VolumeLevel = MyParseVolumeMethod(data); // get the level, 77
VolumeLevelFeedback.FireUpdate(); // all listeners updated
```
#### Using Feedbacks
Feedbacks of the various types have BoolValue, IntValue, UShortValue, and StringValue properties that return the current value of the Feedback.
```cs
if (MyTxDevice.HdcpActiveFeedback.BoolValue)
{
... do something that needs to happen when HDCP is active ...
```
Feedbacks all share an OutputChange event, that fires an event with an empty EventArgs object. The event handler can go get the appropriate \*Value property when the event fires. The example below is a bit contrived, but explains the idea.
```cs
...
MyDisplayDevice.VolumeLevelFeedback.OutputChange += MyDisplayVolumeHandler;
...
}
void MyDisplayVolumeHandler(object o, EventArgs a)
{
MobileControlServer.VolumeLevel = MyDisplayDevice.VolumeLevelFeedback.IntValue;
```
Feedbacks also have a LinkInputSig(\*InputSig sig) method that can directly trigger one or more Sigs on a Crestron device, without requiring an event handler. This is very useful for attaching states of our devices to Crestron touchpanels or EISCs, for example. The BoolFeedback class also has a LinkComplementInputSig(BoolInputSig sig) method that will invert the BoolFeedback's value to one or more attached Sigs.
As well as updating upon change, the Feedback will set the Sig's value to the Feedback value upon calling the LinkInputSig method. This eliminates the need to walk through an object, property-by-property, and update several Sig values - as well as setting up to watch those values for changes. It is all handled in one step.
```cs
public class MyClass
{
Tsw760 MyTp;
MyDisplay Display;
HookUpSigsMethod()
{
...
// changes to VolumeLevelFeedback will automatically propagate to UShortInputSig 123
// changes to HdcpActiveFeedback will propagate to BoolInputSig 456
// and these two panel Sigs are updated immediately as well.
Display.VolumeLevelFeedback.LinkInputSig(MyTp.UshortInput[123]);
MyHdcpDevice.HdcpActiveFeedback.LinkInputSig(MyTp.BoolInput[456]);
```

381
docs/docs/GenericComm.md Normal file
View File

@@ -0,0 +1,381 @@
# GenericComm
One of the most common scenarios in control system development is utilizing RS232 to connect to a device. Essentials doesn't restrict you to connecting a native essentials device or plugin to the comport. You can directly access the comport, and even set baudrates on the fly if you so desire.
Similarly you can instantiate one of several socket types in this manner and bridge them directly to SIMPL.
Consider the following example.
```JSON
{
"template": {
"roomInfo": [
{}
],
"devices": [
{
"key": "processor",
"uid": 0,
"type": "pro3",
"name": "pro3",
"group": "processor",
"supportedConfigModes": [
"compliance",
"essentials"
],
"supportedSystemTypes": [
"hudType",
"presType",
"vtcType",
"custom"
],
"supportsCompliance": true,
"properties": {}
},
{
"key": "Comport-1",
"uid": 3,
"name": "Comport 1",
"group": "api",
"type": "genericComm",
"properties": {
"control": {
"method": "com",
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 115200,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 1,
"controlPortDevKey": "processor",
}
}
},
{
"key": "Comport-2",
"uid": 3,
"name": "Comport 2",
"group": "api",
"type": "genericComm",
"properties": {
"control": {
"method": "ssh",
"tcpSshProperties": {
"address": "192.168.1.57",
"port": 22,
"username": "",
"password": "",
"autoReconnect": true,
"autoReconnectIntervalMs": 10000
}
}
}
},
{
"key": "deviceBridge",
"uid": 4,
"name": "BridgeToDevices",
"group": "api",
"type": "eiscapiadv",
"properties": {
"control": {
"tcpSshProperties": {
"address": "127.0.0.2",
"port": 0
},
"ipid": "03",
"method": "ipidTcp"
},
"devices": [
{
"deviceKey": "Comport-1",
"joinStart": 1
},
{
"deviceKey": "Comport-2",
"joinStart": 3
}
]
}
}
]
}
}
```
## GenericComm Configuration Explanation
This configuration is meant for a Pro3 device, and instantiates one comport and one SSH session and links them to an eisc bridge to another processor slot on ipid 3. Let's break down the ```Comport-1``` device.
```JSON
{
"key": "Comport-1",
"uid": 3,
"name": "Comport 1",
"group": "comm",
"type": "genericComm",
"properties": {
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 115200,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 1,
"controlPortDevKey": "processor",
"method": "com"
}
}
}
```
**```Key```**
The Key is a unique identifier for essentials. The key allows the device to be linked to other devices also defined by key. All Keys MUST be unique, as every device is added to a globally-accessible dictionary. If you have accidentally utilized the same key twice, Essentials will notify you during startup that there is an issue with the device.
**```Uid```**
The Uid is reserved for use with an PepperDash internal config generation tool, and is not useful to Essentials in any way.
**```Name```**
The Name a friendly name assigned to the device. Many devices pass this data to the bridge for utilization in SIMPL.
**```Group```**
Utilized in certain Essentials devices. In this case, the value is unimportant.
**```Type```**
The Type is the identifier for a specific type of device in Essentials. A list of all valid types can be reported by using the consolecommand ```gettypes``` in Essentials. In this case, the type is ```genericComm```. This type is valid for any instance of a serial-based communications channel such as a Serial Port, SSH, UDP, or standard TCP/IP Socket.
**```Properties```**
These are the properties essential to the instantiation of the identified type.
#### Control
The properties within this property are dependant on the type of genericComm you wish to instantiate. There is one common property for control of any type, and that is ```method```. The ```method``` property requires a string that maps to the following enumerations in Essentials :
```cs
namespace PepperDash.Core
{
// Summary:
// Crestron Control Methods for a comm object
public enum eControlMethod
{
None = 0,
Com = 1,
IpId = 2,
IpidTcp = 3,
IR = 4,
Ssh = 5,
Tcpip = 6,
Telnet = 7,
Cresnet = 8,
Cec = 9,
Udp = 10,
}
}
```
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, and ```Udp```.
##### ComParams
A ```Com``` device requires a ```comParams``` object to set the properties of the comport. The values of all properties are case-insensitive.
```JSON
{
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 115200,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
}
```
**Valid ```hardwareHandshake``` values are as follows**
```sh
"None"
"Rts"
"Cts"
"RtsCts"
```
**Valid ```parity``` values are as follows**
```sh
"None"
"Even"
"Odd"
"Mark"
```
**Valid ```protocol``` values are as follows**
```sh
"RS232"
"RS422"
"RS485"
```
**Valid ```baudRate``` values are as follows**
```sh
300
600
1200
1800
2400
3600
4800
7200
9600
14400
19200
28800
38400
57600
115200
```
**Valid ```dataBits``` values are as follows**
```sh
7
8
```
**Valid ```softwareHandshake``` values are as follows**
```sh
"None"
"XON"
"XONT"
"XONR"
```
**Valid ```stopBits``` values are as follows**
```sh
1
2
```
Additionally, a ```control``` object for a physical hardware port needs to map to that physical port. This is accomplished by utilizing the ```controlPortDevKey``` and ```port``` properties.
**```controlPortDevKey```**
This property maps to the ```key``` of the device upon which the port resides.
**```port```**
This property maps to the number of the port on the device you have mapped the relay device to. Even if the device has only a single port, ```port``` must be defined.
##### TcpSshParams
A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
```Json
{
"tcpSshProperties": {
"address": "192.168.1.57",
"port": 22,
"username": "",
"password": "",
"autoReconnect": true,
"autoReconnectIntervalMs": 10000
}
}
```
**```address```**
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. In the case of a UDP device, you can set either a single whitelist address with this data, or an appropriate broadcast address.
**```port```**
This is the port you wish to utilize for the socket connection. Certain protocols require certain ports - ```Ssh``` being ```22``` and ```Telnet``` being ```23```.
**```username```**
This is the username (if required) for authentication to the device you are connecting to. Typcally only required for ```Ssh``` connections.
**```password```**
This is the password (if required) for authentication to the device you are connecting to. Typcally only required for ```Ssh``` connections.
**```autoreconnect```**
This is a boolean value, therefore it is a case-sensitive ```true``` or ```false``` utilized to determine if the socket will attempt to reconnect upon loss of connection.
**```autoReconnectIntervalMs```**
This is the duration of time, in Miliseconds, that the socket will wait before discrete connection attempts if ```autoreconnect``` is set to true.
##### The JoinMap
The join map for a generic comms device is fairly simple.
```cs
namespace PepperDash.Essentials.Core.Bridges
{
public class IBasicCommunicationJoinMap : JoinMapBaseAdvanced
{
[JoinName("TextReceived")]
public JoinDataComplete TextReceived = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Text Received From Remote Device", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial });
[JoinName("SendText")]
public JoinDataComplete SendText = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Text Sent To Remote Device", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial });
[JoinName("SetPortConfig")]
public JoinDataComplete SetPortConfig = new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 },
new JoinMetadata() { Label = "Set Port Config", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial });
[JoinName("Connect")]
public JoinDataComplete Connect = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Connect", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital });
[JoinName("Connected")]
public JoinDataComplete Connected = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
[JoinName("Status")]
public JoinDataComplete Status = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog });
public IBasicCommunicationJoinMap(uint joinStart)
: base(joinStart, typeof(IBasicCommunicationJoinMap))
{
}
}
}
```
```TextReceived``` is a stream of strings received **FROM** the connected device.
```SendText``` is for any strings you wish to send **TO** the connected device.
```Connect``` connects to a remote socket device on the rising edge of the signal.
```Connected``` represents the current connection state. High for Connected, low for Disconnected.
```Status``` is an analog value that is representative of the connection states as reported by the SIMPL TCP/IP socket symbol.
All of the preceeding joins are set to join ```1```. The second serial input join is reserved for ```2```. It allows you to send a ```comparams``` json object as a string, utilizing the same format mentioned previously in this document. Doing so will override the configured comport specifications.

26
docs/docs/Get-started.md Normal file
View File

@@ -0,0 +1,26 @@
# Get started
---
[YouTube Video - Getting Started with PepperDash Essentials](https://youtu.be/FxEZtbpCwiQ)
***
## Download or clone
You may clone Essentials at <https://github.com/PepperDash/Essentials.git>
## How to Get Started
This section assumes knowledge of loading programs to and working with the file system on a Crestron processor.
1. Using an SFTP client, load `PepperDashEssentials1.4.32.cpz` to the processor in program slot 1 and start the program by sending console command `progload -p:1`
1. On first boot, the Essentials Application will build the necessary configuration folder structure in the User/Program1/ path.
1. The application has some example configuration files included. Copy `/Program01/Example Configuration/EssentialsSpaceHuddleRoom/configurationFile-HuddleSpace-2-Source.json` to the `/User/Program1/` folder.
1. Copy the SGD files from `/Program01/SGD` to `/User/Program1/sgd`
1. Reset the program via console `progreset -p:1`. The program will load the example configuration file.
1. Via console, you can run the `devlist:1` command to get some insight into what has been loaded from the configuration file into the system . This will print the basic device information in the form of ["key"] "Name". The "key" value is what we can use to interact with each device uniquely.
1. Run the command `devprops:1 display-1`. This will print the real-time property values of the device with key "display-1".
1. Run the command `devmethods:1 display-1`. This will print the public methods available for the device with key "display-1".
1. Run the command `devjson:1 {"deviceKey":"display-1","methodName":"PowerOn", "params": []}`. This will call the method PowerOn() on the device with key "display-1".
1. Run the provided example XPanel SmartGraphics project and connect to your processor at the appropriate IPID.
Next: [Standalone use](~/docs/Standalone-Use.md)

View File

@@ -0,0 +1,25 @@
# Glossary of Terms
**Assembly**
An assembly is a file that is automatically generated by the compiler upon successful compilation of every . NET application. It can be either a Dynamic Link Library or an executable file. It is generated only once for an application and upon each subsequent compilation the assembly gets updated.
**Device**
A base class, defined in the PepperDash.Core library (`PepperDash.Core.Device`). It can represent a physical device, or a virtual device or behaviour. Generally, most new classes defined in the Essentials ecosystem should derive from Device.
**DeviceManager**
A static class (`PepperDash.Core.Essentials.DeviceManager`) that contains an unordered collection of Devices. Devices are added/registered to the DeviceManager and later can be retrieved as references by Key.
**Essentials Application**
A Crestron SIMPL# Pro application that is made up of the Essentials Framework and any optionally any number of Essentials Plugins
**Essentials Framework**
The collection of core libraries that make up the framework
**Essentials Plugins**
SIMPL# Pro libraries that reference the Essentials Framework and are loaded at runtime to add or extend functionality
**IKeyed**
An important interface defined in PepperDash.Core that requires a string property named Key, whose value must be unique.
**PepperDash.Core**
A SIMPL# utility library referenced by Essentials Framework.

59
docs/docs/Home.md Normal file
View File

@@ -0,0 +1,59 @@
# Welcome to PepperDash Essentials!
PepperDash Essentials is an open-source framework for control systems, built on Crestron's Simpl# Pro framework. It can be configured as a standalone program capable of running a wide variety of system designs and can also be used to augment other Crestron programs.
Essentials is a collection of C# libraries that can be used in many ways. It is a 100% configuration-driven framework that can be extended to add different workflows and behaviors, either through the addition of new device-types and classes, or via a plug-in mechanism. The framework is a collection of things that are all related and interconnected, but in general do not have strong dependencies on each other.
---
## Get started
- [Download essentials build or clone repo](~/docs/Get-started.md)
- [How to get started](~/docs/Get-started.md)
- [YouTube Video Series Playlist](https://youtube.com/playlist?list=PLKOoNNwgPFZdV5wDEBDZxTHu1KROspaBu)
- [Discord Server](https://discord.gg/6Vh3ssDdPs)
Or use the links to the right to navigate our documentation.
---
## Benefits
- Runs on Crestron 3-Series, **4-Series** and VC-4 Control System platforms
- Reduced hardware overhead compared to S+ and Simpl solutions
- Quick development cycle
- Shared resources made easily available
- More flexibility with less code
- Configurable using simple JSON files
- Is awesome
---
## Comment
The Essentials wiki is clearly in-progress right now. Take a look at the links to the right. We are actively working on this documentation, so please be patient with us. If you have any comments on or suggestions for the documentation, please file an issue here, with as much detail as you can provide: <https://github.com/PepperDash/Essentials/issues>
Thanks!
---
## Collaboration
Essentials is an open-source project and we encourage collaboration on this community project. For features that may not be useful to the greater community, or for just-plain learning, we want to remind developers to try writing plugins for Essentials. More information can be found here: [Plugins](~/docs/Plugins.md)
### Open-source-collaborative workflow
The `main` branch always contain the latest stable version. The `development` branch is used for most development efforts.
[GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) will be used as the workflow for this collaborative project. To contribute, follow this process:
1. Fork this repository ([More Info](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/working-with-forks))
2. Create a branch using standard GitFlow branch prefixes (feature/hotfix) followed by a descriptive name.
- Example: `feature/add-awesomeness` or `hotfix/really-big-oops`
- When working on a new feature or bugfix, branch from the `development` branch. When working on a hotfix, branch from `main`.
3. Make commits as necessary (often is better). And use concise, descriptive language, leveraging issue notation and/or [Closing Keywords](https://help.github.com/articles/closing-issues-using-keywords) to ensure any issues addressed by your work are referenced accordingly.
4. When the scope of the work for your branch is complete, make sure to rebase your branch in case further progress has been made since the repo was forked
5. Create a Pull Request to pull your branch into the appropriate branch in the main repository.
6. Your Pull Request will be reviewed by our team and evaluated for inclusion into the main repository.
Next: [Get started](~/docs/Get-started.md)

View File

@@ -0,0 +1,103 @@
## Legacy IR Driver Bridging
```json
{
"id": "1",
"name": "Apple TV",
"key": "appleTv-1",
"type": "genericIrController",
"uid": 3,
"group": "devices",
"properties": {
"control": {
"method": "ir",
"irFile": "Apple_AppleTV_4th_Gen_Essentials.ir",
"controlPortDevKey": "processor",
"controlPortNumber": "1"
}
}
}
```
## Bridge Join Map IR Driver Bridging
```json
{
"id": "1",
"name": "Apple TV",
"key": "appleTv-1",
"type": "genericIrController",
"uid": 3,
"group": "devices",
"properties": {
"control": {
"method": "ir",
"irFile": "Apple_AppleTV_4th_Gen_Essentials.ir",
"controlPortDevKey": "processor",
"controlPortNumber": "1",
"useBridgeJoinMap": true
}
}
}
```
Both methods will bridge the IR signals with `Standard Command` defined in the IR file.
The `useBridgeJoinMap` property implements `GenericIrControllerJoinMap.cs` to standardized IR driver `Standard Command` signal joins. This allows users to swap IR drivers that implement `Standard Command` while bridging IR signals consistently between drivers. For example, when `useBridgeJoinMap` is present, channel up will be mapped to join-22 + device `joinstart` for any IR driver that has the signal marked as `Standard Command`.
## GenericIrControllerJoinMap (Example)
### Digitals
| Join Number | Join Span | Description | Type | Capabilities |
| ----------- | --------- | ----------- | ------------------- | ------------ |
| 1 | 1 | PLAY | Digital | FromSIMPL |
| 2 | 1 | STOP | Digital | FromSIMPL |
| 3 | 1 | PAUSE | Digital | FromSIMPL |
| 4 | 1 | FSCAN | Digital | FromSIMPL |
| 5 | 1 | RSCAN | Digital | FromSIMPL |
| 9 | 1 | POWER | Digital | FromSIMPL |
| 10 | 1 | 0 | Digital | FromSIMPL |
| 11 | 1 | 1 | Digital | FromSIMPL |
| 12 | 1 | 2 | Digital | FromSIMPL |
| 13 | 1 | 3 | Digital | FromSIMPL |
| 14 | 1 | 4 | Digital | FromSIMPL |
| 15 | 1 | 5 | Digital | FromSIMPL |
| 16 | 1 | 6 | Digital | FromSIMPL |
| 17 | 1 | 7 | Digital | FromSIMPL |
| 18 | 1 | 8 | Digital | FromSIMPL |
| 19 | 1 | 9 | Digital | FromSIMPL |
| 21 | 1 | ENTER | Digital | FromSIMPL |
| 22 | 1 | CH+ | Digital | FromSIMPL |
| 23 | 1 | CH- | Digital | FromSIMPL |
| 27 | 1 | POWER_ON | Digital | FromSIMPL |
| 28 | 1 | POWER_OFF | Digital | FromSIMPL |
| 30 | 1 | LAST | Digital | FromSIMPL |
| 41 | 1 | BACK | Digital | FromSIMPL |
| 42 | 1 | GUIDE | Digital | FromSIMPL |
| 43 | 1 | INFO | Digital | FromSIMPL |
| 44 | 1 | MENU | Digital | FromSIMPL |
| 45 | 1 | UP_ARROW | Digital | FromSIMPL |
| 46 | 1 | DN_ARROW | Digital | FromSIMPL |
| 47 | 1 | LEFT_ARROW | Digital | FromSIMPL |
| 48 | 1 | RIGHT_ARROW | Digital | FromSIMPL |
| 49 | 1 | SELECT | Digital | FromSIMPL |
| 54 | 1 | PAGE_UP | Digital | FromSIMPL |
| 55 | 1 | PAGE_DOWN | Digital | FromSIMPL |
| 61 | 1 | A | Digital | FromSIMPL |
| 62 | 1 | B | Digital | FromSIMPL |
| 63 | 1 | C | Digital | FromSIMPL |
| 64 | 1 | D | Digital | FromSIMPL |
### Analogs
| Join Number | Join Span | Description | Type | Capabilities |
| ----------- | --------- | ----------- | ------------------- | ------------ |
### Serials
| Join Number | Join Span | Description | Type | Capabilities |
| ----------- | --------- | ----------- | ------------------- | ------------ |

155
docs/docs/JoinMaps.md Normal file
View File

@@ -0,0 +1,155 @@
# What is a Join Map?
A join map is a class that defines the list of joins accessible to an `EssentialsBridgeableDevice` across an EISC Bridge.
## Why use a Join Map?
A join map is necessary to bridge joins across an EISC bridge from Essentials to a SIMPL program. Join maps can be overriden in a configuration as necessary, but by default each device has a standard join map that publishes joins as a function of the joinOffset property of the device within a given Essentials Bridge. Join maps are reusable and extensible. Several join maps for standard device types already exist within Essentials, and those can be utilized for plugin creation without creation of a new join map. Utilizing standard join maps allows you to create a consistent API between device types that allows switching of devices via config without any new SIMPL or SIMPL#Pro code being written.
## How Join maps Work
Whenever you instantiate a device and link that device to an EISC bridge utilizing your configuration in Essentials, the method `LinkToApi()` is called. This method matches various methods, feedbacks, and properties to joins on the EISC bridge in order to create a consistent API for communication to SIMPL.
Whenever `LinkToApi()` is called, it creates a new instance of the device's joinMap class on demand. The constructor of that joinMap creates an object containing the join metadata, adds any configured join offsets to the standard join map, and adds all associated joins to a global join list that can be easily referenced from the command line.
There are several components for each join within a join map.
```cs
[JoinName("Online")]
public JoinDataComplete Online = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Reports Online Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
```
### Attribute
```cs
[JoinName("Online")]
```
If the attribute is present, the join data is added to the publically available list `Joins`. This can be used to "prebuild" functionality within a join map that you may not yet need. If you do not add this attribute (or simply comment it out), the join data will not be displayed whenever join data is printed using the `getjoinmap` command.
### JoinData
```cs
JoinData() { JoinNumber = 1, JoinSpan = 1 };
```
`JoinData` contains the pertinent information for the bridge. JoinData contains the information that the bridge utilizes to create each associated connection from the EISC to the methods, properties, and feedbacks associated with a device.
`JoinNumber` is the 1-based index of the join you wish to tie to a given method, property, or feedback. This join, combined with the offset defined in the brdige's configuration for a device, will give you the SIMPL EISC join linked to the given data.
`JoinSpan` determines a number of associated joins. Perhaps you have a list of Camera Presets, or a list of inputs. You can create one single join map entry and define the span of all associated join types.
A `JoinData` object with a `JoinNumber` of 11 and a `JoinSpan` of 10 and a `joinOffset` of 0 is associated with joins 11-20 on the EISC.
### JoinMetadata
```cs
JoinMetadata() { Label = "Reports Online Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital }
```
`JoinMetadata` provides the data reported when the `getjoinmap` command is used.
`Label` is the description of the what this join does.
`JoinCapabilities` is represented by an enum defining the direction that the data is flowing for this join. Appropriate values are:
```cs
public enum eJoinCapabilities
{
None = 0,
ToSIMPL = 1,
FromSIMPL = 2,
ToFromSIMPL = ToSIMPL | FromSIMPL
}
```
`JoinType` is represented by an enum defining the data type in SIMPL. Appropriate values are:
```cs
public enum eJoinType
{
None = 0,
Digital = 1,
Analog = 2,
Serial = 4,
DigitalAnalog = Digital | Analog,
DigitalSerial = Digital | Serial,
AnalogSerial = Analog | Serial,
DigitalAnalogSerial = Digital | Analog | Serial
}
```
### JoinDataComplete
```chsarp
JoinDataComplete(JoinData data, JoinMetadata metadata);
```
`JoinDataComplete` represents the `JoinData` and the `JoinMetadata` in a single object. You can call an instance of `JoinDataComplete` to report any information about a specific join. In a device bridge, you would utilize the `JoinNumber` property to link a feature from the plugin to the EISC API.
### Example Join Map
This is the join map for `IBasicCommunication` Devices
```cs
namespace PepperDash.Essentials.Core.Bridges
{
public class IBasicCommunicationJoinMap : JoinMapBaseAdvanced
{
[JoinName("TextReceived")]
public JoinDataComplete TextReceived = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Text Received From Remote Device", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial });
[JoinName("SendText")]
public JoinDataComplete SendText = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Text Sent To Remote Device", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial });
[JoinName("SetPortConfig")]
public JoinDataComplete SetPortConfig = new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 },
new JoinMetadata() { Label = "Set Port Config", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial });
[JoinName("Connect")]
public JoinDataComplete Connect = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Connect", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital });
[JoinName("Connected")]
public JoinDataComplete Connected = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
[JoinName("Status")]
public JoinDataComplete Status = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog });
public IBasicCommunicationJoinMap(uint joinStart)
: base(joinStart, typeof(IBasicCommunicationJoinMap))
{
}
}
}
```
### Returning Data on Join Maps
A mechanism for printing join maps to console from a running Essentials program is built in to Essentials.
Given a single Generic Communication device with a `joinStart` of 11 and a key of "Com-1", defined on a bridge with a key of "Bridge-1" and IPID of A0, the command `getjoinmap Bridge-1 Com-1` would return:
```sh
Join Map for device 'Com-1' on EISC 'Bridge-1':
GenericComm:
Digitals:
Found 2 Digital Joins
Join Number: '11' | JoinSpan: '1' | Label : 'Connect' | Type: 'Digital' | Capabilities : 'FromSimpl'
Join Number: '11' | JoinSpan: '1' | Label : 'Connected' | Type: 'Digital' | Capabilities : 'ToSimpl'
Analogs:
Found 1 Analog Joins
Join Number: '11' | JoinSpan: '1' | Label : 'Status' | Type: 'Analog' | Capabilities : 'ToSimpl'
Serials:
Found 3 Serial Joins
Join Number: '11' | JoinSpan: '1' | Label : 'Text Received From Remote Device' | Type: 'Serial' | Capabilities : 'FromSimpl'
Join Number: '11' | JoinSpan: '1' | Label : 'Text Sent To Remote Device' | Type: 'Serial' | Capabilities : 'ToSimpl'
Join Number: '12' | JoinSpan: '1' | Label : 'Set Port Config' | Type: 'Serial' | Capabilities : 'FromSimpl'
```

View File

@@ -0,0 +1,74 @@
# Deprecated
**Note : this entry is out of date - please see [Plugins](~/docs/Plugins.md)**
## What are Essentials Plugins?
Plugins are SIMPL# Pro libraries that reference the Essentials Framework and can be loaded into an Essentials Application at runtime to extend functionality beyond what the Essentials Framework provides on its own.
## Why Use Plugins?
Plugins are a way to extend or add new functionality to the Essentials Application without having to modify the actual Framework. In most cases, a plugin can be written to support a new device or behavior. Using plugins also limits the scope of understanding needed to work within the Essentials Framework.
## Should I use a Plugin?
Essentials is meant to be a lightweight framework and an extensible basis for development. While some devices are included in the framework, mostly for the purposes of providing examples and developing and prototyping new device types, the bulk of new development is intended to take place in Plugins. Once a plugin adds new functionality that may be of benefit if shared across multiple plugins, it may make sense to port that common logic (base classes and/or interfaces) back into the framework to make it available to others. The thrust of future Essentials development is targeted towards building a library of plugins.
## How do Plugins Work?
One or more plugins can be loaded to the /user/ProgramX/plugins as .dlls or .cplz packages. When the Essentials Application starts, it looks for any .cplz files, unzips them and then iterates any .dll assemblies in that folder and loads them. Once the plugin assemblies are loaded the Essentials Application will then attempt to load a configuration file and construct items as defined in the file. Those items can be defined in either the Essentials Framework or in any of the loaded plugin assemblies.
![Architecture drawing](~/docs/images/Plugin%20Load%20Sequence.png)
## What Must be Implemented in a Plugin for it to Work?
All plugin assemblies must contain a static method called LoadPlugin():
```cs
public class SomeDevice : Device , IBridge //IBridge only needs to be implemented if using a bridge
{
// This string is used to define the minimum version of the
// Essentials Framework required for this plugin
public static string MinimumEssentialsFrameworkVersion = "1.4.23";
// This method gets called by the Essentials Framework when the application starts.
// It is intended to be used to load the new Device type(s) specified in the plugin
public static void LoadPlugin()
{
DeviceFactory.AddFactoryForType("typeName", FactoryMethod);
// typeName should be the unique string that identifies the type of device to build,
// FactoryMethod represents the method that takes a DevicConfig object as and argument
// and returns an instance of the desired device type
}
// This is a factory method to construct a device and return it to be
// added to the DeviceManager
public static Device FactoryMethod(DeviceConfig dc)
{
return new SomeDevice(dc.key, dc.name, dc);
}
#region IBridge
// This method is called by an EiscApi bridge instance and should call an extension method
// defined in your plugin. Required for implementing IBridge
public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey)
{
this.LinkToApiExt(trilist, joinStart, joinMapKey);
}
#endregion
}
```
## SIMPL Bridging
Optionally, if your plugin device needs to be able to bridge to a SIMPL program over EISC, and there isn't already an existing bridge class in the Essentials Framework, you can write a new bridge class in your plugin. However, in order for the Essentials Application to be able to us that new bridge, the bridge must implement the IBridge interface with the required LinkToApi() Extension method.
Often though, you may find that a bridge class already exists in the Essentials Framework that you can leverage. For example, if you were writing a plugin to support a new display model that isn't already in the Essentials Framework, you would define a class in your plugin that inherits from PepperDash.Essentials.Core.DisplayBase. If you're only implementing the standard display control functions such as power/input/volume control, then the existing bridge class `DisplayControllerBridge` can be used. If you needed to add additional functions to the bridge, then you would need to write your own bridge in the plugin.
For additional info see the [SIMPL-Bridging article](~/docs/SIMPL-Bridging.md).
## Template Essentials Plugin Repository
Fork this repository when starting a new plugin. The template repository uses the essentials-builds repository as a submodule. This allows the plugin to reference a specific build version of Essentials. You must make sure that you checkout the correct build of the Essentials-Builds repo that contains any dependencies that your plugin may rely on.
[Essentials Plugin Template Repository](https://github.com/PepperDash/EssentialsPluginTemplate)

131
docs/docs/Plugins.md Normal file
View File

@@ -0,0 +1,131 @@
# What are Essentials Plugins?
**Note : this entry updates a deprecated method - for information related to that deprecated method, see [Plugins - Deprecated](~/docs/Plugins-Deprecated.md)**
***
* [YouTube Video - Loading Plugins in PepperDash Essentials](https://youtu.be/NA64iyNNAgE)
* [YouTube Video - Build Your Own Plugin, Part 1](https://youtu.be/m2phC8g3Kfk)
* [YouTube Video - Build Your Own Plugin, Part 2](https://youtu.be/2_PrWRk6Gy0)
***
Plugins are SIMPL# Pro libraries that reference the Essentials Framework and can be loaded into an Essentials Application at runtime to extend functionality beyond what the Essentials Framework provides on its own.
## Why Use Plugins?
Plugins are a way to extend or add new functionality to the Essentials Application without having to modify the actual Framework. In most cases, a plugin can be written to support a new device or behavior. Using plugins also limits the scope of understanding needed to work within the Essentials Framework.
## Should I use a Plugin?
Essentials is meant to be a lightweight framework and an extensible basis for development. While some devices are included in the framework, mostly for the purposes of providing examples and developing and prototyping new device types, the bulk of new development is intended to take place in Plugins. Once a plugin adds new functionality that may be of benefit if shared across multiple plugins, it may make sense to port that common logic (base classes and/or interfaces) back into the framework to make it available to others. The thrust of future Essentials development is targeted towards building a library of plugins.
## How do Plugins Work?
One or more plugins can be loaded to the /user/ProgramX/plugins as .dlls or .cplz packages. When the Essentials Application starts, it looks for any .cplz files, unzips them and then iterates any .dll assemblies in that folder and loads them. Once the plugin assemblies are loaded the Essentials Application will then attempt to load a configuration file and construct items as defined in the file. Those items can be defined in either the Essentials Framework or in any of the loaded plugin assemblies.
![Architecture drawing](~/docs/images/Plugin%20Load%20Sequence.png)
## What Must be Implemented in a Plugin for it to Work?
All plugin assemblies must contain a class which inherits from ```EssentialsPluginDeviceFactory<T>```, where ```<T>``` is a class which inherits from ```PepperDash.Essentials.Core.EssentialsDevice```
Within this class, we will define some metadata for the plugin and define which constructor to use
for instantiating each class as defined by type.
Note that multiple types can be loaded from the same plugin.
```cs
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
using PepperDash.Core;
using System.Collections.Generic;
namespace MyPlugin
{
public class SomeDeviceFactory : EssentialsPluginDeviceFactory<SomeDevice>
{
// This method defines metadata for the devices in the plugin
public SomeDeviceFactory()
{
// This string is used to define the minimum version of the
// Essentials Framework required for this plugin
MinimumEssentialsFrameworkVersion = "1.5.0";
// This string is used to define all valid type names for
// this plugin. This string is case insensitive
TypeNames = new List<string> { "SomeDevice" , "ThisNewDevice" };
}
// This method instantiates new devices for the defined type
// within the plugin
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
// Deserialize the json properties for the loaded type to a new object
var props = dc.Properties.ToObject<SomeDeviceProperties>();
// Return a newly instantiated device using the desired constructor
// If no valid property data exists, return null with console print
// describing the failure.
if (props == null)
{
Debug.Console(0, "No valid property data for 'SomeDevice' - Verify Configuration.");
Debug.Console(0, dc.Properties.ToString());
return null;
}
return new SomeDevice(dc.Key, dc.Name, props);
}
}
public class OtherDeviceFactory : EssentialsPluginDeviceFactory<OtherDevice>
{
// This method defines metadata for the devices in the plugin
public OtherDeviceFactory()
{
// This string is used to define the minimum version of the
// Essentials Framework required for this plugin
MinimumEssentialsFrameworkVersion = "1.5.0";
// This string is used to define all valid type names for
// this plugin. This string is case insensitive
TypeNames = new List<string> { "OtherDevice", "ThisOtherDevice" };
}
// This method instantiates new devices for the defined type
// within the plugin
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
// Deserialize the json properties for the loaded type to a new object
var props = dc.Properties.ToObject<OtherDeviceProperties>();
// Return a newly instantiated device using the desired constructor
// If no valid property data exists, return null with console print
// describing the failure.
if (props == null)
{
Debug.Console(0, "No valid property data for 'OtherDevice' - Verify Configuration.");
Debug.Console(0, dc.Properties.ToString());
return null;
}
return new OtherDevice(dc.Key, dc.Name, props);
}
}
}
```
## SIMPL Bridging
Optionally, if your plugin device needs to be able to bridge to a SIMPL program over EISC, and there isn't already an existing bridge class in the Essentials Framework, you can write a new bridge class in your plugin. However, in order for the Essentials Application to be able to us that new bridge, the bridge must implement the ```IBridgeAdvanced``` interface with the required ```LinkToApi()``` Extension method.
If you are writing code for a bridgeable device, you should be inheriting from ```EssentialsBridgeableDevice```, which inherits from the already-required ```EssentialsDevice``` and implements ```IBridgeAdvanced```.
Often though, you may find that a bridge class already exists in the Essentials Framework that you can leverage. For example, if you were writing a plugin to support a new display model that isn't already in the Essentials Framework, you would define a class in your plugin that inherits from PepperDash.Essentials.Core.DisplayBase. If you're only implementing the standard display control functions such as power/input/volume control, then the existing bridge class `DisplayControllerBridge` can be used. If you needed to add additional functions to the bridge, then you would need to write your own bridge in the plugin.
For additional info see the [SIMPL-Bridging article](~/docs/SIMPL-Bridging.md).
## Template Essentials Plugin Repository
Fork this repository when starting a new plugin. The template repository uses the essentials-builds repository as a submodule. This allows the plugin to reference a specific build version of Essentials. You must make sure that you checkout the correct build of the Essentials-Builds repo that contains any dependencies that your plugin may rely on.
[Essentials Plugin Template Repository](https://github.com/PepperDash/EssentialsPluginTemplate)

164
docs/docs/RelayOutput.md Normal file
View File

@@ -0,0 +1,164 @@
# RelayOutput
Relays can be bridged directly to SIMPL from any device that is both inlcuded within essentials and has a relay.
Consider the following example.
```JSON
{
"template": {
"roomInfo": [
{}
],
"devices": [
{
"key": "processor",
"uid": 0,
"type": "pro3",
"name": "pro3",
"group": "processor",
"supportedConfigModes": [
"compliance",
"essentials"
],
"supportedSystemTypes": [
"hudType",
"presType",
"vtcType",
"custom"
],
"supportsCompliance": true,
"properties": {}
},
{
"key": "Relay-1",
"uid": 3,
"name": "Relay 1",
"group": "api",
"type": "relayOutput",
"properties": {
"portDeviceKey" : "processor",
"portNumber" : 1
}
},
{
"key": "Relay-2",
"uid": 3,
"name": "Relay 2",
"group": "api",
"type": "relayOutput",
"properties": {
"portDeviceKey" : "processor",
"portNumber" : 2
}
},
{
"key": "deviceBridge",
"uid": 4,
"name": "BridgeToDevices",
"group": "api",
"type": "eiscapiadv",
"properties": {
"control": {
"tcpSshProperties": {
"address": "127.0.0.2",
"port": 0
},
"ipid": "03",
"method": "ipidTcp"
},
"devices": [
{
"deviceKey": "Relay-1",
"joinStart": 1
},
{
"deviceKey": "Relay-2",
"joinStart": 2
}
]
}
}
]
}
}
```
## RelayOutput Configuration Explanation
This configuration is meant for a Pro3 device, and instantiates two relay ports and links them to an eisc bridge to another processor slot on ipid 3. Let's break down the ```Relay-1``` device.
```JSON
{
"key": "Relay-1",
"uid": 3,
"name": "Relay 1",
"group": "api",
"type": "relayOutput",
"properties": {
"portDeviceKey" : "processor",
"portNumber" : 1
}
}
```
**```Key```**
The Key is a unique identifier for essentials. The key allows the device to be linked to other devices also defined by key. All Keys MUST be unique, as every device is added to a globally-accessible dictionary. If you have accidentally utilized the same key twice, Essentials will notify you during startup that there is an issue with the device.
**```Uid```**
The Uid is reserved for use with an PepperDash internal config generation tool, and is not useful to Essentials in any way.
**```Name```**
The Name a friendly name assigned to the device. Many devices pass this data to the bridge for utilization in SIMPL.
**```Group```**
Utilized in certain Essentials devices. In this case, the value is unimportant.
**```Type```**
The Type is the identifier for a specific type of device in Essentials. A list of all valid types can be reported by using the consolecommand ```gettypes``` in Essentials. In this case, the type is ```relayOutput```. This type is valid for any instance of a Relay Output.
**```Properties```**
These are the properties essential to the instantiation of the identified type.
### Properties
There are two properties relevant to the instantiation of a relay device.
**```portDeviceKey```**
This property maps to the ```key``` of the device upon which the relay resides.
**```portNumber```**
This property maps to the number of the relay on the device you have mapped the relay device to. Even if the device has only a single relay, ```portNumber``` must be defined.
### The JoinMap
The joinmap for a ```relayOutput``` device is comprised of a single digital join.
```cs
namespace PepperDash.Essentials.Core.Bridges
{
public class GenericRelayControllerJoinMap : JoinMapBaseAdvanced
{
[JoinName("Relay")]
public JoinDataComplete Relay = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Device Relay State Set / Get", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital });
public GenericRelayControllerJoinMap(uint joinStart)
: base(joinStart, typeof(GenericRelayControllerJoinMap))
{
}
}
}
```
```Relay``` is a digital join that represents both the trigger and the feedback for the associated relay device. Its join is set to 1.

View File

@@ -0,0 +1,477 @@
# Deprecated
**Note : this entry is out of date - please see [SIMPL Windows Bridging](~/docs/SIMPL-Bridging.md)**
## SIMPL Windows Bridging - Deprecated
Essentials allows for devices defined within the SIMPL# Pro application to be bridged to a SIMPL Windows application over Ethernet Intersystem Communication (EISC). This allows a SIMPL Windows program to take advantage of some of the features of the SIMPL# Pro environment, without requiring the entire application to be written in C#.
Some of the main advantages are:
1. The ability to instantiate devices from configuration.
1. The ability to leverage C# concepts to handle data intensive tasks (Serialization/Deserialization of JSON/XML, cyrptography, etc.).
1. The ability to reuse the same compiled SIMPL Windows program (regardless of target processor type) by offloading all the variables that may be room or hardware specific to Essentials.
1. The ability to handle multiple communciation types generically without changing the SIMPL Program (TCP/UDP/SSH/HTTP/HTTPS/CEC, etc.)
1. Much faster development cycle
1. Reduced processor overhead
1. Ability to easily share devices defined in Essentials between multiple other programs
## Implementation
Bridges are devices that are defined within the devices array in the config file. They are unique devices with a specialized purpose; to act as a bridge between Essentials Devices and applications programmed traditionally in Simpl Windows. This is accomplished by instantiating a Three Series Intersystem Communication symbol within the bridge device, and linking its Boolean/Ushort/String inputs and outputs to actions on one or multiple Essentials device(s). The definition for which joins map to which actions is defined within the device to be bridged to in a class that derives from JoinMapBase.
Let's consider the following Essentials Configuration:
```JSON
{
"template": {
"roomInfo": [
{}
],
"devices": [
{
"key": "processor",
"uid": 1,
"type": "pro3",
"name": "PRO3 w/o cards",
"group": "processor",
"supportedConfigModes": [
"essentials"
],
"supportedSystemTypes": [
"hudType",
"presType",
"vtcType",
"custom"
],
"supportsCompliance": true,
"properties": {
"numberOfComPorts": 6,
"numberOfIrPorts": 8,
"numberOfRelays": 8,
"numberOfDIOPorts": 8
}
},
{
"key": "panasonicDisplay01",
"type": "PanasonicThefDisplay",
"name": "Main Display",
"group": "displays",
"uid": 2,
"properties": {
"id": "01",
"inputNumber": 1,
"outputNumber": 1,
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 9600,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 1,
"controlPortDevKey": "processor",
"method": "com"
}
}
},
{
"key": "vtcComPort",
"uid": 3,
"name": "VTC Coms",
"group": "comm",
"type": "genericComm",
"properties": {
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 38400,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 2,
"controlPortDevKey": "processor",
"method": "com"
}
}
},
{
"key": "deviceBridge",
"uid": 4,
"name": "BridgeToDevices",
"group": "api",
"type": "eiscApi",
"properties": {
"control": {
"tcpSshProperties": {
"address": "127.0.0.2",
"port": 0
},
"ipid": "03",
"method": "ipidTcp"
},
"devices": [
{
"deviceKey": "panasonicDisplay01",
"joinStart": 1
},
{
"deviceKey": "vtcComPort",
"joinStart": 51
}
]
}
}
]
}
}
```
We have four Essentials Devices configured:
1. Pro3 with a Key of "processor"
1. Panasonic Display with a Key of "panasonicDisplay01"
1. Com port with a Key of "vtcComPort"
1. Bridge with a Key of "deviceBridge"
We want to have access to the com port for VTC Control from Simpl Windows and we want to control the display from Simpl Windows. To accomplish this, we have created a bridge device and added the devices to be bridged to the "devices" array on the bridge. As you can see we define the device key and the join start, which will determine which joins we will use on the resulting EISC to interact with the devices. In the Bridge control properties we defined ipid 03, and we will need a corresponding Ethernet System Intercommunication in the Simpl Windows program at ipid 03.
Now that our devices have been built, we can refer to the device join maps to see which joins correspond to which actions.
See below:
```cs
namespace PepperDash.Essentials.Bridges
{
public class DisplayControllerJoinMap : JoinMapBase
{
#region Digitals
/// <summary>
/// Turns the display off and reports power off feedback
/// </summary>
public uint PowerOff { get; set; }
/// <summary>
/// Turns the display on and repots power on feedback
/// </summary>
public uint PowerOn { get; set; }
/// <summary>
/// Indicates that the display device supports two way communication when high
/// </summary>
public uint IsTwoWayDisplay { get; set; }
/// <summary>
/// Increments the volume while high
/// </summary>
public uint VolumeUp { get; set; }
/// <summary>
/// Decrements teh volume while high
/// </summary>
public uint VolumeDown { get; set; }
/// <summary>
/// Toggles the mute state. Feedback is high when volume is muted
/// </summary>
public uint VolumeMute { get; set; }
/// <summary>
/// Range of digital joins to select inputs and report current input as feedback
/// </summary>
public uint InputSelectOffset { get; set; }
/// <summary>
/// Range of digital joins to report visibility for input buttons
/// </summary>
public uint ButtonVisibilityOffset { get; set; }
/// <summary>
/// High if the device is online
/// </summary>
public uint IsOnline { get; set; }
#endregion
#region Analogs
/// <summary>
/// Analog join to set the input and report current input as feedback
/// </summary>
public uint InputSelect { get; set; }
/// <summary>
/// Sets the volume level and reports the current level as feedback
/// </summary>
public uint VolumeLevel { get; set; }
#endregion
#region Serials
/// <summary>
/// Reports the name of the display as defined in config as feedback
/// </summary>
public uint Name { get; set; }
/// <summary>
/// Range of serial joins that reports the names of the inputs as feedback
/// </summary>
public uint InputNamesOffset { get; set; }
#endregion
public DisplayControllerJoinMap()
{
// Digital
IsOnline = 50;
PowerOff = 1;
PowerOn = 2;
IsTwoWayDisplay = 3;
VolumeUp = 5;
VolumeDown = 6;
VolumeMute = 7;
ButtonVisibilityOffset = 40;
InputSelectOffset = 10;
// Analog
InputSelect = 11;
VolumeLevel = 5;
// Serial
Name = 1;
InputNamesOffset = 10;
}
public override void OffsetJoinNumbers(uint joinStart)
{
var joinOffset = joinStart - 1;
IsOnline = IsOnline + joinOffset;
PowerOff = PowerOff + joinOffset;
PowerOn = PowerOn + joinOffset;
IsTwoWayDisplay = IsTwoWayDisplay + joinOffset;
ButtonVisibilityOffset = ButtonVisibilityOffset + joinOffset;
Name = Name + joinOffset;
InputNamesOffset = InputNamesOffset + joinOffset;
InputSelectOffset = InputSelectOffset + joinOffset;
InputSelect = InputSelect + joinOffset;
VolumeUp = VolumeUp + joinOffset;
VolumeDown = VolumeDown + joinOffset;
VolumeMute = VolumeMute + joinOffset;
VolumeLevel = VolumeLevel + joinOffset;
}
}
}
```
We know that the Panasonic Display uses the DisplayControllerJoinMap class and can see the join numbers that will give us access to functionality in the Device.
IsOnline = 50
PowerOff = 1
PowerOn = 2
IsTwoWayDisplay = 3
VolumeUp = 5
VolumeDown = 6
VolumeMute = 7
```cs
namespace PepperDash.Essentials.Bridges
{
public class IBasicCommunicationJoinMap : JoinMapBase
{
#region Digitals
/// <summary>
/// Set High to connect, Low to disconnect
/// </summary>
public uint Connect { get; set; }
/// <summary>
/// Reports Connected State (High = Connected)
/// </summary>
public uint Connected { get; set; }
#endregion
#region Analogs
/// <summary>
/// Reports the connections status value
/// </summary>
public uint Status { get; set; }
#endregion
#region Serials
/// <summary>
/// Data back from port
/// </summary>
public uint TextReceived { get; set; }
/// <summary>
/// Sends data to the port
/// </summary>
public uint SendText { get; set; }
/// <summary>
/// Takes a JSON serialized string that sets a COM port's parameters
/// </summary>
public uint SetPortConfig { get; set; }
#endregion
public IBasicCommunicationJoinMap()
{
TextReceived = 1;
SendText = 1;
SetPortConfig = 2;
Connect = 1;
Connected = 1;
Status = 1;
}
public override void OffsetJoinNumbers(uint joinStart)
{
var joinOffset = joinStart - 1;
TextReceived = TextReceived + joinOffset;
SendText = SendText + joinOffset;
SetPortConfig = SetPortConfig + joinOffset;
Connect = Connect + joinOffset;
Connected = Connected + joinOffset;
Status = Status + joinOffset;
}
}
}
```
TextReceived = 1
SendText = 1
SetPortConfig = 2
Connect = 1
Connected = 1
Status = 1
Considering our Bridge config, we can see that the display controls will start at join 1, and the VTC Com port will start at join 51. The result is a single EISC that allows us to interact with our Essentials devices.
To control diplay power from Simpl Windows, we would connect Digital Signals to joins 1 & 2 on the EISC to control Display Power On & Off.
To utilize the com port device, we would connect Serial Signals (VTC_TX$ and VTC_RX$) to join 51 on the EISC.
You can refer to our [Simpl Windows Bridging Example](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for a more complex example.
Example device config: <https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json>
## Notes
1. It is important to realize that there are no safety checks (yet) when assigning joinStarts in bridge configurations. If you were to put two devices on a bridge with overlapping joins, the most recently bridged join would overwrite previously bridged joins. For now it is on the programmer to ensure there are no conflicting join maps.
1. There is _no_ limit to the amount of times a device may be bridged to. You may have the same device on multiple bridges across multiple applications without problem. That being said, we recommend using common sense. Accessing a single com port for VTC control via multiple bridges may not be wise...
1. A bridge need not only bridge between applications on the same processor. A bridge may bridge to an application on a completely separate processor; simply define the ip address in the Bridge control properties accordingly.
1. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/Feedback-Classes.md) for this.
1. When building plugins, we highly recommend reusing JoinMaps, as this will make code more easily interchangeable. For example; if you were to build a display plugin, we'd recommend you use/extend the existing DisplayControllerJoinMap. This way, you can swap plugins without needing any change on the Simpl Windows side. This is extremely powerful when maintaining Simpl Windows code bases for large deployments that may utilize differing equipment per room. If you can build a Simpl Windows program that interacts with established join maps, you can swap out the device via config without any change needed to Simpl Windows.
1. Related to item 5, you can use the same paradigm with respect to physical device communication. If you were to have a DSP device in some rooms communicating over RS232 and some via SSH, it would be trival to swap the device from a Com port to an SSH client in the Essentials Devicee Config and update the Bridge Config to brigde to the desired communication method. Again this would require no change on the Simpl Windows side as long as you maintain the same join Start in the Bridge Device Configuration.
## Common Use Cases
1. There are 10 conference rooms that all operate the same, but have hardware differences that are impossible to account for in SIMPL Windows. For example, each room might have a DM-MD8X8 chassis, but the input and output cards aren't all in the same order, or they might be different models but function the same. You can use Essentials with a unique configuration file for each hardware configuration.
1. You have a floor of conference rooms that all share some centralized hardware like DSP, AV Routing and a shared CEN-GWEXER gateway with multiple GLS-OIR-CSM-EX-BATT occupancy sensors. All the shared hardware can be defined in the Essentials configuration and bridged over an EISC to each program that needs access. The same device can even be exposed to multiple programs over different EISCs.
1. You have a SIMPL program that works for many room types, but because some rooms have different models of processors than others (CP3/CP3N/AV3/PRO3/DMPS3 variants), you have to maintain several versions of the program, compiled for each processor model to maintain access to features like the System Monitor slot. You can use Essentials running in a slot on a processor to expose the System Monitor and many other features of the processor, regardless of model. Now you only need to maintain a single SIMPL program defined for your most complex processor application (ex. PRO3)
## Device Type Join Maps
### AirMediaController
> supports: AM-200, AM-300
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/AirMediaControllerJoinMap.cs>
### AppleTvController
> supports: IR control of Apple TV
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/AppleTvJoinMap.cs>
### CameraControlBase
> supports: any camera that derives from CameraBase
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/CameraControllerJoinMap.cs>
### DisplayController
> supports: IR controlled displays, any two way display driver that derives from PepperDash.Essentials.Core.DisplayBase
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DisplayControllerJoinMap.cs>
### DmChasisController
> supports: All DM-MD-8x8/16x16/32x32 chassis, with or w/o DM-CPU3 Card
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmChassisControllerJoinMap.cs>
### DmRmcController
> supports: All DM-RMC devices
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmRmcControllerJoinMap.cs>
### DmTxController
> supports: All Dm-Tx devices
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmTxControllerJoinMap.cs>
### DmpsAudioOutputController
> supports: Program, Aux1, Aux2 outputs of all DMPS3 Control Systems
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs>
### DmpsRoutingController
> supports: Av routing for all DMPS3 Control Systems
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs>
### GenericRelayController
> supports: Any relay port on a Crestron Control System or Dm Endpoint
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs>
### GenericLightingJoinMap
> supports: Devices derived from PepperDash.Essentials.Core.Lighting.LightingBase
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/GenericLightingJoinMap.cs>
### GlsOccupancySensorBase
> supports: Any Crestron GLS-Type Occupancy sensor - single/dual type
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs>
### HdMdxxxCEController
> supports: HD-MD-400-C-E, HD-MD-300-C-E, HD-MD-200-C-E, HD-MD-200-C-1G-E-B/W
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs>
### IBasicCommunication
> supports: Any COM Port on a Control System or Dm Endpoint device, TCP Client, SSH Client, or UDP Server
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs>
### IDigitalInput
> supports: Any Digital Input on a Control System, or DM Endpoint device
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/IDigitalInputJoinMap.cs>
### SystemMonitorController
> supports: Exposing the system monitor slot for any Control System
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/SystemMonitorJoinMap.cs>
## Example SIMPL Windows Program
We've provided an [example program](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for SIMPL Windows that works with the provided example Essentials configuration file [SIMPLBridgeExample_configurationFile.json](https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json). Load Essentials and the example SIMPL program to two slots on the same processor and you can get a better idea of how to take advantage of SIMPL Windows bridging.
Next: [Essentials architecture](~/docs/Arch-summary.md)

View File

@@ -0,0 +1,411 @@
# Use with SIMPL Windows
***
* [YouTube Video - SIMPL Windows in PepperDash Essentials](https://youtu.be/P2jNzsfpgJE)
***
Essentials allows for devices defined within the SIMPL# Pro application to be bridged to a SIMPL Windows application over Ethernet Intersystem Communication (EISC). This allows a SIMPL Windows program to take advantage of some of the features of the SIMPL# Pro environment, without requiring the entire application to be written in C#.
Some of the main advantages are:
1. The ability to instantiate devices from configuration.
2. The ability to leverage C# concepts to handle data intensive tasks (Serialization/Deserialization of JSON/XML, cyrptography, etc.).
3. The ability to reuse the same compiled SIMPL Windows program (regardless of target processor type) by offloading all the variables that may be room or hardware specific to Essentials.
4. The ability to handle multiple communciation types generically without changing the SIMPL Program (TCP/UDP/SSH/HTTP/HTTPS/CEC, etc.)
5. Much faster development cycle
6. Reduced processor overhead
7. Ability to easily share devices defined in Essentials between multiple other programs
## Implementation
Bridges are devices that are defined within the devices array in the config file. They are unique devices with a specialized purpose: to act as a bridge between Essentials Devices and applications programmed traditionally in SIMPL Windows. This is accomplished by instantiating a Three Series Intersystem Communication symbol within the bridge device, and linking its Boolean/Ushort/String inputs and outputs to actions on one or multiple Essentials device(s). The definition for which joins map to which actions is defined within the device to be bridged to in a class that derives from JoinMapBase.
Let's consider the following Essentials Configuration:
```JSON
{
"template": {
"roomInfo": [
{}
],
"devices": [
{
"key": "processor",
"uid": 1,
"type": "pro3",
"name": "PRO3 w/o cards",
"group": "processor",
"supportedConfigModes": [
"essentials"
],
"supportedSystemTypes": [
"hudType",
"presType",
"vtcType",
"custom"
],
"supportsCompliance": true,
"properties": {
"numberOfComPorts": 6,
"numberOfIrPorts": 8,
"numberOfRelays": 8,
"numberOfDIOPorts": 8
}
},
{
"key": "panasonicDisplay01",
"type": "PanasonicThefDisplay",
"name": "Main Display",
"group": "displays",
"uid": 2,
"properties": {
"id": "01",
"inputNumber": 1,
"outputNumber": 1,
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 9600,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 1,
"controlPortDevKey": "processor",
"method": "com"
}
}
},
{
"key": "vtcComPort",
"uid": 3,
"name": "VTC Coms",
"group": "comm",
"type": "genericComm",
"properties": {
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 38400,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 2,
"controlPortDevKey": "processor",
"method": "com"
}
}
},
{
"key": "deviceBridge",
"uid": 4,
"name": "BridgeToDevices",
"group": "api",
"type": "eiscApi",
"properties": {
"control": {
"tcpSshProperties": {
"address": "127.0.0.2",
"port": 0
},
"ipid": "03",
"method": "ipidTcp"
},
"devices": [
{
"deviceKey": "panasonicDisplay01",
"joinStart": 1
},
{
"deviceKey": "vtcComPort",
"joinStart": 51
}
]
}
}
]
}
}
```
We have four Essentials Devices configured:
1. Pro3 with a Key of "processor"
1. Panasonic Display with a Key of "panasonicDisplay01"
1. Com port with a Key of "vtcComPort"
1. Bridge with a Key of "deviceBridge"
We want to have access to the com port for VTC Control from SIMPL Windows and we want to control the display from SIMPL Windows. To accomplish this, we have created a bridge device and added the devices to be bridged to the "devices" array on the bridge. As you can see we define the device key and the join start, which will determine which joins we will use on the resulting EISC to interact with the devices. In the Bridge control properties we defined ipid 03, and we will need a corresponding Ethernet System Intercommunication in the SIMPL Windows program at ipid 03.
Now that our devices have been built, we can refer to the device join maps to see which joins correspond to which actions.
See below:
```cs
namespace PepperDash.Essentials.Core.Bridges
{
public class DisplayControllerJoinMap : JoinMapBaseAdvanced
{
[JoinName("Name")]
public JoinDataComplete Name = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Name", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial });
[JoinName("PowerOff")]
public JoinDataComplete PowerOff = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Power Off", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital });
[JoinName("PowerOn")]
public JoinDataComplete PowerOn = new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 },
new JoinMetadata() { Label = "Power On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital });
[JoinName("IsTwoWayDisplay")]
public JoinDataComplete IsTwoWayDisplay = new JoinDataComplete(new JoinData() { JoinNumber = 3, JoinSpan = 1 },
new JoinMetadata() { Label = "Is Two Way Display", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
[JoinName("VolumeUp")]
public JoinDataComplete VolumeUp = new JoinDataComplete(new JoinData() { JoinNumber = 5, JoinSpan = 1 },
new JoinMetadata() { Label = "Volume Up", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital });
[JoinName("VolumeLevel")]
public JoinDataComplete VolumeLevel = new JoinDataComplete(new JoinData() { JoinNumber = 5, JoinSpan = 1 },
new JoinMetadata() { Label = "Volume Level", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog });
[JoinName("VolumeDown")]
public JoinDataComplete VolumeDown = new JoinDataComplete(new JoinData() { JoinNumber = 6, JoinSpan = 1 },
new JoinMetadata() { Label = "Volume Down", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital });
[JoinName("VolumeMute")]
public JoinDataComplete VolumeMute = new JoinDataComplete(new JoinData() { JoinNumber = 7, JoinSpan = 1 },
new JoinMetadata() { Label = "Volume Mute", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital });
[JoinName("VolumeMuteOn")]
public JoinDataComplete VolumeMuteOn = new JoinDataComplete(new JoinData() { JoinNumber = 8, JoinSpan = 1 },
new JoinMetadata() { Label = "Volume Mute On", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital });
[JoinName("VolumeMuteOff")]
public JoinDataComplete VolumeMuteOff = new JoinDataComplete(new JoinData() { JoinNumber = 9, JoinSpan = 1 },
new JoinMetadata() { Label = "Volume Mute Off", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital });
[JoinName("InputSelectOffset")]
public JoinDataComplete InputSelectOffset = new JoinDataComplete(new JoinData() { JoinNumber = 11, JoinSpan = 10 },
new JoinMetadata() { Label = "Input Select", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Digital });
[JoinName("InputNamesOffset")]
public JoinDataComplete InputNamesOffset = new JoinDataComplete(new JoinData() { JoinNumber = 11, JoinSpan = 10 },
new JoinMetadata() { Label = "Input Names Offset", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial });
[JoinName("InputSelect")]
public JoinDataComplete InputSelect = new JoinDataComplete(new JoinData() { JoinNumber = 11, JoinSpan = 1 },
new JoinMetadata() { Label = "Input Select", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.Analog });
[JoinName("ButtonVisibilityOffset")]
public JoinDataComplete ButtonVisibilityOffset = new JoinDataComplete(new JoinData() { JoinNumber = 41, JoinSpan = 10 },
new JoinMetadata() { Label = "Button Visibility Offset", JoinCapabilities = eJoinCapabilities.ToFromSIMPL, JoinType = eJoinType.DigitalSerial });
[JoinName("IsOnline")]
public JoinDataComplete IsOnline = new JoinDataComplete(new JoinData() { JoinNumber = 50, JoinSpan = 1 },
new JoinMetadata() { Label = "Is Online", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
public DisplayControllerJoinMap(uint joinStart)
: base(joinStart, typeof(CameraControllerJoinMap))
{
}
}
}
```
We know that the Panasonic Display uses the DisplayControllerJoinMap class and can see the join numbers that will give us access to functionality in the Device.
IsOnline = 50
PowerOff = 1
PowerOn = 2
IsTwoWayDisplay = 3
VolumeUp = 5
VolumeDown = 6
VolumeMute = 7
```cs
namespace PepperDash.Essentials.Core.Bridges
{
public class IBasicCommunicationJoinMap : JoinMapBaseAdvanced
{
[JoinName("TextReceived")]
public JoinDataComplete TextReceived = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Text Received From Remote Device", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Serial });
[JoinName("SendText")]
public JoinDataComplete SendText = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Text Sent To Remote Device", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial });
[JoinName("SetPortConfig")]
public JoinDataComplete SetPortConfig = new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 },
new JoinMetadata() { Label = "Set Port Config", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Serial });
[JoinName("Connect")]
public JoinDataComplete Connect = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Connect", JoinCapabilities = eJoinCapabilities.FromSIMPL, JoinType = eJoinType.Digital });
[JoinName("Connected")]
public JoinDataComplete Connected = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Connected", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Digital });
[JoinName("Status")]
public JoinDataComplete Status = new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 },
new JoinMetadata() { Label = "Status", JoinCapabilities = eJoinCapabilities.ToSIMPL, JoinType = eJoinType.Analog });
public IBasicCommunicationJoinMap(uint joinStart)
: base(joinStart, typeof(IBasicCommunicationJoinMap))
{
}
}
}
```
Considering our Bridge config, we can see that the display controls will start at join 1, and the VTC Com port will start at join 51. The result is a single EISC that allows us to interact with our Essentials devices.
To control diplay power from SIMPL Windows, we would connect Digital Signals to joins 1 & 2 on the EISC to control Display Power On & Off.
To utilize the com port device, we would connect Serial Signals (VTC_TX$ and VTC_RX$) to join 51 on the EISC.
You can refer to our [SIMPL Windows Bridging Example](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for a more complex example.
Example device config: <https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json>
## Notes
1. It is important to realize that there are no safety checks (yet) when assigning joinStarts in bridge configurations. If you were to put two devices on a bridge with overlapping joins, the most recently bridged join would overwrite previously bridged joins. For now it is on the programmer to ensure there are no conflicting join maps.
2. There is _no_ limit to the amount of times a device may be bridged to. You may have the same device on multiple bridges across multiple applications without problem. That being said, we recommend using common sense. Accessing a single com port for VTC control via multiple bridges may not be wise...
3. A bridge need not only bridge between applications on the same processor. A bridge may bridge to an application on a completely separate processor; simply define the ip address in the Bridge control properties accordingly.
4. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/Feedback-Classes.md) for this.
5. When building plugins, we highly recommend reusing JoinMaps, as this will make code more easily interchangeable. For example; if you were to build a display plugin, we'd recommend you use/extend the existing `DisplayControllerJoinMap`. This way, you can swap plugins without needing any change on the SIMPL Windows side. This is extremely powerful when maintaining SIMPL Windows code bases for large deployments that may utilize differing equipment per room. If you can build a SIMPL Windows program that interacts with established join maps, you can swap out the device via config without any change needed to SIMPL Windows.
6. Related to item 5, you can use the same paradigm with respect to physical device communication. If you were to have a DSP device in some rooms communicating over RS232 and some via SSH, it would be trival to swap the device from a Com port to an SSH client in the Essentials Devicee Config and update the Bridge Config to brigde to the desired communication method. Again this would require no change on the SIMPL Windows side as long as you maintain the same join Start in the Bridge Device Configuration.
## Common Use Cases
1. There are 10 conference rooms that all operate the same, but have hardware differences that are impossible to account for in SIMPL Windows. For example, each room might have a DM-MD8X8 chassis, but the input and output cards aren't all in the same order, or they might be different models but function the same. You can use Essentials with a unique configuration file for each hardware configuration.
2. You have a floor of conference rooms that all share some centralized hardware like DSP, AV Routing and a shared CEN-GWEXER gateway with multiple GLS-OIR-CSM-EX-BATT occupancy sensors. All the shared hardware can be defined in the Essentials configuration and bridged over an EISC to each program that needs access. The same device can even be exposed to multiple programs over different EISCs.
3. You have a SIMPL program that works for many room types, but because some rooms have different models of processors than others (CP3/CP3N/AV3/PRO3/DMPS3 variants), you have to maintain several versions of the program, compiled for each processor model to maintain access to features like the System Monitor slot. You can use Essentials running in a slot on a processor to expose the System Monitor and many other features of the processor, regardless of model. Now you only need to maintain a single SIMPL program defined for your most complex processor application (ex. PRO3)
## Join Map Documentation
[Join Map Documentation](~/docs/JoinMaps.md)
## Device Type Join Maps
Please note that these joinmaps _may_ be using a deprecated implementation. The implementation is valid but nonetheless frowned upon for new features and plugins.
### AirMediaController
> supports: AM-200, AM-300
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/AirMediaControllerJoinMap.cs>
### AppleTvController
> supports: IR control of Apple TV
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/AppleTvJoinMap.cs>
### CameraControlBase
> supports: any camera that derives from CameraBase
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/CameraControllerJoinMap.cs>
### DisplayController
> supports: IR controlled displays, any two way display driver that derives from PepperDash.Essentials.Core.DisplayBase
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/DisplayControllerJoinMap.cs>
### DmChasisController
> supports: All DM-MD-8x8/16x16/32x32 chassis, with or w/o DM-CPU3 Card
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/DmChassisControllerJoinMap.cs>
### DmRmcController
> supports: All DM-RMC devices
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/DmRmcControllerJoinMap.cs>
### DmTxController
> supports: All Dm-Tx devices
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/DmTxControllerJoinMap.cs>
### DmpsAudioOutputController
> supports: Program, Aux1, Aux2 outputs of all DMPS3 Control Systems
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs>
### DmpsRoutingController
> supports: Av routing for all DMPS3 Control Systems
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs>
### GenericRelayController
> supports: Any relay port on a Crestron Control System or Dm Endpoint
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs>
### GenericLightingJoinMap
> supports: Devices derived from PepperDash.Essentials.Core.Lighting.LightingBase
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/GenericLightingJoinMap.cs>
### GlsOccupancySensorBase
> supports: Any Crestron GLS-Type Occupancy sensor - single/dual type
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs>
### HdMdxxxCEController
> supports: HD-MD-400-C-E, HD-MD-300-C-E, HD-MD-200-C-E, HD-MD-200-C-1G-E-B/W
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs>
### IBasicCommunication
> supports: Any COM Port on a Control System or Dm Endpoint device, TCP Client, SSH Client, or UDP Server
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs>
### IDigitalInput
> supports: Any Digital Input on a Control System, or DM Endpoint device
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/IDigitalInputJoinMap.cs>
### SystemMonitorController
> supports: Exposing the system monitor slot for any Control System
<https://github.com/PepperDash/Essentials/blob/main/essentials-framework/Essentials%20Core/PepperDashEssentialsBase/Bridges/JoinMaps/SystemMonitorJoinMap.cs>
## Example SIMPL Windows Program
We've provided an [example program](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for SIMPL Windows that works with the provided example Essentials configuration file [SIMPLBridgeExample_configurationFile.json](https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json). Load Essentials and the example SIMPL program to two slots on the same processor and you can get a better idea of how to take advantage of SIMPL Windows bridging.
Next: [Essentials architecture](~/docs/Arch-summary.md)

475
docs/docs/SIMPL-Bridging.md Normal file
View File

@@ -0,0 +1,475 @@
# SIMPL Windows Bridging
**Note : this entry is out of date - please see [SIMPL Windows Bridging - Updated](~/docs/SIMPL-Bridging-Updated.md)**
Essentials allows for devices defined within the SIMPL# Pro application to be bridged to a SIMPL Windows application over Ethernet Intersystem Communication (EISC). This allows a SIMPL Windows program to take advantage of some of the features of the SIMPL# Pro environment, without requiring the entire application to be written in C#.
Some of the main advantages are:
1. The ability to instantiate devices from configuration.
1. The ability to leverage C# concepts to handle data intensive tasks (Serialization/Deserialization of JSON/XML, cyrptography, etc.).
1. The ability to reuse the same compiled SIMPL Windows program (regardless of target processor type) by offloading all the variables that may be room or hardware specific to Essentials.
1. The ability to handle multiple communciation types generically without changing the SIMPL Program (TCP/UDP/SSH/HTTP/HTTPS/CEC, etc.)
1. Much faster development cycle
1. Reduced processor overhead
1. Ability to easily share devices defined in Essentials between multiple other programs
## Implementation
Bridges are devices that are defined within the devices array in the config file. They are unique devices with a specialized purpose; to act as a bridge between Essentials Devices and applications programmed traditionally in Simpl Windows. This is accomplished by instantiating a Three Series Intersystem Communication symbol within the bridge device, and linking its Boolean/Ushort/String inputs and outputs to actions on one or multiple Essentials device(s). The definition for which joins map to which actions is defined within the device to be bridged to in a class that derives from JoinMapBase.
Let's consider the following Essentials Configuration:
```JSON
{
"template": {
"roomInfo": [
{}
],
"devices": [
{
"key": "processor",
"uid": 1,
"type": "pro3",
"name": "PRO3 w/o cards",
"group": "processor",
"supportedConfigModes": [
"essentials"
],
"supportedSystemTypes": [
"hudType",
"presType",
"vtcType",
"custom"
],
"supportsCompliance": true,
"properties": {
"numberOfComPorts": 6,
"numberOfIrPorts": 8,
"numberOfRelays": 8,
"numberOfDIOPorts": 8
}
},
{
"key": "panasonicDisplay01",
"type": "PanasonicThefDisplay",
"name": "Main Display",
"group": "displays",
"uid": 2,
"properties": {
"id": "01",
"inputNumber": 1,
"outputNumber": 1,
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 9600,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 1,
"controlPortDevKey": "processor",
"method": "com"
}
}
},
{
"key": "vtcComPort",
"uid": 3,
"name": "VTC Coms",
"group": "comm",
"type": "genericComm",
"properties": {
"control": {
"comParams": {
"hardwareHandshake": "None",
"parity": "None",
"protocol": "RS232",
"baudRate": 38400,
"dataBits": 8,
"softwareHandshake": "None",
"stopBits": 1
},
"controlPortNumber": 2,
"controlPortDevKey": "processor",
"method": "com"
}
}
},
{
"key": "deviceBridge",
"uid": 4,
"name": "BridgeToDevices",
"group": "api",
"type": "eiscApi",
"properties": {
"control": {
"tcpSshProperties": {
"address": "127.0.0.2",
"port": 0
},
"ipid": "03",
"method": "ipidTcp"
},
"devices": [
{
"deviceKey": "panasonicDisplay01",
"joinStart": 1
},
{
"deviceKey": "vtcComPort",
"joinStart": 51
}
]
}
}
]
}
}
```
We have four Essentials Devices configured:
1. Pro3 with a Key of "processor"
1. Panasonic Display with a Key of "panasonicDisplay01"
1. Com port with a Key of "vtcComPort"
1. Bridge with a Key of "deviceBridge"
We want to have access to the com port for VTC Control from Simpl Windows and we want to control the display from Simpl Windows. To accomplish this, we have created a bridge device and added the devices to be bridged to the "devices" array on the bridge. As you can see we define the device key and the join start, which will determine which joins we will use on the resulting EISC to interact with the devices. In the Bridge control properties we defined ipid 03, and we will need a corresponding Ethernet System Intercommunication in the Simpl Windows program at ipid 03.
Now that our devices have been built, we can refer to the device join maps to see which joins correspond to which actions.
See below:
```cs
namespace PepperDash.Essentials.Bridges
{
public class DisplayControllerJoinMap : JoinMapBase
{
#region Digitals
/// <summary>
/// Turns the display off and reports power off feedback
/// </summary>
public uint PowerOff { get; set; }
/// <summary>
/// Turns the display on and repots power on feedback
/// </summary>
public uint PowerOn { get; set; }
/// <summary>
/// Indicates that the display device supports two way communication when high
/// </summary>
public uint IsTwoWayDisplay { get; set; }
/// <summary>
/// Increments the volume while high
/// </summary>
public uint VolumeUp { get; set; }
/// <summary>
/// Decrements teh volume while high
/// </summary>
public uint VolumeDown { get; set; }
/// <summary>
/// Toggles the mute state. Feedback is high when volume is muted
/// </summary>
public uint VolumeMute { get; set; }
/// <summary>
/// Range of digital joins to select inputs and report current input as feedback
/// </summary>
public uint InputSelectOffset { get; set; }
/// <summary>
/// Range of digital joins to report visibility for input buttons
/// </summary>
public uint ButtonVisibilityOffset { get; set; }
/// <summary>
/// High if the device is online
/// </summary>
public uint IsOnline { get; set; }
#endregion
#region Analogs
/// <summary>
/// Analog join to set the input and report current input as feedback
/// </summary>
public uint InputSelect { get; set; }
/// <summary>
/// Sets the volume level and reports the current level as feedback
/// </summary>
public uint VolumeLevel { get; set; }
#endregion
#region Serials
/// <summary>
/// Reports the name of the display as defined in config as feedback
/// </summary>
public uint Name { get; set; }
/// <summary>
/// Range of serial joins that reports the names of the inputs as feedback
/// </summary>
public uint InputNamesOffset { get; set; }
#endregion
public DisplayControllerJoinMap()
{
// Digital
IsOnline = 50;
PowerOff = 1;
PowerOn = 2;
IsTwoWayDisplay = 3;
VolumeUp = 5;
VolumeDown = 6;
VolumeMute = 7;
ButtonVisibilityOffset = 40;
InputSelectOffset = 10;
// Analog
InputSelect = 11;
VolumeLevel = 5;
// Serial
Name = 1;
InputNamesOffset = 10;
}
public override void OffsetJoinNumbers(uint joinStart)
{
var joinOffset = joinStart - 1;
IsOnline = IsOnline + joinOffset;
PowerOff = PowerOff + joinOffset;
PowerOn = PowerOn + joinOffset;
IsTwoWayDisplay = IsTwoWayDisplay + joinOffset;
ButtonVisibilityOffset = ButtonVisibilityOffset + joinOffset;
Name = Name + joinOffset;
InputNamesOffset = InputNamesOffset + joinOffset;
InputSelectOffset = InputSelectOffset + joinOffset;
InputSelect = InputSelect + joinOffset;
VolumeUp = VolumeUp + joinOffset;
VolumeDown = VolumeDown + joinOffset;
VolumeMute = VolumeMute + joinOffset;
VolumeLevel = VolumeLevel + joinOffset;
}
}
}
```
We know that the Panasonic Display uses the DisplayControllerJoinMap class and can see the join numbers that will give us access to functionality in the Device.
IsOnline = 50
PowerOff = 1
PowerOn = 2
IsTwoWayDisplay = 3
VolumeUp = 5
VolumeDown = 6
VolumeMute = 7
```cs
namespace PepperDash.Essentials.Bridges
{
public class IBasicCommunicationJoinMap : JoinMapBase
{
#region Digitals
/// <summary>
/// Set High to connect, Low to disconnect
/// </summary>
public uint Connect { get; set; }
/// <summary>
/// Reports Connected State (High = Connected)
/// </summary>
public uint Connected { get; set; }
#endregion
#region Analogs
/// <summary>
/// Reports the connections status value
/// </summary>
public uint Status { get; set; }
#endregion
#region Serials
/// <summary>
/// Data back from port
/// </summary>
public uint TextReceived { get; set; }
/// <summary>
/// Sends data to the port
/// </summary>
public uint SendText { get; set; }
/// <summary>
/// Takes a JSON serialized string that sets a COM port's parameters
/// </summary>
public uint SetPortConfig { get; set; }
#endregion
public IBasicCommunicationJoinMap()
{
TextReceived = 1;
SendText = 1;
SetPortConfig = 2;
Connect = 1;
Connected = 1;
Status = 1;
}
public override void OffsetJoinNumbers(uint joinStart)
{
var joinOffset = joinStart - 1;
TextReceived = TextReceived + joinOffset;
SendText = SendText + joinOffset;
SetPortConfig = SetPortConfig + joinOffset;
Connect = Connect + joinOffset;
Connected = Connected + joinOffset;
Status = Status + joinOffset;
}
}
}
```
TextReceived = 1
SendText = 1
SetPortConfig = 2
Connect = 1
Connected = 1
Status = 1
Considering our Bridge config, we can see that the display controls will start at join 1, and the VTC Com port will start at join 51. The result is a single EISC that allows us to interact with our Essentials devices.
To control diplay power from Simpl Windows, we would connect Digital Signals to joins 1 & 2 on the EISC to control Display Power On & Off.
To utilize the com port device, we would connect Serial Signals (VTC_TX$ and VTC_RX$) to join 51 on the EISC.
You can refer to our [Simpl Windows Bridging Example](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for a more complex example.
Example device config: <https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json>
## Notes
1. It is important to realize that there are no safety checks (yet) when assigning joinStarts in bridge configurations. If you were to put two devices on a bridge with overlapping joins, the most recently bridged join would overwrite previously bridged joins. For now it is on the programmer to ensure there are no conflicting join maps.
1. There is _no_ limit to the amount of times a device may be bridged to. You may have the same device on multiple bridges across multiple applications without problem. That being said, we recommend using common sense. Accessing a single com port for VTC control via multiple bridges may not be wise...
1. A bridge need not only bridge between applications on the same processor. A bridge may bridge to an application on a completely separate processor; simply define the ip address in the Bridge control properties accordingly.
1. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/Feedback-Classes.md) for this.
1. When building plugins, we highly recommend reusing JoinMaps, as this will make code more easily interchangeable. For example; if you were to build a display plugin, we'd recommend you use/extend the existing DisplayControllerJoinMap. This way, you can swap plugins without needing any change on the Simpl Windows side. This is extremely powerful when maintaining Simpl Windows code bases for large deployments that may utilize differing equipment per room. If you can build a Simpl Windows program that interacts with established join maps, you can swap out the device via config without any change needed to Simpl Windows.
1. Related to item 5, you can use the same paradigm with respect to physical device communication. If you were to have a DSP device in some rooms communicating over RS232 and some via SSH, it would be trival to swap the device from a Com port to an SSH client in the Essentials Devicee Config and update the Bridge Config to brigde to the desired communication method. Again this would require no change on the Simpl Windows side as long as you maintain the same join Start in the Bridge Device Configuration.
## Common Use Cases
1. There are 10 conference rooms that all operate the same, but have hardware differences that are impossible to account for in SIMPL Windows. For example, each room might have a DM-MD8X8 chassis, but the input and output cards aren't all in the same order, or they might be different models but function the same. You can use Essentials with a unique configuration file for each hardware configuration.
1. You have a floor of conference rooms that all share some centralized hardware like DSP, AV Routing and a shared CEN-GWEXER gateway with multiple GLS-OIR-CSM-EX-BATT occupancy sensors. All the shared hardware can be defined in the Essentials configuration and bridged over an EISC to each program that needs access. The same device can even be exposed to multiple programs over different EISCs.
1. You have a SIMPL program that works for many room types, but because some rooms have different models of processors than others (CP3/CP3N/AV3/PRO3/DMPS3 variants), you have to maintain several versions of the program, compiled for each processor model to maintain access to features like the System Monitor slot. You can use Essentials running in a slot on a processor to expose the System Monitor and many other features of the processor, regardless of model. Now you only need to maintain a single SIMPL program defined for your most complex processor application (ex. PRO3)
## Device Type Join Maps
### AirMediaController
> supports: AM-200, AM-300
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/AirMediaControllerJoinMap.cs>
### AppleTvController
> supports: IR control of Apple TV
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/AppleTvJoinMap.cs>
### CameraControlBase
> supports: any camera that derives from CameraBase
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/CameraControllerJoinMap.cs>
### DisplayController
> supports: IR controlled displays, any two way display driver that derives from PepperDash.Essentials.Core.DisplayBase
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DisplayControllerJoinMap.cs>
### DmChasisController
> supports: All DM-MD-8x8/16x16/32x32 chassis, with or w/o DM-CPU3 Card
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmChassisControllerJoinMap.cs>
### DmRmcController
> supports: All DM-RMC devices
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmRmcControllerJoinMap.cs>
### DmTxController
> supports: All Dm-Tx devices
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmTxControllerJoinMap.cs>
### DmpsAudioOutputController
> supports: Program, Aux1, Aux2 outputs of all DMPS3 Control Systems
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmpsAudioOutputControllerJoinMap.cs>
### DmpsRoutingController
> supports: Av routing for all DMPS3 Control Systems
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/DmpsRoutingControllerJoinMap.cs>
### GenericRelayController
> supports: Any relay port on a Crestron Control System or Dm Endpoint
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/GenericRelayControllerJoinMap.cs>
### GenericLightingJoinMap
> supports: Devices derived from PepperDash.Essentials.Core.Lighting.LightingBase
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/GenericLightingJoinMap.cs>
### GlsOccupancySensorBase
> supports: Any Crestron GLS-Type Occupancy sensor - single/dual type
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/GlsOccupancySensorBaseJoinMap.cs>
### HdMdxxxCEController
> supports: HD-MD-400-C-E, HD-MD-300-C-E, HD-MD-200-C-E, HD-MD-200-C-1G-E-B/W
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/HdMdxxxCEControllerJoinMap.cs>
### IBasicCommunication
> supports: Any COM Port on a Control System or Dm Endpoint device, TCP Client, SSH Client, or UDP Server
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/IBasicCommunicationJoinMap.cs>
### IDigitalInput
> supports: Any Digital Input on a Control System, or DM Endpoint device
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/IDigitalInputJoinMap.cs>
### SystemMonitorController
> supports: Exposing the system monitor slot for any Control System
<https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Bridges/JoinMaps/SystemMonitorJoinMap.cs>
## Example SIMPL Windows Program
We've provided an [example program](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for SIMPL Windows that works with the provided example Essentials configuration file [SIMPLBridgeExample_configurationFile.json](https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json). Load Essentials and the example SIMPL program to two slots on the same processor and you can get a better idea of how to take advantage of SIMPL Windows bridging.
Next: [Essentials architecture](~/docs/Arch-summary.md)

View File

@@ -0,0 +1,19 @@
# Stand-alone Application
Essentials was originally designed as a standalone SIMPL# Pro control system application and has developed into a versatile, pluggable application. This page describes how to use our built-in room types for a completely self-contained "one-slot" control program.
By defining devices and a room in a JSON configuration file, Essentials can control an entire AV control system for a room. A file can be manually created in an IDE such as Visual Studio Code, or it can be generated by a friendly web-based configuration tool on [PepperDash Portal](http://pepperdash.com/products/), or some other configuration tool application, both requiring no knowledge of JSON. These tools step a user through building the necessary devices and setting to achieve a full working room.
## Plugins
### Devices
Essentials supports device plugins for communicating with various devices using both standard Crestron CIP communications, Cresnet, SSH, or other TCP/IP-based communication methods. See [the Plugins section](~/docs/Plugins.md) for more details
### Rooms
In order to tie together equipment into a unit that comprises what devices are used in a room, Essentials supports Room plugins. These plugins are similar to device plugins, in that they're loaded at runtime and allow for customization of business logic and behavior. They're loaded into a different section of the Device Manager, and can reference devices created by device plugins using the device's key.
See Also: [[Supported Devices|Supported-Devices]]
Next: [Simpl Windows bridging](~/docs/SIMPL-Bridging-Updated.md)

View File

@@ -0,0 +1,68 @@
# Essentials Framework Devices by Type
## Cameras
* VISCA protocol
* Cisco (via codec)
* Zoom (via Zoom Room)
## Disc Player
* Any IR disc player that implements standard RAD commands
## Displays
* Any IR display that implements standard RAD commands
* Samsung MDC protocol (commercial)
* NEC Professional series flat panel
* Avocor VTF
* Panasonic TH series flat panels
* Panasonic Projectors [(via plugin)](https://github.com/PepperDash/epi-display-panasonic-projectors)
* LG Commercial series [(via plugin)](https://github.com/PepperDash/epi-display-lg)
* Generic CEC control via HDMI [(via plugin)](https://github.com/PepperDash/epi-generic-cec-display)
* Crestron Certified Driver Display [(via plugin)](https://github.com/batourin/epi-display-ccd)
## Lighting/Shading
* Lutron Quantum
* Somfy Shades (relay control)
## Power Controllers
* Digital Logger
## Set Top Boxes
* Any IR set top box that implements standard RAD commands
## Streaming Players
* AppleTV (IR)
* Roku (IR)
## Video Codecs
* Cisco CE series (C/SX/RoomKit)
* Zoom Room
## DSPs / Audio Codecs
* BiAmp Tesira [(via plugin](https://github.com/PepperDash/epi-dsp-tesira)
## Crestron Devices
* AM-200/300 Airmedia
* All DM Chassis (8x8 * 128x128)
* All DM input/output cards
* All DMPS Processors
* All DM Transmitter models (with COM/IR/Relay/CEC port access)
* All DM Receiver models (with COM/IR/Relay/CEC port access)
* DGE-100
* DM-DGE-200-C
* DIN-8SW8
* CEN-IO-DIGIN-104
* CEN-RFGWEX/GWEXER
* GLS-ODT/OIR-C-CN Occupancy Sensors
* TSW-XXX series touchpanels
* XPanel for SmartGraphics
* Fusion Room and Assets

View File

@@ -0,0 +1 @@
# Getting Started

View File

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 398 KiB

View File

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 228 KiB

View File

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 337 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 648 KiB

After

Width:  |  Height:  |  Size: 648 KiB

View File

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

48
docs/docs/toc.yml Normal file
View File

@@ -0,0 +1,48 @@
- name: Get Started With Essentials
- href: ../index.md
- href: Get-started.md
- name: Usage
items:
- href: Standalone-Use.md
- href: SIMPL-Bridging-Updated.md
items:
- name: Join Maps
href: JoinMaps.md
- name: Bridging to Hardware Resources
href: Bridging-To-Hardware-Resources.md
items:
- name: GenericComm Bridging
href: GenericComm.md
- name: RelayOutput Bridging
href: RelayOutput.md
- name: Digital Input Bridging
href: DigitalInput.md
- name: IR Driver Bridging
href: IR-Driver-Bridging.md
- name: Technical documentation
items:
- href: Arch-summary.md
- name: Devices and DeviceManager
href: Arch-1.md
- name: Configurable lifecycle
href: Arch-lifecycle.md
- name: Activation phases
href: Arch-activate.md
- name: More
href: Arch-topics.md
- name: Plugins
href: Plugins.md
- name: Communication Basics
href: Communication-Basics.md
- name: Debugging
href: Debugging.md
- name: Feedback Classes
href: Feedback-Classes.md
- name: Connection Based Routing
href: Connection-Based-Routing.md
- name: Configuration Structure
href: ConfigurationStructure.md
- name: Supported Devices
href: Supported-Devices.md
- name: Glossary of Terms
href: Glossary-of-Terms.md

59
docs/index.md Normal file
View File

@@ -0,0 +1,59 @@
# Welcome to PepperDash Essentials!
PepperDash Essentials is an open-source framework for control systems, built on Crestron's Simpl# Pro framework. It can be configured as a standalone program capable of running a wide variety of system designs and can also be used to augment other Crestron programs.
Essentials is a collection of C# libraries that can be used in many ways. It is a 100% configuration-driven framework that can be extended to add different workflows and behaviors, either through the addition of new device-types and classes, or via a plug-in mechanism. The framework is a collection of things that are all related and interconnected, but in general do not have strong dependencies on each other.
---
## Get started
- [Download essentials build or clone repo](~/docs/Get-started.md)
- [How to get started](~/docs/Get-started.md)
- [YouTube Video Series Playlist](https://youtube.com/playlist?list=PLKOoNNwgPFZdV5wDEBDZxTHu1KROspaBu)
- [Discord Server](https://discord.gg/6Vh3ssDdPs)
Or use the links to the right to navigate our documentation.
---
## Benefits
- Runs on Crestron 3-Series, **4-Series** and VC-4 Control System platforms
- Reduced hardware overhead compared to S+ and Simpl solutions
- Quick development cycle
- Shared resources made easily available
- More flexibility with less code
- Configurable using simple JSON files
- Is awesome
---
## Comment
The Essentials wiki is clearly in-progress right now. Take a look at the links to the right. We are actively working on this documentation, so please be patient with us. If you have any comments on or suggestions for the documentation, please file an issue here, with as much detail as you can provide: <https://github.com/PepperDash/Essentials/issues>
Thanks!
---
## Collaboration
Essentials is an open-source project and we encourage collaboration on this community project. For features that may not be useful to the greater community, or for just-plain learning, we want to remind developers to try writing plugins for Essentials. More information can be found here: [Plugins](~/docs/Plugins.md)
### Open-source-collaborative workflow
The `main` branch always contain the latest stable version. The `development` branch is used for most development efforts.
[GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) will be used as the workflow for this collaborative project. To contribute, follow this process:
1. Fork this repository ([More Info](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/working-with-forks))
2. Create a branch using standard GitFlow branch prefixes (feature/hotfix) followed by a descriptive name.
- Example: `feature/add-awesomeness` or `hotfix/really-big-oops`
- When working on a new feature or bugfix, branch from the `development` branch. When working on a hotfix, branch from `main`.
3. Make commits as necessary (often is better). And use concise, descriptive language, leveraging issue notation and/or [Closing Keywords](https://help.github.com/articles/closing-issues-using-keywords) to ensure any issues addressed by your work are referenced accordingly.
4. When the scope of the work for your branch is complete, make sure to rebase your branch in case further progress has been made since the repo was forked
5. Create a Pull Request to pull your branch into the appropriate branch in the main repository.
6. Your Pull Request will be reviewed by our team and evaluated for inclusion into the main repository.
Next: [Get started](~/docs/Get-started.md)

Binary file not shown.

4
docs/toc.yml Normal file
View File

@@ -0,0 +1,4 @@
- name: Docs
href: docs/
- name: API
href: api/

View File

@@ -1,11 +1,11 @@
<Project>
<PropertyGroup>
<Version>2.0.0-local</Version>
<Version>2.19.4-local</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<Authors>PepperDash Technologies</Authors>
<Company>PepperDash Technologies</Company>
<Authors>PepperDash Technology</Authors>
<Company>PepperDash Technology</Company>
<Product>PepperDash Essentials</Product>
<Copyright>Copyright © 2023</Copyright>
<Copyright>Copyright © 2025</Copyright>
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>Crestron; 4series</PackageTags>
@@ -13,6 +13,8 @@
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>

View File

@@ -1,26 +1,70 @@
<Project>
<ItemGroup>
<None Include="$(PackageOutputPath)\$(AssemblyName)\*.cpz" Condition="$(ProjectType) == 'Program'">
<ItemGroup>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library'">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</None>
<None Include="$(PackageOutputPath)\$(AssemblyName)\*.cplz" Condition="$(ProjectType) == 'ProgramLibrary'">
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program'">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</None>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary'">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</None>
</ItemGroup>
<Target Name="Create CPLZ" AfterTargets="Build; AfterRebuild" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != ''">
<PropertyGroup Condition="$(ProjectType) == 'Library'">
<FileName>$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz</FileName>
</PropertyGroup>
<PropertyGroup Condition="$(ProjectType) == 'ProgramLibrary'">
<FileName>$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz</FileName>
</PropertyGroup>
<PropertyGroup Condition="$(ProjectType) == 'Program'">
<FileName>$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz</FileName>
</PropertyGroup>
<Target Name="DeleteCLZ" BeforeTargets="CoreBuild" Condition="$(ProjectType) == 'Library' And $(TargetDir) != ''">
<ItemGroup>
<OldCLZFiles Include="$(TargetDir)$(TargetName).*.$(TargetFramework).clz" />
</ItemGroup>
<Delete Files="@(OldCLZFiles)" Condition="@(OldCLZFiles) != ''">
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
</Delete>
<Message Text="Deleted old CLZ files: '@(DeletedList)'" Condition="@(DeletedList) != ''" />
</Target>
<Target Name="DeleteCPZ" BeforeTargets="CoreBuild" Condition="$(ProjectType) == 'Program' And $(TargetDir) != ''">
<ItemGroup>
<OldCPZFiles Include="$(TargetDir)$(TargetName).*.$(TargetFramework).cpz" />
</ItemGroup>
<Delete Files="@(OldCPZFiles)" Condition="@(OldCPZFiles) != ''">
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
</Delete>
<Message Text="Deleted old CPZ files: '@(DeletedList)'" Condition="@(DeletedList) != ''" />
</Target>
<Target Name="DeleteCPLZ" BeforeTargets="CoreBuild" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != ''">
<ItemGroup>
<OldCPLZFiles Include="$(TargetDir)$(TargetName).*.$(TargetFramework).cplz" />
</ItemGroup>
<Delete Files="@(OldCPLZFiles)" Condition="@(OldCPLZFiles) != ''">
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
</Delete>
<Message Text="Deleted old CPLZ files: '@(DeletedList)'" Condition="@(DeletedList) != ''" />
</Target>
<Target Name="CreateCPLZ" AfterTargets="Build" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != ''" DependsOnTargets="DeleteCPLZ">
<Message Text="Creating CPLZ $(TargetDir)"></Message>
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists($(PackageOutputPath))" />
<MakeDir Directories="$(PackageOutputPath)\$(AssemblyName)" Condition="!Exists('$(PackageOutputPath)\$(AssemblyName)')" />
<ZipDirectory SourceDirectory="$(TargetDir)" DestinationFile="$(PackageOutputPath)\$(AssemblyName)\$(TargetName).$(Version).$(TargetFramework).cplz" Overwrite="true"/>
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists($(PackageOutputPath))" />
<ZipDirectory SourceDirectory="$(TargetDir)" DestinationFile="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).cplz" Overwrite="true"/>
<Copy SourceFiles="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).cplz" DestinationFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" />
</Target>
<Target Name="Copy CLZ" AfterTargets="SimplSharpPostProcess" Condition="($(ProjectType) == 'Library')">
<Message Text="Copying CLZ"></Message>
<Move SourceFiles="$(TargetDir)\$(TargetName).clz" DestinationFiles="$(TargetDir)\$(TargetName).$(Version).$(TargetFramework).clz"/>
<Copy SourceFiles="$(TargetDir)\$(TargetName).$(Version).$(TargetFramework).clz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).clz"/>
</Target>
<Target Name="Copy CPZ NET6" AfterTargets="SimplSharpPostProcess" Condition="($(ProjectType) == 'Program' And ( '$(TargetFramework)' == 'net6.0' ) Or ( '$(TargetFramework)' == 'net8.0' ))">
<Target Name="Copy CPZ" AfterTargets="SimplSharpPostProcess" Condition="($(ProjectType) == 'Program' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' ))">
<Message Text="Copying CPZ"></Message>
<Move SourceFiles="$(TargetDir)$(TargetName).cpz" DestinationFiles="$(PackageOutputPath)\$(AssemblyName)\$(TargetName).$(Version).$(TargetFramework).cpz" />
</Target>
<Target Name="Copy CPZ NET47" AfterTargets="SimplSharpPostProcess47" Condition="($(ProjectType) == 'Program' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' ))">
<Message Text="Copying CPZ"></Message>
<Move SourceFiles="$(TargetDir)$(TargetName).cpz" DestinationFiles="$(PackageOutputPath)\$(AssemblyName)\$(TargetName).$(Version).$(TargetFramework).cpz" />
<Move SourceFiles="$(TargetDir)$(TargetName).cpz" DestinationFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" />
<Copy SourceFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).cpz" />
</Target>
</Project>

View File

@@ -0,0 +1,43 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace PepperDash.Core
{
/// <summary>
/// Helper class for formatting communication text and byte data for debugging purposes.
/// </summary>
public class ComTextHelper
{
/// <summary>
/// Gets escaped text for a byte array
/// </summary>
/// <param name="bytes"></param>
/// <returns>string with all bytes escaped</returns>
public static string GetEscapedText(byte[] bytes)
{
return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets escaped text for a string
/// </summary>
/// <param name="text"></param>
/// <returns>string with all bytes escaped</returns>
public static string GetEscapedText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets debug text for a string
/// </summary>
/// <param name="text"></param>
/// <returns>string with all non-printable characters escaped</returns>
public static string GetDebugText(string text)
{
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
}
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Core
{
/// <summary>
/// Defines the string event handler for line events on the gather
/// </summary>
/// <param name="text"></param>
public delegate void LineReceivedHandler(string text);
/// <summary>
/// Attaches to IBasicCommunication as a text gather
/// </summary>
public class CommunicationGather
{
/// <summary>
/// Event that fires when a line is received from the IBasicCommunication source.
/// The event merely contains the text, not an EventArgs type class.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> LineReceived;
/// <summary>
/// The communication port that this gathers on
/// </summary>
public ICommunicationReceiver Port { get; private set; }
/// <summary>
/// Default false. If true, the delimiter will be included in the line output
/// events
/// </summary>
public bool IncludeDelimiter { get; set; }
/// <summary>
/// For receive buffer
/// </summary>
StringBuilder ReceiveBuffer = new StringBuilder();
/// <summary>
/// Delimiter, like it says!
/// </summary>
char Delimiter;
string[] StringDelimiters;
/// <summary>
/// Constructor for using a char delimiter
/// </summary>
/// <param name="port"></param>
/// <param name="delimiter"></param>
public CommunicationGather(ICommunicationReceiver port, char delimiter)
{
Port = port;
Delimiter = delimiter;
port.TextReceived += new EventHandler<GenericCommMethodReceiveTextArgs>(Port_TextReceived);
}
/// <summary>
/// Constructor for using a single string delimiter
/// </summary>
/// <param name="port"></param>
/// <param name="delimiter"></param>
public CommunicationGather(ICommunicationReceiver port, string delimiter)
:this(port, new string[] { delimiter} )
{
}
/// <summary>
/// Constructor for using an array of string delimiters
/// </summary>
/// <param name="port"></param>
/// <param name="delimiters"></param>
public CommunicationGather(ICommunicationReceiver port, string[] delimiters)
{
Port = port;
StringDelimiters = delimiters;
port.TextReceived += Port_TextReceivedStringDelimiter;
}
/// <summary>
/// Stop method
/// </summary>
public void Stop()
{
Port.TextReceived -= Port_TextReceived;
Port.TextReceived -= Port_TextReceivedStringDelimiter;
}
/// <summary>
/// Handler for raw data coming from port
/// </summary>
void Port_TextReceived(object sender, GenericCommMethodReceiveTextArgs args)
{
var handler = LineReceived;
if (handler != null)
{
ReceiveBuffer.Append(args.Text);
var str = ReceiveBuffer.ToString();
var lines = str.Split(Delimiter);
if (lines.Length > 0)
{
for (int i = 0; i < lines.Length - 1; i++)
{
string strToSend = null;
if (IncludeDelimiter)
strToSend = lines[i] + Delimiter;
else
strToSend = lines[i];
handler(this, new GenericCommMethodReceiveTextArgs(strToSend));
}
ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
void Port_TextReceivedStringDelimiter(object sender, GenericCommMethodReceiveTextArgs args)
{
var handler = LineReceived;
if (handler != null)
{
// Receive buffer should either be empty or not contain the delimiter
// If the line does not have a delimiter, append the
ReceiveBuffer.Append(args.Text);
var str = ReceiveBuffer.ToString();
// Case: Receiving DEVICE get version\x0d\0x0a+OK "value":"1234"\x0d\x0a
// RX: DEV
// Split: (1) "DEV"
// RX: I
// Split: (1) "DEVI"
// RX: CE get version
// Split: (1) "DEVICE get version"
// RX: \x0d\x0a+OK "value":"1234"\x0d\x0a
// Split: (2) DEVICE get version, +OK "value":"1234"
// Iterate the delimiters and fire an event for any matching delimiter
foreach (var delimiter in StringDelimiters)
{
var lines = Regex.Split(str, delimiter);
if (lines.Length == 1)
continue;
for (int i = 0; i < lines.Length - 1; i++)
{
string strToSend = null;
if (IncludeDelimiter)
strToSend = lines[i] + delimiter;
else
strToSend = lines[i];
handler(this, new GenericCommMethodReceiveTextArgs(strToSend, delimiter));
}
ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]);
}
}
}
/// <summary>
/// Deconstructor. Disconnects from port TextReceived events.
/// </summary>
~CommunicationGather()
{
Stop();
}
}
}

View File

@@ -0,0 +1,138 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Core
{
/// <summary>
/// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable
/// </summary>
public class CommunicationStreamDebugging
{
/// <summary>
/// Device Key that this instance configures
/// </summary>
public string ParentDeviceKey { get; private set; }
/// <summary>
/// Timer to disable automatically if not manually disabled
/// </summary>
private CTimer DebugExpiryPeriod;
/// <summary>
/// Gets or sets the DebugSetting
/// </summary>
public eStreamDebuggingSetting DebugSetting { get; private set; }
private uint _DebugTimeoutInMs;
private const uint _DefaultDebugTimeoutMin = 30;
/// <summary>
/// Timeout in Minutes
/// </summary>
public uint DebugTimeoutMinutes
{
get
{
return _DebugTimeoutInMs / 60000;
}
}
/// <summary>
/// Gets or sets the RxStreamDebuggingIsEnabled
/// </summary>
public bool RxStreamDebuggingIsEnabled { get; private set; }
/// <summary>
/// Indicates that transmit stream debugging is enabled
/// </summary>
public bool TxStreamDebuggingIsEnabled { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="parentDeviceKey"></param>
public CommunicationStreamDebugging(string parentDeviceKey)
{
ParentDeviceKey = parentDeviceKey;
}
/// <summary>
/// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues
/// </summary>
/// <param name="setting"></param>
/// <summary>
/// SetDebuggingWithDefaultTimeout method
/// </summary>
public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting)
{
if (setting == eStreamDebuggingSetting.Off)
{
DisableDebugging();
return;
}
SetDebuggingWithSpecificTimeout(setting, _DefaultDebugTimeoutMin);
}
/// <summary>
/// Sets the debugging setting for the specified number of minutes
/// </summary>
/// <param name="setting"></param>
/// <param name="minutes"></param>
/// <summary>
/// SetDebuggingWithSpecificTimeout method
/// </summary>
public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes)
{
if (setting == eStreamDebuggingSetting.Off)
{
DisableDebugging();
return;
}
_DebugTimeoutInMs = minutes * 60000;
StopDebugTimer();
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs);
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
RxStreamDebuggingIsEnabled = true;
if ((setting & eStreamDebuggingSetting.Tx) == eStreamDebuggingSetting.Tx)
TxStreamDebuggingIsEnabled = true;
Debug.SetDeviceDebugSettings(ParentDeviceKey, setting);
}
/// <summary>
/// Disabled debugging
/// </summary>
private void DisableDebugging()
{
StopDebugTimer();
Debug.SetDeviceDebugSettings(ParentDeviceKey, eStreamDebuggingSetting.Off);
}
private void StopDebugTimer()
{
RxStreamDebuggingIsEnabled = false;
TxStreamDebuggingIsEnabled = false;
if (DebugExpiryPeriod == null)
{
return;
}
DebugExpiryPeriod.Stop();
DebugExpiryPeriod.Dispose();
DebugExpiryPeriod = null;
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace PepperDash.Core
{
/// <summary>
/// Represents a ControlPropertiesConfig
/// </summary>
public class ControlPropertiesConfig
{
/// <summary>
/// The method of control
/// </summary>
[JsonProperty("method")]
[JsonConverter(typeof(StringEnumConverter))]
public eControlMethod Method { get; set; }
/// <summary>
/// The key of the device that contains the control port
/// </summary>
[JsonProperty("controlPortDevKey", NullValueHandling = NullValueHandling.Ignore)]
public string ControlPortDevKey { get; set; }
/// <summary>
/// The number of the control port on the device specified by ControlPortDevKey
/// </summary>
[JsonProperty("controlPortNumber", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
public uint? ControlPortNumber { get; set; }
/// <summary>
/// The name of the control port on the device specified by ControlPortDevKey
/// </summary>
[JsonProperty("controlPortName", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
public string ControlPortName { get; set; }
/// <summary>
/// Properties for ethernet based communications
/// </summary>
[JsonProperty("tcpSshProperties", NullValueHandling = NullValueHandling.Ignore)]
public TcpSshPropertiesConfig TcpSshProperties { get; set; }
/// <summary>
/// The filename and path for the IR file
/// </summary>
[JsonProperty("irFile", NullValueHandling = NullValueHandling.Ignore)]
public string IrFile { get; set; }
/// <summary>
/// The IpId of a Crestron device
/// </summary>
[JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)]
public string IpId { get; set; }
/// <summary>
/// Readonly uint representation of the IpId
/// </summary>
[JsonIgnore]
public uint IpIdInt { get { return Convert.ToUInt32(IpId, 16); } }
/// <summary>
/// Char indicating end of line
/// </summary>
[JsonProperty("endOfLineChar", NullValueHandling = NullValueHandling.Ignore)]
public char EndOfLineChar { get; set; }
/// <summary>
/// Defaults to Environment.NewLine;
/// </summary>
[JsonProperty("endOfLineString", NullValueHandling = NullValueHandling.Ignore)]
public string EndOfLineString { get; set; }
/// <summary>
/// Indicates
/// </summary>
[JsonProperty("deviceReadyResponsePattern", NullValueHandling = NullValueHandling.Ignore)]
public string DeviceReadyResponsePattern { get; set; }
/// <summary>
/// Used when communcating to programs running in VC-4
/// </summary>
[JsonProperty("roomId", NullValueHandling = NullValueHandling.Ignore)]
public string RoomId { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ControlPropertiesConfig()
{
}
}
}

View File

@@ -0,0 +1,251 @@
/*PepperDash Technology Corp.
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
namespace PepperDash.Core
{
/// <summary>
/// Delegate for notifying of socket status changes
/// </summary>
/// <param name="client"></param>
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
/// <summary>
/// EventArgs class for socket status changes
/// </summary>
public class GenericSocketStatusChageEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the Client
/// </summary>
public ISocketStatus Client { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="client"></param>
public GenericSocketStatusChageEventArgs(ISocketStatus client)
{
Client = client;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericSocketStatusChageEventArgs() { }
}
/// <summary>
/// Delegate for notifying of TCP Server state changes
/// </summary>
/// <param name="state"></param>
public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
/// <summary>
/// EventArgs class for TCP Server state changes
/// </summary>
public class GenericTcpServerStateChangedEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the State
/// </summary>
public ServerState State { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="state"></param>
public GenericTcpServerStateChangedEventArgs(ServerState state)
{
State = state;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerStateChangedEventArgs() { }
}
/// <summary>
/// Delegate for TCP Server socket status changes
/// </summary>
/// <param name="socket"></param>
/// <param name="clientIndex"></param>
/// <param name="clientStatus"></param>
public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
/// <summary>
/// EventArgs for TCP server socket status changes
/// </summary>
public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public object Socket { get; private set; }
/// <summary>
///
/// </summary>
public uint ReceivedFromClientIndex { get; private set; }
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus { get; set; }
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
/// <param name="clientStatus"></param>
public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus)
{
Socket = socket;
ClientStatus = clientStatus;
}
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
/// <param name="clientIndex"></param>
/// <param name="clientStatus"></param>
public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus)
{
Socket = socket;
ReceivedFromClientIndex = clientIndex;
ClientStatus = clientStatus;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerSocketStatusChangeEventArgs() { }
}
/// <summary>
/// EventArgs for TCP server com method receive text
/// </summary>
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
{
/// <summary>
///
/// </summary>
public uint ReceivedFromClientIndex { get; private set; }
/// <summary>
///
/// </summary>
public ushort ReceivedFromClientIndexShort
{
get
{
return (ushort)ReceivedFromClientIndex;
}
}
/// <summary>
/// Gets or sets the Text
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text)
{
Text = text;
}
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="clientIndex"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex)
{
Text = text;
ReceivedFromClientIndex = clientIndex;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerCommMethodReceiveTextArgs() { }
}
/// <summary>
/// EventArgs for TCP server client ready for communication
/// </summary>
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public bool IsReady;
/// <summary>
///
/// </summary>
/// <param name="isReady"></param>
public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
{
IsReady = isReady;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
}
/// <summary>
/// EventArgs for UDP connected
/// </summary>
public class GenericUdpConnectedEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ushort UConnected;
/// <summary>
///
/// </summary>
public bool Connected;
/// <summary>
/// Constructor
/// </summary>
public GenericUdpConnectedEventArgs() { }
/// <summary>
///
/// </summary>
/// <param name="uconnected"></param>
public GenericUdpConnectedEventArgs(ushort uconnected)
{
UConnected = uconnected;
}
/// <summary>
///
/// </summary>
/// <param name="connected"></param>
public GenericUdpConnectedEventArgs(bool connected)
{
Connected = connected;
}
}
}

View File

@@ -0,0 +1,958 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
namespace PepperDash.Core
{
/// <summary>
/// A class to handle secure TCP/IP communications with a server
/// </summary>
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized Secure Tcp _client";
/// <summary>
/// Stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
#region GenericSecureTcpIpClient Events & Delegates
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/// Auto reconnect evant handler
/// </summary>
public event EventHandler AutoReconnectTriggered;
/// <summary>
/// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread.
/// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region GenricTcpIpClient properties
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get { return _hostname; }
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Gets or sets the Port
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Internal secure client
/// </summary>
private SecureTCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
// private Timer for auto reconnect
private CTimer RetryTimer;
#endregion
#region GenericSecureTcpIpClient properties
/// <summary>
/// Gets or sets the SharedKeyRequired
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// Gets or sets the SharedKey
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Gets or sets the IsReadyForCommunication
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Bool Heartbeat Enabled flag
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
/// S+ helper for Heartbeat Enabled
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
/// Heartbeat String
/// </summary>
public string HeartbeatString { get; set; }
//public int HeartbeatInterval = 50000;
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatInterval { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
// Used to force disconnection on a dead connect attempt
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
bool ProgramIsStopping;
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
/// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericSecureTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="key"></param>
/// <param name="clientConfigObject"></param>
public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
Initialize(clientConfigObject);
}
/// <summary>
/// Default constructor for S+
/// </summary>
public GenericSecureTcpIpClient()
: base(SplusKey)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary>
/// <param name="config"></param>
public void Initialize(TcpClientConfigObject config)
{
if (config == null)
{
Debug.Console(0, this, "Could not initialize client with key: {0}", Key);
return;
}
try
{
Hostname = config.Control.TcpSshProperties.Address;
Port = config.Control.TcpSshProperties.Port > 0 && config.Control.TcpSshProperties.Port <= 65535
? config.Control.TcpSshProperties.Port
: 80;
AutoReconnect = config.Control.TcpSshProperties.AutoReconnect;
AutoReconnectIntervalMs = config.Control.TcpSshProperties.AutoReconnectIntervalMs > 1000
? config.Control.TcpSshProperties.AutoReconnectIntervalMs
: 5000;
SharedKey = config.SharedKey;
SharedKeyRequired = config.SharedKeyRequired;
HeartbeatEnabled = config.HeartbeatRequired;
HeartbeatRequiredIntervalInSeconds = config.HeartbeatRequiredIntervalInSeconds > 0
? config.HeartbeatRequiredIntervalInSeconds
: (ushort)15;
HeartbeatString = string.IsNullOrEmpty(config.HeartbeatStringToMatch)
? "heartbeat"
: config.HeartbeatStringToMatch;
BufferSize = config.Control.TcpSshProperties.BufferSize > 2000
? config.Control.TcpSshProperties.BufferSize
: 2000;
ReceiveQueueSize = config.ReceiveQueueSize > 20
? config.ReceiveQueueSize
: 20;
MessageQueue = new CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
catch (Exception ex)
{
Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex);
}
}
#endregion
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection");
ProgramIsStopping = true;
Disconnect();
}
}
/// <summary>
/// Deactivate the client
/// </summary>
/// <returns></returns>
/// <summary>
/// Deactivate method
/// </summary>
public override bool Deactivate()
{
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
return;
}
try
{
IsTryingToConnect = true;
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
return;
}
// clean up previous client
if (_client != null)
{
Disconnect();
}
DisconnectCalledByUser = false;
_client = new SecureTCPClient(Hostname, Port, BufferSize);
_client.SocketStatusChange += Client_SocketStatusChange;
if (HeartbeatEnabled)
_client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5);
_client.AddressClientConnectedTo = Hostname;
_client.PortNumber = Port;
// SecureClient = c;
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
//if (ConnectionHasHungCallback != null)
//{
// ConnectionHasHungCallback();
//}
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
_client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
}
IsTryingToConnect = false;
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
}
else
{
//CLient connected and shared key is not required so just raise the ready for communication event. if Shared key
//required this is called by the shared key being negotiated
if (IsReadyForCommunication == false)
{
OnClientReadyForcommunications(true); // Key not required
}
}
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
this.LogVerbose("Disconnect Called");
DisconnectCalledByUser = true;
// stop trying reconnects, if we are
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (_client != null)
{
DisconnectClient();
this.LogDebug("Disconnected");
}
}
/// <summary>
/// DisconnectClient method
/// </summary>
public void DisconnectClient()
{
if (_client == null) return;
Debug.Console(1, this, "Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
// close up client. ALWAYS use this when disconnecting.
IsTryingToConnect = false;
Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
_client.SocketStatusChange -= Client_SocketStatusChange;
_client.Dispose();
_client = null;
if (ConnectFailTimer == null) return;
ConnectFailTimer.Stop();
ConnectFailTimer.Dispose();
ConnectFailTimer = null;
}
#region Methods
/// <summary>
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (_client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Disconnect();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(SecureTCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
var handler = TextReceivedQueueInvoke;
try
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
{
//var bytesHandler = BytesReceived;
//if (bytesHandler != null)
// bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
if (handler != null)
{
MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str));
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive);
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
}
}
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
{
client.DisconnectFromServer();
}
}
/// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically.
/// </summary>
void DequeueEvent()
{
try
{
while (true)
{
// Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather.
var message = MessageQueue.Dequeue();
var handler = TextReceivedQueueInvoke;
if (handler != null)
{
handler(this, message);
}
}
}
catch (Exception e)
{
this.LogException(e, "DequeueEvent error");
}
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
}
void HeartbeatStart()
{
if (HeartbeatEnabled)
{
this.LogVerbose("Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
}
}
void HeartbeatStop()
{
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
}
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(string received)
{
try
{
if (HeartbeatEnabled)
{
if (!string.IsNullOrEmpty(HeartbeatString))
{
var remainingText = received.Replace(HeartbeatString, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatString))
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
void HeartbeatAckTimerFail(object o)
{
try
{
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// SendText method
/// </summary>
public void SendText(string text)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
_client.SendDataAsync(bytes, bytes.Length, (c, n) =>
{
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
}
}
}
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes)
{
if (bytes.Length > 0)
{
try
{
if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
_client.SendData(bytes, bytes.Length);
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus)
{
if (ProgramIsStopping)
{
ProgramIsStopping = false;
return;
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
// The client could be null or disposed by this time...
if (_client == null || _client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
HeartbeatStop();
OnClientReadyForcommunications(false); // socket has gone low
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler == null) return;
handler(this, new GenericSocketStatusChageEventArgs(this));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (IsReadyForCommunication)
HeartbeatStart();
var handler = ClientReadyForCommunications;
if (handler == null) return;
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

View File

@@ -0,0 +1,909 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
namespace PepperDash.Core
{
/// <summary>
/// Generic secure TCP/IP client for server
/// </summary>
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Notifies of auto reconnect sequence triggered
/// </summary>
public event EventHandler AutoReconnectTriggered;
/// <summary>
/// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread.
/// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// Notifies of socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// This is something of a band-aid callback. If the client times out during the connection process, because the server
/// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help
/// keep a watch on the server and reset it if necessary.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Gets or sets the Port
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// Gets or sets the SharedKey
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Gets or sets the BufferSize
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
/// <summary>
///
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
///
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
///
/// </summary>
public string HeartbeatString { get; set; }
//public int HeartbeatInterval = 50000;
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatInterval { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
SecureTCPClient Client;
bool ProgramIsStopping;
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
/// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericSecureTcpIpClient_ForServer(string key, string address, int port, int bufferSize)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
}
/// <summary>
/// Constructor for S+
/// </summary>
public GenericSecureTcpIpClient_ForServer()
: base("Uninitialized Secure Tcp Client For Server")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="key"></param>
/// <param name="clientConfigObject"></param>
public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Initialize(clientConfigObject);
}
#endregion
#region Methods
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary>
/// <param name="clientConfigObject"></param>
public void Initialize(TcpClientConfigObject clientConfigObject)
{
try
{
if (clientConfigObject != null)
{
var TcpSshProperties = clientConfigObject.Control.TcpSshProperties;
Hostname = TcpSshProperties.Address;
AutoReconnect = TcpSshProperties.AutoReconnect;
AutoReconnectIntervalMs = TcpSshProperties.AutoReconnectIntervalMs > 1000 ?
TcpSshProperties.AutoReconnectIntervalMs : 5000;
SharedKey = clientConfigObject.SharedKey;
SharedKeyRequired = clientConfigObject.SharedKeyRequired;
HeartbeatEnabled = clientConfigObject.HeartbeatRequired;
HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ?
clientConfigObject.HeartbeatRequiredIntervalInSeconds : (ushort)15;
HeartbeatString = string.IsNullOrEmpty(clientConfigObject.HeartbeatStringToMatch) ? "heartbeat" : clientConfigObject.HeartbeatStringToMatch;
Port = TcpSshProperties.Port;
BufferSize = TcpSshProperties.BufferSize > 2000 ? TcpSshProperties.BufferSize : 2000;
ReceiveQueueSize = clientConfigObject.ReceiveQueueSize > 20 ? clientConfigObject.ReceiveQueueSize : 20;
MessageQueue = new CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
else
{
ErrorLog.Error("Could not initialize client with key: {0}", Key);
}
}
catch
{
ErrorLog.Error("Could not initialize client with key: {0}", Key);
}
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
ProgramIsStopping = true;
Disconnect();
}
}
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
return;
}
try
{
IsTryingToConnect = true;
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
return;
}
// clean up previous client
if (Client != null)
{
Cleanup();
}
DisconnectCalledByUser = false;
Client = new SecureTCPClient(Hostname, Port, BufferSize);
Client.SocketStatusChange += Client_SocketStatusChange;
if (HeartbeatEnabled)
Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5);
Client.AddressClientConnectedTo = Hostname;
Client.PortNumber = Port;
// SecureClient = c;
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
//if (ConnectionHasHungCallback != null)
//{
// ConnectionHasHungCallback();
//}
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
}
IsTryingToConnect = false;
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
}
else
{
//CLient connected and shared key is not required so just raise the ready for communication event. if Shared key
//required this is called by the shared key being negotiated
if (IsReadyForCommunication == false)
{
OnClientReadyForcommunications(true); // Key not required
}
}
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
this.LogVerbose("Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
void Cleanup()
{
IsTryingToConnect = false;
if (Client != null)
{
//SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose();
Client = null;
}
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
ConnectFailTimer.Dispose();
ConnectFailTimer = null;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Cleanup();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if(AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(SecureTCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
var handler = TextReceivedQueueInvoke;
try
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
{
//var bytesHandler = BytesReceived;
//if (bytesHandler != null)
// bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str));
if (handler != null)
{
MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str));
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive);
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
}
}
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
{
client.DisconnectFromServer();
}
}
/// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically.
/// </summary>
void DequeueEvent()
{
try
{
while (true)
{
// Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather.
var message = MessageQueue.Dequeue();
var handler = TextReceivedQueueInvoke;
if (handler != null)
{
handler(this, message);
}
}
}
catch (Exception e)
{
this.LogException(e, "DequeueEvent error");
}
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
}
void HeartbeatStart()
{
if (HeartbeatEnabled)
{
Debug.Console(2, this, "Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
}
}
void HeartbeatStop()
{
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
}
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(string received)
{
try
{
if (HeartbeatEnabled)
{
if (!string.IsNullOrEmpty(HeartbeatString))
{
var remainingText = received.Replace(HeartbeatString, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatString))
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
void HeartbeatAckTimerFail(object o)
{
try
{
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// SendText method
/// </summary>
public void SendText(string text)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Client.SendDataAsync(bytes, bytes.Length, (c, n) =>
{
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
}
}
}
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes)
{
if (bytes.Length > 0)
{
try
{
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
Client.SendData(bytes, bytes.Length);
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus)
{
if (ProgramIsStopping)
{
ProgramIsStopping = false;
return;
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
// The client could be null or disposed by this time...
if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
HeartbeatStop();
OnClientReadyForcommunications(false); // socket has gone low
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,683 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Org.BouncyCastle.Utilities;
using PepperDash.Core.Logging;
using Renci.SshNet;
using Renci.SshNet.Common;
namespace PepperDash.Core
{
/// <summary>
/// SSH Client
/// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SPlusKey = "Uninitialized SshClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Event that fires when data is received. Delivers args with byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Event that fires when data is received. Delivered as text.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Event when the connection status changes.
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/// Gets or sets the Hostname
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets or sets the Username
/// </summary>
public string Username { get; set; }
/// <summary>
/// Gets or sets the Password
/// </summary>
public string Password { get; set; }
/// <summary>
/// True when the server is connected - when status == 2.
/// </summary>
public bool IsConnected
{
// returns false if no client or not connected
get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Socket status change event
/// </summary>
public SocketStatus ClientStatus
{
get { lock (_stateLock) { return _ClientStatus; } }
private set
{
bool shouldFireEvent = false;
lock (_stateLock)
{
if (_ClientStatus != value)
{
_ClientStatus = value;
shouldFireEvent = true;
}
}
// Fire event outside lock to avoid deadlock
if (shouldFireEvent)
OnConnectionChange();
}
}
private SocketStatus _ClientStatus;
private bool _ConnectEnabled;
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected with be true when this == 2.
/// </summary>
public ushort UStatus
{
get { lock (_stateLock) { return (ushort)_ClientStatus; } }
}
/// <summary>
/// Determines whether client will attempt reconnection on failure. Default is true
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Will be set and unset by connect and disconnect only
/// </summary>
public bool ConnectEnabled
{
get { lock (_stateLock) { return _ConnectEnabled; } }
private set { lock (_stateLock) { _ConnectEnabled = value; } }
}
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Gets or sets the AutoReconnectIntervalMs
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
private SshClient client;
private ShellStream shellStream;
private readonly Timer reconnectTimer;
//Lock object to prevent simulatneous connect/disconnect operations
//private CCriticalSection connectLock = new CCriticalSection();
private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1);
// Thread-safety lock for state changes
private readonly object _stateLock = new object();
private bool disconnectLogged = false;
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
public bool DisableEcho { get; set; }
/// <summary>
/// Typical constructor.
/// </summary>
public GenericSshClient(string key, string hostname, int port, string username, string password) :
base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Key = key;
Hostname = hostname;
Port = port;
Username = username;
Password = password;
AutoReconnectIntervalMs = 5000;
reconnectTimer = new Timer(o =>
{
if (ConnectEnabled) // Now thread-safe property access
{
Connect();
}
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
/// <summary>
/// S+ Constructor - Must set all properties before calling Connect
/// </summary>
public GenericSshClient()
: base(SPlusKey)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
reconnectTimer = new Timer(o =>
{
if (ConnectEnabled) // Now thread-safe property access
{
Connect();
}
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (client != null)
{
this.LogDebug("Program stopping. Closing connection");
Disconnect();
}
}
}
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
// Don't go unless everything is here
if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535
|| Username == null || Password == null)
{
this.LogError("Connect failed. Check hostname, port, username and password are set or not null");
return;
}
ConnectEnabled = true;
try
{
connectLock.Wait();
if (IsConnected)
{
this.LogDebug("Connection already connected. Exiting Connect");
}
else
{
this.LogDebug("Attempting connect");
// Cancel reconnect if running.
StopReconnectTimer();
// Cleanup the old client if it already exists
if (client != null)
{
this.LogDebug("Cleaning up disconnected client");
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
// This handles both password and keyboard-interactive (like on OS-X, 'nixes)
KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username);
kauth.AuthenticationPrompt += new EventHandler<AuthenticationPromptEventArgs>(kauth_AuthenticationPrompt);
PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password);
this.LogDebug("Creating new SshClient");
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
client = new SshClient(connectionInfo);
client.ErrorOccurred += Client_ErrorOccurred;
//Attempt to connect
ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
try
{
client.Connect();
var modes = new Dictionary<TerminalModes, uint>();
if (DisableEcho)
{
modes.Add(TerminalModes.ECHO, 0);
}
shellStream = client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes);
if (shellStream.DataAvailable)
{
// empty the buffer if there is data
shellStream.Read();
}
shellStream.DataReceived += Stream_DataReceived;
this.LogInformation("Connected");
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
disconnectLogged = false;
}
catch (SshConnectionException e)
{
var ie = e.InnerException; // The details are inside!!
if (ie is SocketException)
{
this.LogError("CONNECTION failure: Cannot reach host");
this.LogVerbose(ie, "Exception details: ");
}
if (ie is System.Net.Sockets.SocketException socketException)
{
this.LogError("Connection failure: Cannot reach {host} on {port}",
Hostname, Port);
this.LogVerbose(socketException, "SocketException details: ");
}
if (ie is SshAuthenticationException)
{
this.LogError("Authentication failure for username {userName}", Username);
this.LogVerbose(ie, "AuthenticationException details: ");
}
else
{
this.LogError("Error on connect: {error}", ie.Message);
this.LogVerbose(ie, "Exception details: ");
}
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
}
}
catch (SshOperationTimeoutException ex)
{
this.LogWarning("Connection attempt timed out: {message}", ex.Message);
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
}
}
catch (Exception e)
{
this.LogError("Unhandled exception on connect: {error}", e.Message);
this.LogVerbose(e, "Exception details: ");
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
}
}
}
}
finally
{
connectLock.Release();
}
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
ConnectEnabled = false;
// Stop trying reconnects, if we are
StopReconnectTimer();
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
/// <summary>
/// Kills the stream, cleans up the client and sets it to null
/// </summary>
private void KillClient(SocketStatus status)
{
KillStream();
try
{
if (client != null)
{
client.ErrorOccurred -= Client_ErrorOccurred;
client.Disconnect();
client.Dispose();
client = null;
ClientStatus = status;
this.LogDebug("Disconnected");
}
}
catch (Exception ex)
{
this.LogException(ex, "Exception in Kill Client");
}
}
/// <summary>
/// Kills the stream
/// </summary>
private void KillStream()
{
try
{
if (shellStream != null)
{
shellStream.DataReceived -= Stream_DataReceived;
shellStream.Close();
shellStream.Dispose();
shellStream = null;
this.LogDebug("Disconnected stream");
}
}
catch (Exception ex)
{
this.LogException(ex, "Exception in Kill Stream:{0}");
}
}
/// <summary>
/// Handles the keyboard interactive authentication, should it be required.
/// </summary>
private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{
foreach (AuthenticationPrompt prompt in e.Prompts)
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
prompt.Response = Password;
}
/// <summary>
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
/// </summary>
private void Stream_DataReceived(object sender, ShellDataEventArgs e)
{
if (((ShellStream)sender).Length <= 0L)
{
return;
}
var response = ((ShellStream)sender).Read();
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
var bytes = Encoding.UTF8.GetBytes(response);
this.PrintReceivedBytes(bytes);
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
this.PrintReceivedText(response);
textHandler(this, new GenericCommMethodReceiveTextArgs(response));
}
}
/// <summary>
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
/// event
/// </summary>
private void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{
CrestronInvoke.BeginInvoke(o =>
{
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
this.LogError("Disconnected by remote");
else
this.LogException(e.Exception, "Unhandled SSH client error");
try
{
connectLock.Wait();
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
}
finally
{
connectLock.Release();
}
if (AutoReconnect && ConnectEnabled)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
}
});
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
private void OnConnectionChange()
{
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
}
#region IBasicCommunication Members
/// <summary>
/// Sends text to the server
/// </summary>
/// <param name="text">The text to send</param>
public void SendText(string text)
{
try
{
if (client != null && shellStream != null && IsConnected)
{
this.PrintSentText(text);
shellStream.Write(text);
shellStream.Flush();
}
else
{
this.LogDebug("Client is null or disconnected. Cannot Send Text");
}
}
catch (ObjectDisposedException)
{
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
StartReconnectTimer();
}
catch (Exception ex)
{
this.LogException(ex, "Exception sending text: '{message}'", text);
}
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes">The bytes to send</param>
public void SendBytes(byte[] bytes)
{
try
{
if (client != null && shellStream != null && IsConnected)
{
this.PrintSentBytes(bytes);
shellStream.Write(bytes, 0, bytes.Length);
shellStream.Flush();
}
else
{
this.LogDebug("Client is null or disconnected. Cannot Send Bytes");
}
}
catch (ObjectDisposedException ex)
{
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
StartReconnectTimer();
}
catch (Exception ex)
{
this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes));
}
}
#endregion
/// <summary>
/// Safely starts the reconnect timer with exception handling
/// </summary>
private void StartReconnectTimer()
{
try
{
reconnectTimer?.Change(AutoReconnectIntervalMs, System.Threading.Timeout.Infinite);
}
catch (ObjectDisposedException)
{
// Timer was disposed, ignore
this.LogDebug("Attempted to start timer but it was already disposed");
}
}
/// <summary>
/// Safely stops the reconnect timer with exception handling
/// </summary>
private void StopReconnectTimer()
{
try
{
reconnectTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
catch (ObjectDisposedException)
{
// Timer was disposed, ignore
this.LogDebug("Attempted to stop timer but it was already disposed");
}
}
/// <summary>
/// Deactivate method - properly dispose of resources
/// </summary>
public override bool Deactivate()
{
try
{
this.LogDebug("Deactivating SSH client - disposing resources");
// Stop trying reconnects
ConnectEnabled = false;
StopReconnectTimer();
// Disconnect and cleanup client
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
// Dispose timer
try
{
reconnectTimer?.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed, ignore
}
// Dispose semaphore
try
{
connectLock?.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed, ignore
}
return base.Deactivate();
}
catch (Exception ex)
{
this.LogException(ex, "Error during SSH client deactivation");
return false;
}
}
}
//*****************************************************************************************************
//*****************************************************************************************************
/// <summary>
/// Represents a SshConnectionChangeEventArgs
/// </summary>
public class SshConnectionChangeEventArgs : EventArgs
{
/// <summary>
/// Connection State
/// </summary>
public bool IsConnected { get; private set; }
/// <summary>
/// Gets or sets the UIsConnected
/// </summary>
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
/// <summary>
/// Gets or sets the Client
/// </summary>
public GenericSshClient Client { get; private set; }
/// <summary>
/// Gets or sets the Status
/// </summary>
public ushort Status { get { return Client.UStatus; } }
/// <summary>
/// S+ Constructor
/// </summary>
public SshConnectionChangeEventArgs() { }
/// <summary>
/// EventArgs class
/// </summary>
/// <param name="isConnected">Connection State</param>
/// <param name="client">The Client</param>
public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
{
IsConnected = isConnected;
Client = client;
}
}
}

View File

@@ -0,0 +1,569 @@
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// A class to handle basic TCP/IP communications with a server
/// </summary>
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized TcpIpClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get
{
return _hostname;
}
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Gets or sets the Port
/// </summary>
public int Port { get; set; }
/// <summary>
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The actual client class
/// </summary>
private TCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Ushort representation of client status
/// </summary>
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Set only when the disconnect method is called
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
//Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection();
// private Timer for auto reconnect
private CTimer RetryTimer;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">unique string to differentiate between instances</param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
Hostname = address;
Port = port;
BufferSize = bufferSize;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
public GenericTcpIpClient(string key)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
/// <summary>
/// Default constructor for S+
/// </summary>
public GenericTcpIpClient()
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Debug.Console(1, this, "Program stopping. Closing connection");
Deactivate();
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
/// <summary>
/// Deactivate method
/// </summary>
public override bool Deactivate()
{
RetryTimer.Stop();
RetryTimer.Dispose();
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key);
return;
}
}
try
{
connectLock.Enter();
if (IsConnected)
{
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
}
else
{
//Stop retry timer if running
RetryTimer.Stop();
_client = new TCPClient(Hostname, Port, BufferSize);
_client.SocketStatusChange -= Client_SocketStatusChange;
_client.SocketStatusChange += Client_SocketStatusChange;
DisconnectCalledByUser = false;
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
private void Reconnect()
{
if (_client == null)
{
return;
}
try
{
connectLock.Enter();
if (IsConnected || DisconnectCalledByUser == true)
{
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()");
}
else
{
Debug.Console(1, this, "Attempting reconnect now");
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
try
{
connectLock.Enter();
DisconnectCalledByUser = true;
// Stop trying reconnects, if we are
RetryTimer.Stop();
DisconnectClient();
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// DisconnectClient method
/// </summary>
public void DisconnectClient()
{
if (_client != null)
{
Debug.Console(1, this, "Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
}
}
/// <summary>
/// Callback method for connection attempt
/// </summary>
/// <param name="c"></param>
void ConnectToServerCallback(TCPClient c)
{
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus);
WaitAndTryReconnect();
}
else
{
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
}
}
/// <summary>
/// Disconnects, waits and attemtps to connect again
/// </summary>
void WaitAndTryReconnect()
{
CrestronInvoke.BeginInvoke(o =>
{
try
{
connectLock.Enter();
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
{
DisconnectClient();
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Reset(AutoReconnectIntervalMs);
}
}
finally
{
connectLock.Leave();
}
});
}
/// <summary>
/// Recieves incoming data
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes)
{
if (client != null)
{
if (numBytes > 0)
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
this.PrintReceivedBytes(bytes);
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
this.PrintReceivedText(str);
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
client.ReceiveDataAsync(Receive);
}
}
/// <summary>
/// SendText method
/// </summary>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array
this.PrintSentText(text);
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
/// <summary>
/// SendEscapedText method
/// </summary>
public void SendEscapedText(string text)
{
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
{
var hex = s.Groups[1].Value;
return ((char)Convert.ToByte(hex, 16)).ToString();
});
SendText(unescapedText);
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes"></param>
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes)
{
this.PrintSentBytes(bytes);
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
/// <summary>
/// Socket Status Change Handler
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
WaitAndTryReconnect();
}
else
{
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive);
}
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
}
/// <summary>
/// Represents a TcpSshPropertiesConfig
/// </summary>
public class TcpSshPropertiesConfig
{
/// <summary>
/// Address to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
/// Port to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Username credential
/// </summary>
public string Username { get; set; }
/// <summary>
/// Gets or sets the Password
/// </summary>
public string Password { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Gets or sets the AutoReconnect
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Gets or sets the AutoReconnectIntervalMs
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
[JsonProperty("disableSshEcho")]
public bool DisableSshEcho { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public TcpSshPropertiesConfig()
{
BufferSize = 32768;
AutoReconnect = true;
AutoReconnectIntervalMs = 5000;
Username = "";
Password = "";
DisableSshEcho = false;
}
}
}

View File

@@ -0,0 +1,775 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
namespace PepperDash.Core
{
/// <summary>
/// Generic TCP/IP client for server
/// </summary>
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Notifies of socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// This is something of a band-aid callback. If the client times out during the connection process, because the server
/// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help
/// keep a watch on the server and reset it if necessary.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Gets or sets the Port
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// Gets or sets the SharedKey
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Gets or sets the BufferSize
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
/// <summary>
///
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
///
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
///
/// </summary>
public string HeartbeatString = "heartbeat";
/// <summary>
///
/// </summary>
public int HeartbeatInterval = 50000;
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
TCPClient Client;
bool ProgramIsStopping;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericTcpIpClient_ForServer(string key, string address, int port, int bufferSize)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
}
/// <summary>
/// Constructor for S+
/// </summary>
public GenericTcpIpClient_ForServer()
: base("Uninitialized DynamicTcpClient")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
#endregion
#region Methods
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
ProgramIsStopping = true;
Disconnect();
}
}
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
return;
}
try
{
IsTryingToConnect = true;
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
return;
}
// clean up previous client
if (Client != null)
{
Cleanup();
}
DisconnectCalledByUser = false;
Client = new TCPClient(Hostname, Port, BufferSize);
Client.SocketStatusChange += Client_SocketStatusChange;
if(HeartbeatEnabled)
Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5);
Client.AddressClientConnectedTo = Hostname;
Client.PortNumber = Port;
// SecureClient = c;
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
//if (ConnectionHasHungCallback != null)
//{
// ConnectionHasHungCallback();
//}
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
}
IsTryingToConnect = false;
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
}
else
{
//CLient connected and shared key is not required so just raise the ready for communication event. if Shared key
//required this is called by the shared key being negotiated
if (IsReadyForCommunication == false)
{
OnClientReadyForcommunications(true); // Key not required
}
}
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
this.LogVerbose("Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
void Cleanup()
{
IsTryingToConnect = false;
if (Client != null)
{
//SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose();
Client = null;
}
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
ConnectFailTimer.Dispose();
ConnectFailTimer = null;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Cleanup();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
try
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
{
//var bytesHandler = BytesReceived;
//if (bytesHandler != null)
// bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str));
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
}
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive);
}
void HeartbeatStart()
{
if (HeartbeatEnabled)
{
Debug.Console(2, this, "Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
}
}
void HeartbeatStop()
{
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
}
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(string received)
{
try
{
if (HeartbeatEnabled)
{
if (!string.IsNullOrEmpty(HeartbeatString))
{
var remainingText = received.Replace(HeartbeatString, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatString))
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
void HeartbeatAckTimerFail(object o)
{
try
{
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// SendText method
/// </summary>
public void SendText(string text)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Client.SendDataAsync(bytes, bytes.Length, (c, n) =>
{
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
}
}
}
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes)
{
if (bytes.Length > 0)
{
try
{
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
Client.SendData(bytes, bytes.Length);
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
if (ProgramIsStopping)
{
ProgramIsStopping = false;
return;
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
// The client could be null or disposed by this time...
if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
HeartbeatStop();
OnClientReadyForcommunications(false); // socket has gone low
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,413 @@
using System;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
using PepperDash.Core.Logging;
namespace PepperDash.Core
{
/// <summary>
/// Generic UDP Server device
/// </summary>
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
{
private const string SplusKey = "Uninitialized Udp Server";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
///
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
///
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// This event will fire when a message is dequeued that includes the source IP and Port info if needed to determine the source of the received data.
/// </summary>
public event EventHandler<GenericUdpReceiveTextExtraArgs> DataRecievedExtra;
/// <summary>
///
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
///
/// </summary>
public event EventHandler<GenericUdpConnectedEventArgs> UpdateConnectionStatus;
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus
{
get
{
return Server.ServerStatus;
}
}
/// <summary>
///
/// </summary>
public ushort UStatus
{
get { return (ushort)Server.ServerStatus; }
}
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Indicates that the UDP Server is enabled
/// </summary>
public bool IsConnected
{
get;
private set;
}
/// <summary>
/// Numeric value indicating
/// </summary>
public ushort UIsConnected
{
get { return IsConnected ? (ushort)1 : (ushort)0; }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The server
/// </summary>
public UDPServer Server { get; private set; }
/// <summary>
/// Constructor for S+. Make sure to set key, address, port, and buffersize using init method
/// </summary>
public GenericUdpServer()
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
BufferSize = 5000;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericUdpServer(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address;
Port = port;
BufferSize = bufferSize;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
}
/// <summary>
/// Call from S+ to initialize values
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key, string address, ushort port)
{
Key = key;
Hostname = address;
UPort = port;
}
/// <summary>
///
/// </summary>
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp
&& IsConnected)
{
Connect();
}
}
/// <summary>
///
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping)
return;
Debug.Console(1, this, "Program stopping. Disabling Server");
Disconnect();
}
/// <summary>
/// Connect method
/// </summary>
public void Connect()
{
if (Server == null)
{
try
{
var address = IPAddress.Parse(Hostname);
Server = new UDPServer(address, Port, BufferSize);
}
catch (Exception ex)
{
this.LogError("Error parsing IP Address '{ipAddress}': message: {message}", Hostname, ex.Message);
this.LogInformation("Creating UDPServer with default buffersize");
Server = new UDPServer();
}
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key);
return;
}
}
var status = Server.EnableUDPServer(Hostname, Port);
Debug.Console(2, this, "SocketErrorCode: {0}", status);
if (status == SocketErrorCodes.SOCKET_OK)
IsConnected = true;
var handler = UpdateConnectionStatus;
if (handler != null)
handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
// Start receiving data
Server.ReceiveDataAsync(Receive);
}
/// <summary>
/// Disconnect method
/// </summary>
public void Disconnect()
{
if (Server != null)
Server.DisableUDPServer();
IsConnected = false;
var handler = UpdateConnectionStatus;
if (handler != null)
handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
}
/// <summary>
/// Recursive method to receive data
/// </summary>
/// <param name="server"></param>
/// <param name="numBytes"></param>
void Receive(UDPServer server, int numBytes)
{
Debug.Console(2, this, "Received {0} bytes", numBytes);
try
{
if (numBytes <= 0)
return;
var sourceIp = Server.IPAddressLastMessageReceivedFrom;
var sourcePort = Server.IPPortLastMessageReceivedFrom;
var bytes = server.IncomingDataBuffer.Take(numBytes).ToArray();
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
var dataRecivedExtra = DataRecievedExtra;
if (dataRecivedExtra != null)
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
Debug.Console(2, this, "Bytes: {0}", bytes.ToString());
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
this.PrintReceivedBytes(bytes);
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
this.PrintReceivedText(str);
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
catch (Exception ex)
{
this.LogException(ex, "GenericUdpServer Receive error");
}
finally
{
server.ReceiveDataAsync(Receive);
}
}
/// <summary>
/// General send method
/// </summary>
/// <param name="text"></param>
/// <summary>
/// SendText method
/// </summary>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (IsConnected && Server != null)
{
this.PrintSentText(text);
Server.SendData(bytes, bytes.Length);
}
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes)
{
this.PrintSentBytes(bytes);
if (IsConnected && Server != null)
Server.SendData(bytes, bytes.Length);
}
}
/// <summary>
/// Represents a GenericUdpReceiveTextExtraArgs
/// </summary>
public class GenericUdpReceiveTextExtraArgs : EventArgs
{
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
public string IpAddress { get; private set; }
/// <summary>
///
/// </summary>
public int Port { get; private set; }
/// <summary>
///
/// </summary>
public byte[] Bytes { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="bytes"></param>
public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
{
Text = text;
IpAddress = ipAddress;
Port = port;
Bytes = bytes;
}
/// <summary>
/// Stupid S+ Constructor
/// </summary>
public GenericUdpReceiveTextExtraArgs() { }
}
/// <summary>
///
/// </summary>
public class UdpServerPropertiesConfig
{
/// <summary>
///
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
///
/// </summary>
public UdpServerPropertiesConfig()
{
BufferSize = 32768;
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Extension methods for stream debugging
/// </summary>
public static class StreamDebuggingExtensions
{
private static readonly string app = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? $"App {InitialParametersClass.ApplicationNumber}" : $"{InitialParametersClass.RoomId}";
/// <summary>
/// Print the sent bytes to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="bytes">bytes to print</param>
public static void PrintSentBytes(this IStreamDebugging comms, byte[] bytes)
{
if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'");
}
/// <summary>
/// Print the received bytes to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="bytes">bytes to print</param>
public static void PrintReceivedBytes(this IStreamDebugging comms, byte[] bytes)
{
if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'");
}
/// <summary>
/// Print the sent text to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="text">text to print</param>
public static void PrintSentText(this IStreamDebugging comms, string text)
{
if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending Text: '{ComTextHelper.GetDebugText(text)}'");
}
/// <summary>
/// Print the received text to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="text">text to print</param>
public static void PrintReceivedText(this IStreamDebugging comms, string text)
{
if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received Text: '{ComTextHelper.GetDebugText(text)}'");
}
}
}

View File

@@ -0,0 +1,59 @@
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Represents a TcpClientConfigObject
/// </summary>
public class TcpClientConfigObject
{
/// <summary>
/// TcpSsh Properties
/// </summary>
[JsonProperty("control")]
public ControlPropertiesConfig Control { get; set; }
/// <summary>
/// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
/// </summary>
[JsonProperty("secure")]
public bool Secure { get; set; }
/// <summary>
/// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
/// </summary>
[JsonProperty("sharedKeyRequired")]
public bool SharedKeyRequired { get; set; }
/// <summary>
/// The shared key that must match on the server and client
/// </summary>
[JsonProperty("sharedKey")]
public string SharedKey { get; set; }
/// <summary>
/// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received.
/// heartbeats do not raise received events.
/// </summary>
[JsonProperty("heartbeatRequired")]
public bool HeartbeatRequired { get; set; }
/// <summary>
/// The interval in seconds for the heartbeat from the client. If not received client is disconnected
/// </summary>
[JsonProperty("heartbeatRequiredIntervalInSeconds")]
public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
/// <summary>
/// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
/// </summary>
[JsonProperty("heartbeatStringToMatch")]
public string HeartbeatStringToMatch { get; set; }
/// <summary>
/// Receive Queue size must be greater than 20 or defaults to 20
/// </summary>
[JsonProperty("receiveQueueSize")]
public int ReceiveQueueSize { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
/// </summary>
public class TcpServerConfigObject
{
/// <summary>
/// Uique key
/// </summary>
public string Key { get; set; }
/// <summary>
/// Max Clients that the server will allow to connect.
/// </summary>
public ushort MaxClients { get; set; }
/// <summary>
/// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
/// </summary>
public bool Secure { get; set; }
/// <summary>
/// Port for the server to listen on
/// </summary>
public int Port { get; set; }
/// <summary>
/// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// The shared key that must match on the server and client
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received.
/// heartbeats do not raise received events.
/// </summary>
public bool HeartbeatRequired { get; set; }
/// <summary>
/// The interval in seconds for the heartbeat from the client. If not received client is disconnected
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
/// <summary>
/// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
/// </summary>
public string HeartbeatStringToMatch { get; set; }
/// <summary>
/// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Receive Queue size must be greater than 20 or defaults to 20
/// </summary>
public int ReceiveQueueSize { get; set; }
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Crestron Control Methods for a comm object
/// </summary>
public enum eControlMethod
{
/// <summary>
///
/// </summary>
None = 0,
/// <summary>
/// RS232/422/485
/// </summary>
Com,
/// <summary>
/// Crestron IpId (most Crestron ethernet devices)
/// </summary>
IpId,
/// <summary>
/// Crestron IpIdTcp (HD-MD series, etc.)
/// </summary>
IpidTcp,
/// <summary>
/// Crestron IR control
/// </summary>
IR,
/// <summary>
/// SSH client
/// </summary>
Ssh,
/// <summary>
/// TCP/IP client
/// </summary>
Tcpip,
/// <summary>
/// Telnet
/// </summary>
Telnet,
/// <summary>
/// Crestnet device
/// </summary>
Cresnet,
/// <summary>
/// CEC Control, via a DM HDMI port
/// </summary>
Cec,
/// <summary>
/// UDP Server
/// </summary>
Udp,
/// <summary>
/// HTTP client
/// </summary>
Http,
/// <summary>
/// HTTPS client
/// </summary>
Https,
/// <summary>
/// Websocket client
/// </summary>
Ws,
/// <summary>
/// Secure Websocket client
/// </summary>
Wss,
/// <summary>
/// Secure TCP/IP
/// </summary>
SecureTcpIp,
/// <summary>
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction
/// </summary>
ComBridge,
/// <summary>
/// InfinetEX control
/// </summary>
InfinetEx
}
}

View File

@@ -0,0 +1,24 @@
using System;
namespace PepperDash.Core
{
/// <summary>
/// The available settings for stream debugging data format types
/// </summary>
[Flags]
public enum eStreamDebuggingDataTypeSettings
{
/// <summary>
/// Debug data in byte format
/// </summary>
Bytes = 0,
/// <summary>
/// Debug data in text format
/// </summary>
Text = 1,
/// <summary>
/// Debug data in both byte and text formats
/// </summary>
Both = Bytes | Text
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace PepperDash.Core
{
/// <summary>
/// The available settings for stream debugging
/// </summary>
[Flags]
public enum eStreamDebuggingSetting
{
/// <summary>
/// Debug off
/// </summary>
Off = 0,
/// <summary>
/// Debug received data
/// </summary>
Rx = 1,
/// <summary>
/// Debug transmitted data
/// </summary>
Tx = 2,
/// <summary>
/// Debug both received and transmitted data
/// </summary>
Both = Rx | Tx
}
}

View File

@@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// An incoming communication stream
/// </summary>
public interface ICommunicationReceiver : IKeyed
{
/// <summary>
/// Notifies of bytes received
/// </summary>
event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Indicates connection status
/// </summary>
[JsonProperty("isConnected")]
bool IsConnected { get; }
/// <summary>
/// Connect to the device
/// </summary>
void Connect();
/// <summary>
/// Disconnect from the device
/// </summary>
void Disconnect();
}
/// <summary>
/// Defines the contract for IBasicCommunication
/// </summary>
public interface IBasicCommunication : ICommunicationReceiver
{
/// <summary>
/// Send text to the device
/// </summary>
/// <param name="text"></param>
void SendText(string text);
/// <summary>
/// Send bytes to the device
/// </summary>
/// <param name="bytes"></param>
void SendBytes(byte[] bytes);
}
/// <summary>
/// Represents a device that implements IBasicCommunication and IStreamDebugging
/// </summary>
public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging
{
}
/// <summary>
/// Represents a device with stream debugging capablities
/// </summary>
public interface IStreamDebugging : IKeyed
{
/// <summary>
/// Object to enable stream debugging
/// </summary>
[JsonProperty("streamDebugging")]
CommunicationStreamDebugging StreamDebugging { get; }
}
/// <summary>
/// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
/// GenericTcpIpClient
/// </summary>
public interface ISocketStatus : IBasicCommunication
{
/// <summary>
/// Notifies of socket status changes
/// </summary>
event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/// The current socket status of the client
/// </summary>
[JsonProperty("clientStatus")]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
SocketStatus ClientStatus { get; }
}
/// <summary>
/// Describes a device that implements ISocketStatus and IStreamDebugging
/// </summary>
public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging
{
}
/// <summary>
/// Describes a device that can automatically attempt to reconnect
/// </summary>
public interface IAutoReconnect
{
/// <summary>
/// Enable automatic recconnect
/// </summary>
[JsonProperty("autoReconnect")]
bool AutoReconnect { get; set; }
/// <summary>
/// Interval in ms to attempt automatic recconnections
/// </summary>
[JsonProperty("autoReconnectIntervalMs")]
int AutoReconnectIntervalMs { get; set; }
}
/// <summary>
///
/// </summary>
public enum eGenericCommMethodStatusChangeType
{
/// <summary>
/// Connected
/// </summary>
Connected,
/// <summary>
/// Disconnected
/// </summary>
Disconnected
}
/// <summary>
/// This delegate defines handler for IBasicCommunication status changes
/// </summary>
/// <param name="comm">Device firing the status change</param>
/// <param name="status"></param>
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
/// <summary>
///
/// </summary>
public class GenericCommMethodReceiveBytesArgs : EventArgs
{
/// <summary>
/// Gets or sets the Bytes
/// </summary>
public byte[] Bytes { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
public GenericCommMethodReceiveBytesArgs(byte[] bytes)
{
Bytes = bytes;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericCommMethodReceiveBytesArgs() { }
}
/// <summary>
///
/// </summary>
public class GenericCommMethodReceiveTextArgs : EventArgs
{
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
public string Delimiter { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
public GenericCommMethodReceiveTextArgs(string text)
{
Text = text;
}
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="delimiter"></param>
public GenericCommMethodReceiveTextArgs(string text, string delimiter)
: this(text)
{
Delimiter = delimiter;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericCommMethodReceiveTextArgs() { }
}
}

View File

@@ -0,0 +1,257 @@
using System;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using Serilog.Events;
namespace PepperDash.Core.Config
{
/// <summary>
/// Reads a Portal formatted config file
/// </summary>
public class PortalConfigReader
{
const string template = "template";
const string system = "system";
const string systemUrl = "system_url";
const string templateUrl = "template_url";
const string info = "info";
const string devices = "devices";
const string rooms = "rooms";
const string sourceLists = "sourceLists";
const string destinationLists = "destinationLists";
const string cameraLists = "cameraLists";
const string audioControlPointLists = "audioControlPointLists";
const string tieLines = "tieLines";
const string joinMaps = "joinMaps";
const string global = "global";
/// <summary>
/// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
/// </summary>
/// <returns>JObject of config file</returns>
public static void ReadAndMergeFileIfNecessary(string filePath, string savePath)
{
try
{
if (!File.Exists(filePath))
{
Debug.LogError(
"ERROR: Configuration file not present. Please load file to {0} and reset program", filePath);
}
using (StreamReader fs = new StreamReader(filePath))
{
var jsonObj = JObject.Parse(fs.ReadToEnd());
if(jsonObj[template] != null && jsonObj[system] != null)
{
// it's a double-config, merge it.
var merged = MergeConfigs(jsonObj);
if (jsonObj[systemUrl] != null)
{
merged[systemUrl] = jsonObj[systemUrl].Value<string>();
}
if (jsonObj[templateUrl] != null)
{
merged[templateUrl] = jsonObj[templateUrl].Value<string>();
}
jsonObj = merged;
}
using (StreamWriter fw = new StreamWriter(savePath))
{
fw.Write(jsonObj.ToString(Formatting.Indented));
Debug.LogMessage(LogEventLevel.Debug, "JSON config merged and saved to {0}", savePath);
}
}
}
catch (Exception e)
{
Debug.LogMessage(e, "ERROR: Config load failed");
}
}
/// <summary>
///
/// </summary>
/// <param name="doubleConfig"></param>
/// <returns></returns>
/// <summary>
/// MergeConfigs method
/// </summary>
public static JObject MergeConfigs(JObject doubleConfig)
{
var system = JObject.FromObject(doubleConfig["system"]);
var template = JObject.FromObject(doubleConfig["template"]);
var merged = new JObject();
// Put together top-level objects
if (system[info] != null)
merged.Add(info, Merge(template[info], system[info], info));
else
merged.Add(info, template[info]);
merged.Add(devices, MergeArraysOnTopLevelProperty(template[devices] as JArray,
system[devices] as JArray, "key", devices));
if (system[rooms] == null)
merged.Add(rooms, template[rooms]);
else
merged.Add(rooms, MergeArraysOnTopLevelProperty(template[rooms] as JArray,
system[rooms] as JArray, "key", rooms));
if (system[sourceLists] == null)
merged.Add(sourceLists, template[sourceLists]);
else
merged.Add(sourceLists, Merge(template[sourceLists], system[sourceLists], sourceLists));
if (system[destinationLists] == null)
merged.Add(destinationLists, template[destinationLists]);
else
merged.Add(destinationLists,
Merge(template[destinationLists], system[destinationLists], destinationLists));
if (system[cameraLists] == null)
merged.Add(cameraLists, template[cameraLists]);
else
merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists));
if (system[audioControlPointLists] == null)
merged.Add(audioControlPointLists, template[audioControlPointLists]);
else
merged.Add(audioControlPointLists,
Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists));
// Template tie lines take precedence. Config tool doesn't do them at system
// level anyway...
if (template[tieLines] != null)
merged.Add(tieLines, template[tieLines]);
else if (system[tieLines] != null)
merged.Add(tieLines, system[tieLines]);
else
merged.Add(tieLines, new JArray());
if (template[joinMaps] != null)
merged.Add(joinMaps, template[joinMaps]);
else
merged.Add(joinMaps, new JObject());
if (system[global] != null)
merged.Add(global, Merge(template[global], system[global], global));
else
merged.Add(global, template[global]);
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
return merged;
}
/// <summary>
/// Merges the contents of a base and a delta array, matching the entries on a top-level property
/// given by propertyName. Returns a merge of them. Items in the delta array that do not have
/// a matched item in base array will not be merged. Non keyed system items will replace the template items.
/// </summary>
static JArray MergeArraysOnTopLevelProperty(JArray a1, JArray a2, string propertyName, string path)
{
var result = new JArray();
if (a2 == null || a2.Count == 0) // If the system array is null or empty, return the template array
return a1;
else if (a1 != null)
{
if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array
{ // with the system array
return a2;
}
else // The arrays are keyed, merge them by key
{
for (int i = 0; i < a1.Count(); i++)
{
var a1Dev = a1[i];
// Try to get a system device and if found, merge it onto template
var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value<int>("uid") == tmplDev.Value<int>("uid"));
if (a2Match != null)
{
var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match));
result.Add(mergedItem);
}
else
result.Add(a1Dev);
}
}
}
return result;
}
/// <summary>
/// Helper for using with JTokens. Converts to JObject
/// </summary>
static JObject Merge(JToken t1, JToken t2, string path)
{
return Merge(JObject.FromObject(t1), JObject.FromObject(t2), path);
}
/// <summary>
/// Merge o2 onto o1
/// </summary>
/// <param name="o1"></param>
/// <param name="o2"></param>
/// <param name="path"></param>
static JObject Merge(JObject o1, JObject o2, string path)
{
foreach (var o2Prop in o2)
{
var propKey = o2Prop.Key;
var o1Value = o1[propKey];
var o2Value = o2[propKey];
// if the property doesn't exist on o1, then add it.
if (o1Value == null)
{
o1.Add(propKey, o2Value);
}
// otherwise merge them
else
{
// Drill down
var propPath = String.Format("{0}.{1}", path, propKey);
try
{
if (o1Value is JArray)
{
if (o2Value is JArray)
{
o1Value.Replace(MergeArraysOnTopLevelProperty(o1Value as JArray, o2Value as JArray, "key", propPath));
}
}
else if (o2Prop.Value.HasValues && o1Value.HasValues)
{
o1Value.Replace(Merge(JObject.FromObject(o1Value), JObject.FromObject(o2Value), propPath));
}
else
{
o1Value.Replace(o2Prop.Value);
}
}
catch (Exception e)
{
Debug.LogError($"Cannot merge items at path {propPath}: \r{e}");
}
}
}
return o1;
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Represents a EncodingHelper
/// </summary>
public class EncodingHelper
{
/// <summary>
/// ConvertUtf8ToAscii method
/// </summary>
public static string ConvertUtf8ToAscii(string utf8String)
{
return Encoding.ASCII.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length);
}
/// <summary>
/// ConvertUtf8ToUtf16 method
/// </summary>
public static string ConvertUtf8ToUtf16(string utf8String)
{
return Encoding.Unicode.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length);
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Serilog;
namespace PepperDash.Core
{
/// <summary>
/// Unique key interface to require a unique key for the class
/// </summary>
public interface IKeyed
{
/// <summary>
/// Unique Key
/// </summary>
[JsonProperty("key")]
string Key { get; }
}
/// <summary>
/// Named Keyed device interface. Forces the device to have a Unique Key and a name.
/// </summary>
public interface IKeyName : IKeyed
{
/// <summary>
/// Isn't it obvious :)
/// </summary>
[JsonProperty("name")]
string Name { get; }
}
}

View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using Serilog.Events;
namespace PepperDash.Core
{
//*********************************************************************************************************
/// <summary>
/// Represents a Device
/// </summary>
public class Device : IKeyName
{
/// <summary>
/// Unique Key
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// Gets or sets the Name
/// </summary>
public string Name { get; protected set; }
/// <summary>
///
/// </summary>
public bool Enabled { get; protected set; }
/// <summary>
/// A place to store reference to the original config object, if any. These values should
/// NOT be used as properties on the device as they are all publicly-settable values.
/// </summary>
//public DeviceConfig Config { get; private set; }
/// <summary>
/// Helper method to check if Config exists
/// </summary>
//public bool HasConfig { get { return Config != null; } }
List<Action> _PreActivationActions;
List<Action> _PostActivationActions;
/// <summary>
///
/// </summary>
public static Device DefaultDevice { get { return _DefaultDevice; } }
static Device _DefaultDevice = new Device("Default", "Default");
/// <summary>
/// Base constructor for all Devices.
/// </summary>
/// <param name="key"></param>
public Device(string key)
{
Key = key;
if (key.Contains(".")) Debug.LogMessage(LogEventLevel.Information, "WARNING: Device key should not include '.'", this);
Name = "";
}
/// <summary>
/// Constructor with key and name
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
public Device(string key, string name) : this(key)
{
Name = name;
}
//public Device(DeviceConfig config)
// : this(config.Key, config.Name)
//{
// Config = config;
//}
/// <summary>
/// Adds a pre activation action
/// </summary>
/// <param name="act"></param>
public void AddPreActivationAction(Action act)
{
if (_PreActivationActions == null)
_PreActivationActions = new List<Action>();
_PreActivationActions.Add(act);
}
/// <summary>
/// Adds a post activation action
/// </summary>
/// <param name="act"></param>
/// <summary>
/// AddPostActivationAction method
/// </summary>
public void AddPostActivationAction(Action act)
{
if (_PostActivationActions == null)
_PostActivationActions = new List<Action>();
_PostActivationActions.Add(act);
}
/// <summary>
/// PreActivate method
/// </summary>
public void PreActivate()
{
if (_PreActivationActions != null)
_PreActivationActions.ForEach(a =>
{
try
{
a.Invoke();
}
catch (Exception e)
{
Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this);
}
});
}
/// <summary>
/// Activate method
/// </summary>
public bool Activate()
{
//if (_PreActivationActions != null)
// _PreActivationActions.ForEach(a => a.Invoke());
var result = CustomActivate();
//if(result && _PostActivationActions != null)
// _PostActivationActions.ForEach(a => a.Invoke());
return result;
}
/// <summary>
/// PostActivate method
/// </summary>
public void PostActivate()
{
if (_PostActivationActions != null)
_PostActivationActions.ForEach(a =>
{
try
{
a.Invoke();
}
catch (Exception e)
{
Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
}
});
}
/// <summary>
/// Called in between Pre and PostActivationActions when Activate() is called.
/// Override to provide addtitional setup when calling activation. Overriding classes
/// do not need to call base.CustomActivate()
/// </summary>
/// <returns>true if device activated successfully.</returns>
/// <summary>
/// CustomActivate method
/// </summary>
public virtual bool CustomActivate() { return true; }
/// <summary>
/// Call to deactivate device - unlink events, etc. Overriding classes do not
/// need to call base.Deactivate()
/// </summary>
/// <returns></returns>
public virtual bool Deactivate() { return true; }
/// <summary>
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
/// </summary>
public virtual void Initialize()
{
}
/// <summary>
/// Helper method to check object for bool value false and fire an Action method
/// </summary>
/// <param name="o">Should be of type bool, others will be ignored</param>
/// <param name="a">Action to be run when o is false</param>
public void OnFalse(object o, Action a)
{
if (o is bool && !(bool)o) a();
}
/// <summary>
/// Returns a string representation of the object, including its key and name.
/// </summary>
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
/// null or empty, "---" is used in place of the name.</remarks>
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
/// <summary>
/// ToString method
/// </summary>
public override string ToString()
{
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
}
}
}

View File

@@ -0,0 +1,117 @@
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Serilog.Events;
namespace PepperDash.Core
{
/// <summary>
/// Represents a EthernetHelper
/// </summary>
public class EthernetHelper
{
/// <summary>
///
/// </summary>
public static EthernetHelper LanHelper
{
get
{
if (_LanHelper == null) _LanHelper = new EthernetHelper(0);
return _LanHelper;
}
}
static EthernetHelper _LanHelper;
// ADD OTHER HELPERS HERE
/// <summary>
/// Gets or sets the PortNumber
/// </summary>
public int PortNumber { get; private set; }
private EthernetHelper(int portNumber)
{
PortNumber = portNumber;
}
/// <summary>
///
/// </summary>
[JsonProperty("linkActive")]
public bool LinkActive
{
get
{
var status = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS, 0);
Debug.LogMessage(LogEventLevel.Information, "LinkActive = {0}", status);
return status == "";
}
}
/// <summary>
///
/// </summary>
[JsonProperty("dchpActive")]
public bool DhcpActive
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, 0) == "ON";
}
}
/// <summary>
///
/// </summary>
[JsonProperty("hostname")]
public string Hostname
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
}
}
/// <summary>
///
/// </summary>
[JsonProperty("ipAddress")]
public string IPAddress
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
}
}
/// <summary>
///
/// </summary>
[JsonProperty("subnetMask")]
public string SubnetMask
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, 0);
}
}
/// <summary>
///
/// </summary>
[JsonProperty("defaultGateway")]
public string DefaultGateway
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0);
}
}
}
}

View File

@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Bool change event args
/// </summary>
public class BoolChangeEventArgs : EventArgs
{
/// <summary>
/// Boolean state property
/// </summary>
public bool State { get; set; }
/// <summary>
/// Gets or sets the IntValue
/// </summary>
public ushort IntValue { get { return (ushort)(State ? 1 : 0); } }
/// <summary>
/// Gets or sets the Type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Gets or sets the Index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public BoolChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="state"></param>
/// <param name="type"></param>
public BoolChangeEventArgs(bool state, ushort type)
{
State = state;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="state"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public BoolChangeEventArgs(bool state, ushort type, ushort index)
{
State = state;
Type = type;
Index = index;
}
}
/// <summary>
/// Represents a UshrtChangeEventArgs
/// </summary>
public class UshrtChangeEventArgs : EventArgs
{
/// <summary>
/// Ushort change event args integer value
/// </summary>
public ushort IntValue { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Gets or sets the Index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public UshrtChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="intValue"></param>
/// <param name="type"></param>
public UshrtChangeEventArgs(ushort intValue, ushort type)
{
IntValue = intValue;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="intValue"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public UshrtChangeEventArgs(ushort intValue, ushort type, ushort index)
{
IntValue = intValue;
Type = type;
Index = index;
}
}
/// <summary>
/// Represents a StringChangeEventArgs
/// </summary>
public class StringChangeEventArgs : EventArgs
{
/// <summary>
/// String change event args value
/// </summary>
public string StringValue { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Gets or sets the Index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public StringChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="stringValue"></param>
/// <param name="type"></param>
public StringChangeEventArgs(string stringValue, ushort type)
{
StringValue = stringValue;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="stringValue"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public StringChangeEventArgs(string stringValue, ushort type, ushort index)
{
StringValue = stringValue;
Type = type;
Index = index;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.GenericRESTfulCommunications
{
/// <summary>
/// Constants
/// </summary>
public class GenericRESTfulConstants
{
/// <summary>
/// Generic boolean change
/// </summary>
public const ushort BoolValueChange = 1;
/// <summary>
/// Generic Ushort change
/// </summary>
public const ushort UshrtValueChange = 101;
/// <summary>
/// Response Code Ushort change
/// </summary>
public const ushort ResponseCodeChange = 102;
/// <summary>
/// Generic String chagne
/// </summary>
public const ushort StringValueChange = 201;
/// <summary>
/// Response string change
/// </summary>
public const ushort ResponseStringChange = 202;
/// <summary>
/// Error string change
/// </summary>
public const ushort ErrorStringChange = 203;
}
}

View File

@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.Net.Https;
namespace PepperDash.Core.GenericRESTfulCommunications
{
/// <summary>
/// Generic RESTful communication class
/// </summary>
public class GenericRESTfulClient
{
/// <summary>
/// Boolean event handler
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort event handler
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String event handler
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Constructor
/// </summary>
public GenericRESTfulClient()
{
}
/// <summary>
/// Generic RESTful submit request
/// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
/// <param name="requestType"></param>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="contentType"></param>
public void SubmitRequest(string url, ushort port, ushort requestType, string contentType, string username, string password)
{
if (url.StartsWith("https:", StringComparison.OrdinalIgnoreCase))
{
SubmitRequestHttps(url, port, requestType, contentType, username, password);
}
else if (url.StartsWith("http:", StringComparison.OrdinalIgnoreCase))
{
SubmitRequestHttp(url, port, requestType, contentType, username, password);
}
else
{
OnStringChange(string.Format("Invalid URL {0}", url), 0, GenericRESTfulConstants.ErrorStringChange);
}
}
/// <summary>
/// Private HTTP submit request
/// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
/// <param name="requestType"></param>
/// <param name="contentType"></param>
/// <param name="username"></param>
/// <param name="password"></param>
private void SubmitRequestHttp(string url, ushort port, ushort requestType, string contentType, string username, string password)
{
try
{
HttpClient client = new HttpClient();
HttpClientRequest request = new HttpClientRequest();
HttpClientResponse response;
client.KeepAlive = false;
if(port >= 1 || port <= 65535)
client.Port = port;
else
client.Port = 80;
var authorization = "";
if (!string.IsNullOrEmpty(username))
authorization = EncodeBase64(username, password);
if (!string.IsNullOrEmpty(authorization))
request.Header.SetHeaderValue("Authorization", authorization);
if (!string.IsNullOrEmpty(contentType))
request.Header.ContentType = contentType;
request.Url.Parse(url);
request.RequestType = (Crestron.SimplSharp.Net.Http.RequestType)requestType;
response = client.Dispatch(request);
CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString()));
if (!string.IsNullOrEmpty(response.ContentString.ToString()))
OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange);
if (response.Code > 0)
OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange);
}
catch (Exception e)
{
//var msg = string.Format("SubmitRequestHttp({0}, {1}, {2}) failed:{3}", url, port, requestType, e.Message);
//CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
CrestronConsole.PrintLine(e.Message);
OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange);
}
}
/// <summary>
/// Private HTTPS submit request
/// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
/// <param name="requestType"></param>
/// <param name="contentType"></param>
/// <param name="username"></param>
/// <param name="password"></param>
private void SubmitRequestHttps(string url, ushort port, ushort requestType, string contentType, string username, string password)
{
try
{
HttpsClient client = new HttpsClient();
HttpsClientRequest request = new HttpsClientRequest();
HttpsClientResponse response;
client.KeepAlive = false;
client.HostVerification = false;
client.PeerVerification = false;
var authorization = "";
if (!string.IsNullOrEmpty(username))
authorization = EncodeBase64(username, password);
if (!string.IsNullOrEmpty(authorization))
request.Header.SetHeaderValue("Authorization", authorization);
if (!string.IsNullOrEmpty(contentType))
request.Header.ContentType = contentType;
request.Url.Parse(url);
request.RequestType = (Crestron.SimplSharp.Net.Https.RequestType)requestType;
response = client.Dispatch(request);
CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString()));
if(!string.IsNullOrEmpty(response.ContentString.ToString()))
OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange);
if(response.Code > 0)
OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange);
}
catch (Exception e)
{
//var msg = string.Format("SubmitRequestHttps({0}, {1}, {2}, {3}, {4}) failed:{5}", url, port, requestType, username, password, e.Message);
//CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
CrestronConsole.PrintLine(e.Message);
OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange);
}
}
/// <summary>
/// Private method to encode username and password to Base64 string
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns>authorization</returns>
private string EncodeBase64(string username, string password)
{
var authorization = "";
try
{
if (!string.IsNullOrEmpty(username))
{
string base64String = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(string.Format("{0}:{1}", username, password)));
authorization = string.Format("Basic {0}", base64String);
}
}
catch (Exception e)
{
var msg = string.Format("EncodeBase64({0}, {1}) failed:\r{2}", username, password, e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
return "" ;
}
return authorization;
}
/// <summary>
/// Protected method to handle boolean change events
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Protected mehtod to handle ushort change events
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort value, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(value, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// Protected method to handle string change events
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.JsonStandardObjects
{
/// <summary>
/// Constants for simpl modules
/// </summary>
public class JsonStandardDeviceConstants
{
/// <summary>
/// Json object evaluated constant
/// </summary>
public const ushort JsonObjectEvaluated = 2;
/// <summary>
/// Json object changed constant
/// </summary>
public const ushort JsonObjectChanged = 104;
}
/// <summary>
///
/// </summary>
public class DeviceChangeEventArgs : EventArgs
{
/// <summary>
/// Device change event args object
/// </summary>
public DeviceConfig Device { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Gets or sets the Index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public DeviceChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="device"></param>
/// <param name="type"></param>
public DeviceChangeEventArgs(DeviceConfig device, ushort type)
{
Device = device;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="device"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public DeviceChangeEventArgs(DeviceConfig device, ushort type, ushort index)
{
Device = device;
Type = type;
Index = index;
}
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Linq;
using Crestron.SimplSharp;
using PepperDash.Core.JsonToSimpl;
using Serilog.Events;
namespace PepperDash.Core.JsonStandardObjects
{
/// <summary>
/// Device class
/// </summary>
public class DeviceConfig
{
/// <summary>
/// JSON config key property
/// </summary>
public string key { get; set; }
/// <summary>
/// JSON config name property
/// </summary>
public string name { get; set; }
/// <summary>
/// JSON config type property
/// </summary>
public string type { get; set; }
/// <summary>
/// JSON config properties
/// </summary>
public PropertiesConfig properties { get; set; }
/// <summary>
/// Bool change event handler
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort change event handler
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String change event handler
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Object change event handler
/// </summary>
public event EventHandler<DeviceChangeEventArgs> DeviceChange;
/// <summary>
/// Constructor
/// </summary>
public DeviceConfig()
{
properties = new PropertiesConfig();
}
/// <summary>
/// Initialize method
/// </summary>
/// <param name="uniqueID"></param>
/// <param name="deviceKey"></param>
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string uniqueID, string deviceKey)
{
// S+ set EvaluateFb low
OnBoolChange(false, 0, JsonStandardDeviceConstants.JsonObjectEvaluated);
// validate parameters
if (string.IsNullOrEmpty(uniqueID) || string.IsNullOrEmpty(deviceKey))
{
Debug.LogMessage(LogEventLevel.Debug, "UniqueID ({0} or key ({1} is null or empty", uniqueID, deviceKey);
// S+ set EvaluteFb high
OnBoolChange(true, 0, JsonStandardDeviceConstants.JsonObjectEvaluated);
return;
}
key = deviceKey;
try
{
// get the file using the unique ID
JsonToSimplMaster jsonMaster = J2SGlobal.GetMasterByFile(uniqueID);
if (jsonMaster == null)
{
Debug.LogMessage(LogEventLevel.Debug, "Could not find JSON file with uniqueID {0}", uniqueID);
return;
}
// get the device configuration using the key
var devices = jsonMaster.JsonObject.ToObject<RootObject>().devices;
var device = devices.FirstOrDefault(d => d.key.Equals(key));
if (device == null)
{
Debug.LogMessage(LogEventLevel.Debug, "Could not find device with key {0}", key);
return;
}
OnObjectChange(device, 0, JsonStandardDeviceConstants.JsonObjectChanged);
var index = devices.IndexOf(device);
OnStringChange(string.Format("devices[{0}]", index), 0, JsonToSimplConstants.FullPathToArrayChange);
}
catch (Exception e)
{
var msg = string.Format("Device {0} lookup failed:\r{1}", key, e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
}
finally
{
// S+ set EvaluteFb high
OnBoolChange(true, 0, JsonStandardDeviceConstants.JsonObjectEvaluated);
}
}
#region EventHandler Helpers
/// <summary>
/// BoolChange event handler helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// UshrtChange event handler helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort state, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(state, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// StringChange event handler helper
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
/// <summary>
/// ObjectChange event handler helper
/// </summary>
/// <param name="device"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnObjectChange(DeviceConfig device, ushort index, ushort type)
{
if (DeviceChange != null)
{
var args = new DeviceChangeEventArgs(device, type);
args.Index = index;
DeviceChange(this, args);
}
}
#endregion EventHandler Helpers
}
}

View File

@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.JsonStandardObjects
{
/*
Convert JSON snippt to C#: http://json2csharp.com/#
JSON Snippet:
{
"devices": [
{
"key": "deviceKey",
"name": "deviceName",
"type": "deviceType",
"properties": {
"deviceId": 1,
"enabled": true,
"control": {
"method": "methodName",
"controlPortDevKey": "deviceControlPortDevKey",
"controlPortNumber": 1,
"comParams": {
"baudRate": 9600,
"dataBits": 8,
"stopBits": 1,
"parity": "None",
"protocol": "RS232",
"hardwareHandshake": "None",
"softwareHandshake": "None",
"pacing": 0
},
"tcpSshProperties": {
"address": "172.22.1.101",
"port": 23,
"username": "user01",
"password": "password01",
"autoReconnect": false,
"autoReconnectIntervalMs": 10000
}
}
}
}
]
}
*/
/// <summary>
/// Represents a ComParamsConfig
/// </summary>
public class ComParamsConfig
{
/// <summary>
/// Gets or sets the baudRate
/// </summary>
public int baudRate { get; set; }
/// <summary>
///
/// </summary>
public int dataBits { get; set; }
/// <summary>
///
/// </summary>
public int stopBits { get; set; }
/// <summary>
///
/// </summary>
public string parity { get; set; }
/// <summary>
///
/// </summary>
public string protocol { get; set; }
/// <summary>
///
/// </summary>
public string hardwareHandshake { get; set; }
/// <summary>
///
/// </summary>
public string softwareHandshake { get; set; }
/// <summary>
///
/// </summary>
public int pacing { get; set; }
// convert properties for simpl
/// <summary>
/// Gets or sets the simplBaudRate
/// </summary>
public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } }
/// <summary>
/// Gets or sets the simplDataBits
/// </summary>
public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } }
/// <summary>
///
/// </summary>
public ushort simplStopBits { get { return Convert.ToUInt16(stopBits); } }
/// <summary>
///
/// </summary>
public ushort simplPacing { get { return Convert.ToUInt16(pacing); } }
/// <summary>
/// Constructor
/// </summary>
public ComParamsConfig()
{
}
}
/// <summary>
/// Device TCP/SSH properties class
/// </summary>
public class TcpSshPropertiesConfig
{
/// <summary>
///
/// </summary>
public string address { get; set; }
/// <summary>
///
/// </summary>
public int port { get; set; }
/// <summary>
///
/// </summary>
public string username { get; set; }
/// <summary>
///
/// </summary>
public string password { get; set; }
/// <summary>
///
/// </summary>
public bool autoReconnect { get; set; }
/// <summary>
///
/// </summary>
public int autoReconnectIntervalMs { get; set; }
// convert properties for simpl
/// <summary>
/// Gets or sets the simplPort
/// </summary>
public ushort simplPort { get { return Convert.ToUInt16(port); } }
/// <summary>
/// Gets or sets the simplAutoReconnect
/// </summary>
public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } }
/// <summary>
///
/// </summary>
public ushort simplAutoReconnectIntervalMs { get { return Convert.ToUInt16(autoReconnectIntervalMs); } }
/// <summary>
/// Constructor
/// </summary>
public TcpSshPropertiesConfig()
{
}
}
/// <summary>
/// Device control class
/// </summary>
public class ControlConfig
{
/// <summary>
///
/// </summary>
public string method { get; set; }
/// <summary>
///
/// </summary>
public string controlPortDevKey { get; set; }
/// <summary>
///
/// </summary>
public int controlPortNumber { get; set; }
/// <summary>
///
/// </summary>
public ComParamsConfig comParams { get; set; }
/// <summary>
///
/// </summary>
public TcpSshPropertiesConfig tcpSshProperties { get; set; }
// convert properties for simpl
/// <summary>
/// Gets or sets the simplControlPortNumber
/// </summary>
public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } }
/// <summary>
/// Constructor
/// </summary>
public ControlConfig()
{
comParams = new ComParamsConfig();
tcpSshProperties = new TcpSshPropertiesConfig();
}
}
/// <summary>
/// Represents a PropertiesConfig
/// </summary>
public class PropertiesConfig
{
/// <summary>
///
/// </summary>
public int deviceId { get; set; }
/// <summary>
///
/// </summary>
public bool enabled { get; set; }
/// <summary>
///
/// </summary>
public ControlConfig control { get; set; }
// convert properties for simpl
/// <summary>
/// Gets or sets the simplDeviceId
/// </summary>
public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } }
/// <summary>
/// Gets or sets the simplEnabled
/// </summary>
public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } }
/// <summary>
/// Constructor
/// </summary>
public PropertiesConfig()
{
control = new ControlConfig();
}
}
/// <summary>
/// Root device class
/// </summary>
public class RootObject
{
/// <summary>
/// The collection of devices
/// </summary>
public List<DeviceConfig> devices { get; set; }
}
}

View File

@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Constants for Simpl modules
/// </summary>
public class JsonToSimplConstants
{
/// <summary>
///
/// </summary>
public const ushort BoolValueChange = 1;
/// <summary>
///
/// </summary>
public const ushort JsonIsValidBoolChange = 2;
/// <summary>
/// Reports the if the device is 3-series compatible
/// </summary>
public const ushort ProgramCompatibility3SeriesChange = 3;
/// <summary>
/// Reports the if the device is 4-series compatible
/// </summary>
public const ushort ProgramCompatibility4SeriesChange = 4;
/// <summary>
/// Reports the device platform enum value
/// </summary>
public const ushort DevicePlatformValueChange = 5;
/// <summary>
///
/// </summary>
public const ushort UshortValueChange = 101;
/// <summary>
///
/// </summary>
public const ushort StringValueChange = 201;
/// <summary>
///
/// </summary>
public const ushort FullPathToArrayChange = 202;
/// <summary>
///
/// </summary>
public const ushort ActualFilePathChange = 203;
/// <summary>
///
/// </summary>
public const ushort FilenameResolvedChange = 204;
/// <summary>
///
/// </summary>
public const ushort FilePathResolvedChange = 205;
/// <summary>
/// Reports the root directory change
/// </summary>
public const ushort RootDirectoryChange = 206;
/// <summary>
/// Reports the room ID change
/// </summary>
public const ushort RoomIdChange = 207;
/// <summary>
/// Reports the room name change
/// </summary>
public const ushort RoomNameChange = 208;
}
/// <summary>
/// S+ values delegate
/// </summary>
public delegate void SPlusValuesDelegate();
/// <summary>
/// S+ values wrapper
/// </summary>
public class SPlusValueWrapper
{
/// <summary>
/// Gets or sets the ValueType
/// </summary>
public SPlusType ValueType { get; private set; }
/// <summary>
///
/// </summary>
public ushort Index { get; private set; }
/// <summary>
///
/// </summary>
public ushort BoolUShortValue { get; set; }
/// <summary>
///
/// </summary>
public string StringValue { get; set; }
/// <summary>
///
/// </summary>
public SPlusValueWrapper() {}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <param name="index"></param>
public SPlusValueWrapper(SPlusType type, ushort index)
{
ValueType = type;
Index = index;
}
}
/// <summary>
/// Enumeration of SPlusType values
/// </summary>
public enum SPlusType
{
/// <summary>
/// Digital
/// </summary>
Digital,
/// <summary>
/// Analog
/// </summary>
Analog,
/// <summary>
/// String
/// </summary>
String
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Serilog.Events;
//using PepperDash.Core;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// The global class to manage all the instances of JsonToSimplMaster
/// </summary>
public class J2SGlobal
{
static List<JsonToSimplMaster> Masters = new List<JsonToSimplMaster>();
/// <summary>
/// Adds a file master. If the master's key or filename is equivalent to any existing
/// master, this will fail
/// </summary>
/// <param name="master">New master to add</param>
///
/// <summary>
/// AddMaster method
/// </summary>
public static void AddMaster(JsonToSimplMaster master)
{
if (master == null)
throw new ArgumentNullException("master");
if (string.IsNullOrEmpty(master.UniqueID))
throw new InvalidOperationException("JSON Master cannot be added with a null UniqueId");
Debug.LogMessage(LogEventLevel.Debug, "JSON Global adding master {0}", master.UniqueID);
if (Masters.Contains(master)) return;
var existing = Masters.FirstOrDefault(m =>
m.UniqueID.Equals(master.UniqueID, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
Masters.Add(master);
}
else
{
var msg = string.Format("Cannot add JSON Master with unique ID '{0}'.\rID is already in use on another master.", master.UniqueID);
CrestronConsole.PrintLine(msg);
ErrorLog.Warn(msg);
}
}
/// <summary>
/// GetMasterByFile method
/// </summary>
public static JsonToSimplMaster GetMasterByFile(string file)
{
return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using Serilog.Events;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Represents a JsonToSimplArrayLookupChild
/// </summary>
public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase
{
/// <summary>
///
/// </summary>
public string SearchPropertyName { get; set; }
/// <summary>
///
/// </summary>
public string SearchPropertyValue { get; set; }
int ArrayIndex;
/// <summary>
/// For gt2.4.1 array lookups
/// </summary>
/// <param name="file"></param>
/// <param name="key"></param>
/// <param name="pathPrefix"></param>
/// <param name="pathSuffix"></param>
/// <param name="searchPropertyName"></param>
/// <param name="searchPropertyValue"></param>
public void Initialize(string file, string key, string pathPrefix, string pathSuffix,
string searchPropertyName, string searchPropertyValue)
{
base.Initialize(file, key, pathPrefix, pathSuffix);
SearchPropertyName = searchPropertyName;
SearchPropertyValue = searchPropertyValue;
}
/// <summary>
/// For newer >=2.4.1 array lookups.
/// </summary>
/// <param name="file"></param>
/// <param name="key"></param>
/// <param name="pathPrefix"></param>
/// <param name="pathAppend"></param>
/// <param name="pathSuffix"></param>
/// <param name="searchPropertyName"></param>
/// <param name="searchPropertyValue"></param>
public void InitializeWithAppend(string file, string key, string pathPrefix, string pathAppend,
string pathSuffix, string searchPropertyName, string searchPropertyValue)
{
string pathPrefixWithAppend = (pathPrefix != null ? pathPrefix : "") + GetPathAppend(pathAppend);
base.Initialize(file, key, pathPrefixWithAppend, pathSuffix);
SearchPropertyName = searchPropertyName;
SearchPropertyValue = searchPropertyValue;
}
//PathPrefix+ArrayName+[x]+path+PathSuffix
/// <summary>
///
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
protected override string GetFullPath(string path)
{
return string.Format("{0}[{1}].{2}{3}",
PathPrefix == null ? "" : PathPrefix,
ArrayIndex,
path,
PathSuffix == null ? "" : PathSuffix);
}
/// <summary>
/// ProcessAll method
/// </summary>
/// <inheritdoc />
public override void ProcessAll()
{
if (FindInArray())
base.ProcessAll();
}
/// <summary>
/// Provides the path append for GetFullPath
/// </summary>
/// <returns></returns>
string GetPathAppend(string a)
{
if (string.IsNullOrEmpty(a))
{
return "";
}
if (a.StartsWith("."))
{
return a;
}
else
{
return "." + a;
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
bool FindInArray()
{
if (Master == null)
throw new InvalidOperationException("Cannot do operations before master is linked");
if (Master.JsonObject == null)
throw new InvalidOperationException("Cannot do operations before master JSON has read");
if (PathPrefix == null)
throw new InvalidOperationException("Cannot do operations before PathPrefix is set");
var token = Master.JsonObject.SelectToken(PathPrefix);
if (token is JArray)
{
var array = token as JArray;
try
{
var item = array.FirstOrDefault(o =>
{
var prop = o[SearchPropertyName];
return prop != null && prop.Value<string>()
.Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase);
});
if (item == null)
{
Debug.LogMessage(LogEventLevel.Debug,"JSON Child[{0}] Array '{1}' '{2}={3}' not found: ", Key,
PathPrefix, SearchPropertyName, SearchPropertyValue);
this.LinkedToObject = false;
return false;
}
this.LinkedToObject = true;
ArrayIndex = array.IndexOf(item);
OnStringChange(string.Format("{0}[{1}]", PathPrefix, ArrayIndex), 0, JsonToSimplConstants.FullPathToArrayChange);
Debug.LogMessage(LogEventLevel.Debug, "JSON Child[{0}] Found array match at index {1}", Key, ArrayIndex);
return true;
}
catch (Exception e)
{
Debug.LogMessage(e, "JSON Child[{key}] Array '{pathPrefix}' lookup error: '{searchPropertyName}={searchPropertyValue}'", null, Key,
PathPrefix, SearchPropertyName, SearchPropertyValue, e);
}
}
else
{
Debug.LogMessage(LogEventLevel.Debug, "JSON Child[{0}] Path '{1}' is not an array", Key, PathPrefix);
}
return false;
}
}
}

View File

@@ -0,0 +1,422 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Base class for JSON objects
/// </summary>
public abstract class JsonToSimplChildObjectBase : IKeyed
{
/// <summary>
/// Notifies of bool change
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Notifies of ushort change
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UShortChange;
/// <summary>
/// Notifies of string change
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Delegate to get all values
/// </summary>
public SPlusValuesDelegate GetAllValuesDelegate { get; set; }
/// <summary>
/// Gets or sets the SetAllPathsDelegate
/// </summary>
public SPlusValuesDelegate SetAllPathsDelegate { get; set; }
/// <summary>
/// Gets or sets the Key
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// This will be prepended to all paths to allow path swapping or for more organized
/// sub-paths
/// </summary>
public string PathPrefix { get; protected set; }
/// <summary>
/// This is added to the end of all paths
/// </summary>
public string PathSuffix { get; protected set; }
/// <summary>
/// Gets or sets the LinkedToObject
/// </summary>
public bool LinkedToObject { get; protected set; }
/// <summary>
/// Reference to Master instance
/// </summary>
protected JsonToSimplMaster Master;
/// <summary>
/// Paths to boolean values in JSON structure
/// </summary>
protected Dictionary<ushort, string> BoolPaths = new Dictionary<ushort, string>();
/// <summary>
/// Paths to numeric values in JSON structure
/// </summary>
protected Dictionary<ushort, string> UshortPaths = new Dictionary<ushort, string>();
/// <summary>
/// Paths to string values in JSON structure
/// </summary>
protected Dictionary<ushort, string> StringPaths = new Dictionary<ushort, string>();
/// <summary>
/// Call this before doing anything else
/// </summary>
/// <param name="masterUniqueId"></param>
/// <param name="key"></param>
/// <param name="pathPrefix"></param>
/// <param name="pathSuffix"></param>
public void Initialize(string masterUniqueId, string key, string pathPrefix, string pathSuffix)
{
Key = key;
PathPrefix = pathPrefix;
PathSuffix = pathSuffix;
Master = J2SGlobal.GetMasterByFile(masterUniqueId);
if (Master != null)
Master.AddChild(this);
else
Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
}
/// <summary>
/// Sets the path prefix for the object
/// </summary>
/// <param name="pathPrefix"></param>
/// <summary>
/// SetPathPrefix method
/// </summary>
public void SetPathPrefix(string pathPrefix)
{
PathPrefix = pathPrefix;
}
/// <summary>
/// Set the JPath to evaluate for a given bool out index.
/// </summary>
public void SetBoolPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
BoolPaths[index] = path;
}
/// <summary>
/// SetUshortPath method
/// </summary>
public void SetUshortPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
UshortPaths[index] = path;
}
/// <summary>
/// SetStringPath method
/// </summary>
public void SetStringPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
StringPaths[index] = path;
}
/// <summary>
/// ProcessAll method
/// </summary>
/// <inheritdoc />
public virtual void ProcessAll()
{
if (!LinkedToObject)
{
Debug.Console(1, this, "Not linked to object in file. Skipping");
return;
}
if (SetAllPathsDelegate == null)
{
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring ProcessAll");
return;
}
SetAllPathsDelegate();
foreach (var kvp in BoolPaths)
ProcessBoolPath(kvp.Key);
foreach (var kvp in UshortPaths)
ProcessUshortPath(kvp.Key);
foreach (var kvp in StringPaths)
ProcessStringPath(kvp.Key);
}
/// <summary>
/// Processes a bool property, converting to bool, firing off a BoolChange event
/// </summary>
void ProcessBoolPath(ushort index)
{
string response;
if (Process(BoolPaths[index], out response))
OnBoolChange(response.Equals("true", StringComparison.OrdinalIgnoreCase),
index, JsonToSimplConstants.BoolValueChange);
else { }
// OnBoolChange(false, index, JsonToSimplConstants.BoolValueChange);
}
// Processes the path to a ushort, converting to ushort if able, twos complement if necessary, firing off UshrtChange event
void ProcessUshortPath(ushort index) {
string response;
if (Process(UshortPaths[index], out response)) {
ushort val;
try { val = Convert.ToInt32(response) < 0 ? (ushort)(Convert.ToInt16(response) + 65536) : Convert.ToUInt16(response); }
catch { val = 0; }
OnUShortChange(val, index, JsonToSimplConstants.UshortValueChange);
}
else { }
// OnUShortChange(0, index, JsonToSimplConstants.UshortValueChange);
}
// Processes the path to a string property and fires of a StringChange event.
void ProcessStringPath(ushort index)
{
string response;
if (Process(StringPaths[index], out response))
OnStringChange(response, index, JsonToSimplConstants.StringValueChange);
else { }
// OnStringChange("", index, JsonToSimplConstants.StringValueChange);
}
/// <summary>
/// Processes the given path.
/// </summary>
/// <param name="path">JPath formatted path to the desired property</param>
/// <param name="response">The string value of the property, or a default value if it
/// doesn't exist</param>
/// <returns> This will return false in the case that EvaulateAllOnJsonChange
/// is false and the path does not evaluate to a property in the incoming JSON. </returns>
bool Process(string path, out string response)
{
path = GetFullPath(path);
Debug.Console(1, "JSON Child[{0}] Processing {1}", Key, path);
response = "";
if (Master == null)
{
Debug.Console(1, "JSONChild[{0}] cannot process without Master attached", Key);
return false;
}
if (Master.JsonObject != null && path != string.Empty)
{
bool isCount = false;
path = path.Trim();
if (path.EndsWith(".Count"))
{
path = path.Remove(path.Length - 6, 6);
isCount = true;
}
try // Catch a strange cast error on a bad path
{
var t = Master.JsonObject.SelectToken(path);
if (t != null)
{
// return the count of children objects - if any
if (isCount)
response = (t.HasValues ? t.Children().Count() : 0).ToString();
else
response = t.Value<string>();
Debug.Console(1, " ='{0}'", response);
return true;
}
}
catch
{
response = "";
}
}
// If the path isn't found, return this to determine whether to pass out the non-value or not.
return false;
}
//************************************************************************************************
// Save-related functions
/// <summary>
/// Called from Master to read inputs and update their values in master JObject
/// Callback should hit one of the following four methods
/// </summary>
public void UpdateInputsForMaster()
{
if (!LinkedToObject)
{
Debug.Console(1, this, "Not linked to object in file. Skipping");
return;
}
if (SetAllPathsDelegate == null)
{
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
return;
}
SetAllPathsDelegate();
var del = GetAllValuesDelegate;
if (del != null)
GetAllValuesDelegate();
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
/// <summary>
/// USetBoolValue method
/// </summary>
public void USetBoolValue(ushort key, ushort theValue)
{
SetBoolValue(key, theValue == 1);
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
/// <summary>
/// SetBoolValue method
/// </summary>
public void SetBoolValue(ushort key, bool theValue)
{
if (BoolPaths.ContainsKey(key))
SetValueOnMaster(BoolPaths[key], new JValue(theValue));
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
/// <summary>
/// SetUShortValue method
/// </summary>
public void SetUShortValue(ushort key, ushort theValue)
{
if (UshortPaths.ContainsKey(key))
SetValueOnMaster(UshortPaths[key], new JValue(theValue));
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
/// <summary>
/// SetStringValue method
/// </summary>
public void SetStringValue(ushort key, string theValue)
{
if (StringPaths.ContainsKey(key))
SetValueOnMaster(StringPaths[key], new JValue(theValue));
}
/// <summary>
///
/// </summary>
/// <param name="keyPath"></param>
/// <param name="valueToSave"></param>
/// <summary>
/// SetValueOnMaster method
/// </summary>
public void SetValueOnMaster(string keyPath, JValue valueToSave)
{
var path = GetFullPath(keyPath);
try
{
Debug.Console(1, "JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
//var token = Master.JsonObject.SelectToken(path);
//if (token != null) // The path exists in the file
Master.AddUnsavedValue(path, valueToSave);
}
catch (Exception e)
{
Debug.Console(1, "JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
}
}
/// <summary>
/// Called during Process(...) to get the path to a given property. By default,
/// returns PathPrefix+path+PathSuffix. Override to change the way path is built.
/// </summary>
protected virtual string GetFullPath(string path)
{
return (PathPrefix != null ? PathPrefix : "") +
path + (PathSuffix != null ? PathSuffix : "");
}
// Helpers for events
//******************************************************************************************
/// <summary>
/// Event helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
//******************************************************************************************
/// <summary>
/// Event helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUShortChange(ushort state, ushort index, ushort type)
{
var handler = UShortChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(state, type);
args.Index = index;
UShortChange(this, args);
}
}
/// <summary>
/// Event helper
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,292 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Represents a JSON file that can be read and written to
/// </summary>
public class JsonToSimplFileMaster : JsonToSimplMaster
{
/// <summary>
/// Sets the filepath as well as registers this with the Global.Masters list
/// </summary>
public string Filepath { get; private set; }
/// <summary>
/// Gets or sets the ActualFilePath
/// </summary>
public string ActualFilePath { get; private set; }
/// <summary>
/// Gets or sets the Filename
/// </summary>
public string Filename { get; private set; }
/// <summary>
///
/// </summary>
public string FilePathName { get; private set; }
/*****************************************************************************************/
/** Privates **/
// The JSON file in JObject form
// For gathering the incoming data
object StringBuilderLock = new object();
// To prevent multiple same-file access
static object FileLock = new object();
/*****************************************************************************************/
/// <summary>
/// SIMPL+ default constructor.
/// </summary>
public JsonToSimplFileMaster()
{
}
/// <summary>
/// Read, evaluate and udpate status
/// </summary>
public void EvaluateFile(string filepath)
{
try
{
OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange);
var dirSeparator = Path.DirectorySeparatorChar;
var dirSeparatorAlt = Path.AltDirectorySeparatorChar;
var series = CrestronEnvironment.ProgramCompatibility;
var is3Series = (eCrestronSeries.Series3 == (series & eCrestronSeries.Series3));
OnBoolChange(is3Series, 0,
JsonToSimplConstants.ProgramCompatibility3SeriesChange);
var is4Series = (eCrestronSeries.Series4 == (series & eCrestronSeries.Series4));
OnBoolChange(is4Series, 0,
JsonToSimplConstants.ProgramCompatibility4SeriesChange);
var isServer = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server;
OnBoolChange(isServer, 0,
JsonToSimplConstants.DevicePlatformValueChange);
// get the roomID
var roomId = Crestron.SimplSharp.InitialParametersClass.RoomId;
if (!string.IsNullOrEmpty(roomId))
{
OnStringChange(roomId, 0, JsonToSimplConstants.RoomIdChange);
}
// get the roomName
var roomName = Crestron.SimplSharp.InitialParametersClass.RoomName;
if (!string.IsNullOrEmpty(roomName))
{
OnStringChange(roomName, 0, JsonToSimplConstants.RoomNameChange);
}
var rootDirectory = Directory.GetApplicationRootDirectory();
OnStringChange(rootDirectory, 0, JsonToSimplConstants.RootDirectoryChange);
var splusPath = string.Empty;
if (Regex.IsMatch(filepath, @"user", RegexOptions.IgnoreCase))
{
if (is4Series)
splusPath = Regex.Replace(filepath, "user", "user", RegexOptions.IgnoreCase);
else if (isServer)
splusPath = Regex.Replace(filepath, "user", "User", RegexOptions.IgnoreCase);
else
splusPath = filepath;
}
filepath = splusPath.Replace(dirSeparatorAlt, dirSeparator);
Filepath = string.Format("{1}{0}{2}", dirSeparator, rootDirectory,
filepath.TrimStart(dirSeparator, dirSeparatorAlt));
OnStringChange(string.Format("Attempting to evaluate {0}", Filepath), 0, JsonToSimplConstants.StringValueChange);
if (string.IsNullOrEmpty(Filepath))
{
OnStringChange(string.Format("Cannot evaluate file. JSON file path not set"), 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine("Cannot evaluate file. JSON file path not set");
return;
}
// get file directory and name to search
var fileDirectory = Path.GetDirectoryName(Filepath);
var fileName = Path.GetFileName(Filepath);
OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName);
if (Directory.Exists(fileDirectory))
{
// get the directory info
var directoryInfo = new DirectoryInfo(fileDirectory);
// get the file to be read
var actualFile = directoryInfo.GetFiles(fileName).FirstOrDefault();
if (actualFile == null)
{
var msg = string.Format("JSON file not found: {0}", Filepath);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
return;
}
// \xSE\xR\PDT000-Template_Main_Config-Combined_DSP_v00.02.json
// \USER\PDT000-Template_Main_Config-Combined_DSP_v00.02.json
ActualFilePath = actualFile.FullName;
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Actual JSON file is {0}", ActualFilePath);
Filename = actualFile.Name;
OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange);
OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON Filename is {0}", Filename);
FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator);
OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange);
OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON File Path is {0}", FilePathName);
var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
JsonObject = JObject.Parse(json);
foreach (var child in Children)
child.ProcessAll();
OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange);
}
else
{
OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "'{0}' not found", fileDirectory);
}
}
catch (Exception e)
{
var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(stackTrace);
ErrorLog.Error(stackTrace);
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"></param>
/// <summary>
/// setDebugLevel method
/// </summary>
public void setDebugLevel(uint level)
{
Debug.SetDebugLevel(level);
}
/// <summary>
/// Saves the values to the file
/// </summary>
public override void Save()
{
// this code is duplicated in the other masters!!!!!!!!!!!!!
UnsavedValues = new Dictionary<string, JValue>();
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
return;
}
lock (FileLock)
{
Debug.Console(1, "Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
}
else // No token. Let's make one
{
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
// JContainer jpart = JsonObject;
// // walk down the path and find where it goes
//#warning Does not handle arrays.
// foreach (var part in path.Split('.'))
// {
// var openPos = part.IndexOf('[');
// if (openPos > -1)
// {
// openPos++; // move to number
// var closePos = part.IndexOf(']');
// var arrayName = part.Substring(0, openPos - 1); // get the name
// var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos));
// // Check if the array itself exists and add the item if so
// if (jpart[arrayName] != null)
// {
// var arrayObj = jpart[arrayName] as JArray;
// var item = arrayObj[index];
// if (item == null)
// arrayObj.Add(new JObject());
// }
// Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW");
// continue;
// }
// // Build the
// if (jpart[part] == null)
// jpart.Add(new JProperty(part, new JObject()));
// jpart = jpart[part] as JContainer;
// }
// jpart.Replace(UnsavedValues[path]);
}
}
using (StreamWriter sw = new StreamWriter(ActualFilePath))
{
try
{
sw.Write(JsonObject.ToString());
sw.Flush();
}
catch (Exception e)
{
string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err);
ErrorLog.Warn(err);
return;
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Represents a JsonToSimplFixedPathObject
/// </summary>
public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase
{
/// <summary>
/// Constructor
/// </summary>
public JsonToSimplFixedPathObject()
{
this.LinkedToObject = true;
}
}
}

View File

@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Represents a JsonToSimplGenericMaster
/// </summary>
public class JsonToSimplGenericMaster : JsonToSimplMaster
{
/*****************************************************************************************/
/** Privates **/
// The JSON file in JObject form
// For gathering the incoming data
object StringBuilderLock = new object();
// To prevent multiple same-file access
static object WriteLock = new object();
/// <summary>
/// Gets or sets the SaveCallback
/// </summary>
public Action<string> SaveCallback { get; set; }
/*****************************************************************************************/
/// <summary>
/// SIMPL+ default constructor.
/// </summary>
public JsonToSimplGenericMaster()
{
}
/// <summary>
/// Loads in JSON and triggers evaluation on all children
/// </summary>
/// <param name="json"></param>
public void LoadWithJson(string json)
{
OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange);
try
{
JsonObject = JObject.Parse(json);
foreach (var child in Children)
child.ProcessAll();
OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange);
}
catch (Exception e)
{
var msg = string.Format("JSON parsing failed:\r{0}", e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
}
}
/// <summary>
/// Loads JSON into JsonObject, but does not trigger evaluation by children
/// </summary>
/// <param name="json"></param>
/// <summary>
/// SetJsonWithoutEvaluating method
/// </summary>
public void SetJsonWithoutEvaluating(string json)
{
try
{
JsonObject = JObject.Parse(json);
}
catch (Exception e)
{
Debug.Console(0, this, "JSON parsing failed:\r{0}", e);
}
}
/// <summary>
/// Save method
/// </summary>
/// <inheritdoc />
public override void Save()
{
// this code is duplicated in the other masters!!!!!!!!!!!!!
UnsavedValues = new Dictionary<string, JValue>();
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, this, "Master. checking child [{0}] for updates to save", child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, this, "Master. No updated values to save. Skipping");
return;
}
lock (WriteLock)
{
Debug.Console(1, this, "Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, this, "Master Updating '{0}'", path);
}
else // No token. Let's make one
{
Debug.Console(1, "Master Cannot write value onto missing property: '{0}'", path);
}
}
}
if (SaveCallback != null)
SaveCallback(JsonObject.ToString());
else
Debug.Console(0, this, "WARNING: No save callback defined.");
}
}
}

Some files were not shown because too many files have changed in this diff Show More