Compare commits

...

159 commits

Author SHA1 Message Date
equinoy
8c908c80e2 feat: add IHasMultiviewControl interface for multiview enable and layout feedback
Exposes SetMultiviewEnabled plus MultiviewEnabled/MultiviewLayout feedbacks on an
Essentials Core interface so room plugins can drive a multiview/UVC compositor
(e.g. StreamSync) without a compile-time dependency on the hardware plugin. Layout
selection remains on IHasScreensWithLayouts.ApplyLayout.
2026-08-20 14:31:29 -05:00
Neil Dorin
da2a09f7cb feat: add INetworkSwitchVlanManager and INetworkSwitchPoeManager interfaces for VLAN and PoE control 2026-08-13 12:06:13 -06:00
Neil Dorin
2588b81f0d feat: add error handling and logging to SendAtcFullMessageObject method in IDialerCallStatusMessenger 2026-08-12 10:59:01 -06:00
Neil Dorin
7fca4d74bf feat: add IAnnotationStream interface for annotation stream functionality 2026-08-11 14:16:56 -06:00
Neil Dorin
77e6ba7f36 feat: make IsInCall property virtual in VideoCodecBase class 2026-08-10 22:00:22 -06:00
Neil Dorin
61b5ca6297 feat: add Mobile Control documentation and update table of contents 2026-07-23 22:16:55 -06:00
Neil Dorin
5a183e7333 (force-patch): Updates devtools version 2026-07-23 13:00:36 -06:00
Neil Dorin
5e0a4c04ad feat: add missing using directive for System in IDtmfDecode interface 2026-07-23 08:54:53 -06:00
Neil Dorin
a782651e4d feat: enhance documentation for IDtmfDecode interface and DtmfReceivedEventArgs class 2026-07-23 08:49:11 -06:00
Neil Dorin
dac5325ecf feat: add IDtmfDecode interface and DtmfReceivedEventArgs class for DTMF digit handling 2026-07-23 08:47:57 -06:00
Neil Dorin
56208342c9 feat: implement deployment of mcUserApp zip files with cleanup and extraction 2026-07-22 15:36:15 -06:00
Neil Dorin
830e207ea7 fix: correct device key reference in LevelControl property 2026-07-22 11:43:50 -06:00
Neil Dorin
4252e9c8e6 feat: add thread safety to error timer management in StatusMonitorBase 2026-07-22 09:04:43 -06:00
Neil Dorin
42b358862f feat: add DisableEcho property to GenericSshClient and integrate with CommFactory 2026-07-21 15:16:54 -06:00
Neil Dorin
9656e3c408 feat: add audio input port to GenericSink for enhanced routing capabilities 2026-07-21 13:19:25 -06:00
Neil Dorin
f971a3adff feat: add IDspPresetsDirectRecall interface to define preset recall functionality 2026-07-21 11:21:22 -06:00
Neil Dorin
6852f47ce2 feat: add multiview layout support with new interface and state classes 2026-07-15 11:35:51 -06:00
Neil Dorin
f05a25c3e2 feat: update RoutingFeedbackManager to use ConcurrentDictionary for update timers to ensure thread safety 2026-07-14 21:47:52 -06:00
Neil Dorin
079f2beb8b feat: enhance routing graph handling for tile-sink children and add RoutingGraphHelpers utility class 2026-07-14 19:34:14 -06:00
Neil Dorin
7d1e0432cb feat: add IRoutingSinkWithLayouts interface to support layouts with multiple tile sinks 2026-07-13 12:26:57 -06:00
Neil Dorin
21995db550 feat: refactor FeedbackCollection and RoutingPortCollection to enforce unique keys and add AddRange method 2026-07-13 10:40:56 -06:00
Neil Dorin
dddb67317a
Merge pull request #1441 from PepperDash/feature/package-manifest-v3-routing-backport
Backport PR #1439: packageManifest CWS endpoint + v1/v2 config merge fix
2026-07-10 21:58:45 -06:00
Neil Dorin
3f8b97bff7 feat: add IHasDynamicMultiviewLayout interface and MultiviewParticipantSource class for dynamic layout support 2026-07-10 21:48:01 -06:00
jkdevito
4a366a5a51 fix: guard against empty Version in configured-but-not-loaded package passthrough
PopulatePackages' "configured but not currently loaded" pass-through could
still emit PackageVersion entries with a null/empty Version, reintroducing
the schema issue the loaded-reflection branch's own guard already prevents
(downstream parsers drop entries whose version isn't a string).
2026-07-10 13:03:28 -05:00
jkdevito
4631d3a537 refactor: replace client-reported UI app version tracking with disk-based check
Remove ConnectedClientVersionInfo and the per-client version dictionary in
MobileControlSystemController, which grew unbounded and depended on a UI
client connecting and self-reporting its APP_VERSION.

Add TouchpanelWrapperAppVersionChecker, which instead scans the deployed
mcUserApp files on disk for the configured versions.touchpanelWrapperApp
version string. This works without any client connected and doesn't
accumulate state. Wired into the mobileinfo console command output.
2026-07-10 12:25:43 -05:00
Neil Dorin
f0b546f97d feat: add custom logo URL feedback for light and dark backgrounds 2026-07-10 10:05:49 -06:00
jkdevito
e7406dbead ci(force-patch): increment patch by force 2026-07-10 09:51:23 -05:00
jkdevito
2d8e42f566 style: use file-scoped namespace in GetPackageManifestRequestHandler
Matches the convention used by the other request handlers in this
folder (ReportVersionsRequestHandler.cs, AppDebugRequestHandler.cs,
etc). Addresses PR #1441 review comment.
2026-07-10 09:37:29 -05:00
jkdevito
f0025ce56c Backport PR #1439: packageManifest CWS endpoint + v1/v2 config merge fix
Backports PepperDash/Essentials#1439 (merged to main at 6ceddcf5) onto
dev/v3-routing (v3.0.0-dev-v3-routing.40):

- New GET /packageManifest CWS route reporting Essentials/plugin/touchpanel
  UI package version info (GetPackageManifestRequestHandler).
- EssentialsConfig.Versions now round-trips via [JsonProperty("versions")].
- ConfigReader now detects v1 vs v2 config by presence of system/template
  nodes (previously any versions node caused the v1->v2 merge to be
  skipped), and re-applies the versions node after merge since
  PortalConfigReader.MergeConfigs does not carry it forward.
- Mobile control: track and validate UI client app versions
  (ConnectedClientVersionInfo, MobileControlSystemController changes merged
  cleanly from the PR).
- Directory.Build.props: adds PackageId AssemblyMetadata item (kept this
  branch's 3.0.0-local version).

Conflict resolution notes:
- This branch had already introduced its own AppVersion type (Version +
  RepoUrl only) for TouchpanelWrapperApp/UserInterfaces. Replaced it with
  the PR's unified NugetVersion (adds PackageId/RepoUrl/Name, all
  NullValueHandling.Ignore) to match main's schema, since AppVersion had
  no other references in the codebase.
- ConfigReader.LoadConfig on this branch had already fixed a double
  Stream.ReadToEnd() bug (reading fileContents once); kept that fix and
  layered the PR's v1/v2 detection fix + versions-node preservation on
  top of it, plus this branch's null-ConfigObject-after-merge guard.
- EssentialsWebApi.SetupRoutes: inserted the new packageManifest route
  into this branch's existing (longer) route list, which already
  includes routing-specific endpoints not present on main.

Builds clean (dotnet build on the full solution, 0 errors).

Not pushed - pending local hardware/config testing before push.
2026-07-10 09:37:29 -05:00
Neil Dorin
73bc50ef0d feat: update IP address retrieval logic to handle invalid values 2026-07-09 11:51:53 -06:00
Neil Dorin
f68e033850 feat(force-build): Rebuild with updated dev tools 2026-07-09 09:33:59 -06:00
Neil Dorin
d5432e489d feat: add USB camera input ports for enhanced multi-camera routing support 2026-07-08 15:51:53 -06:00
Neil Dorin
94837a60cf feat: update debounce timer logic to handle multiple input ports for a single sink 2026-07-08 15:37:14 -06:00
Neil Dorin
4ab4ee1680 feat: update directory contact name from "Corporate Bridge" to "Overflow" 2026-07-08 14:41:04 -06:00
Neil Dorin
122881100a feat: enhance MockVC to support participant management and add HDMI output port 2026-07-08 14:39:50 -06:00
Neil Dorin
34c82588f0 feat: add additional HDMI output ports to MockVC for enhanced routing capabilities 2026-07-07 15:10:08 -06:00
Neil Dorin
7de2aa7c2e feat: update directory contacts and enhance directory messaging to include current root directory 2026-07-07 14:57:23 -06:00
Neil Dorin
f3f7755735 feat: refine messenger factory to prevent conflicting state shapes for IDialerCallStatus and ICodecCallControls 2026-07-02 17:21:06 -06:00
Neil Dorin
d771320fff feat: refactor DeviceJsonApi parameter conversion and add MockRoutingMidpoint with configuration properties 2026-07-02 15:43:07 -06:00
Neil Dorin
9f18275f81 feat: update RoutingFeedbackManager to map downstream sink input ports and optimize performance in route updates 2026-07-02 10:53:51 -06:00
Neil Dorin
fb4ba8ccfa feat: enhance routing functionality with additional parameters in RouteDescriptor and update RouteDescriptorCollection for change notifications 2026-07-01 17:14:32 -06:00
Neil Dorin
4bb2917b69 feat: add RouteDescriptorCollectionChanged event to notify changes in route descriptors 2026-06-30 17:00:26 -06:00
Neil Dorin
aae41b5947 feat: refactor messaging interfaces and remove subscription support from IMobileControlMessengerWithSubscriptions 2026-06-26 15:53:51 -06:00
Neil Dorin
b945ecb470 feat: add Dial method with password support and implement ICodecCallControlsMessenger for call control messaging 2026-06-25 20:30:48 -06:00
Neil Dorin
8748157362 feat: enhance IHasDirectoryMessenger with new actions and state messaging for directory management
feat: update MobileControlConfig to enable messenger subscriptions by default
fix: improve HandleBatchDeviceFullStatus to support granular device status requests and enhance error handling
2026-06-25 11:46:27 -06:00
Neil Dorin
77c78af74b feat: add VideoMuteFb property to Participant class for video mute feedback 2026-06-24 22:29:31 -06:00
Neil Dorin
f7c3ed4b8b feat: add mobile control messengers for participant audio and video mute functionality 2026-06-24 22:28:40 -06:00
Neil Dorin
f4fe8eff90 feat: enhance routing and messaging logic with improved source handling and batch processing capabilities 2026-06-24 20:10:52 -06:00
Neil Dorin
a732c5e08e feat: enhance WebSocket services with HTTP GET handler for certificate confirmation and increase timeout for port forwarding 2026-06-23 16:08:42 -06:00
Neil Dorin
a18f7800d1 fix: improve port forwarding logic and error handling in RoutingFeedback session 2026-06-23 15:33:46 -06:00
Neil Dorin
00c7128778 Merge remote-tracking branch 'origin/fix/mobile-static-asset-mime-types' into dev/v3-routing 2026-06-23 15:33:34 -06:00
equinoy
b7b4b56cec fix(mobile-control): serve correct MIME types for app static assets
Use query-stripped file path for extension detection
Add content types for svg, png, jpg/jpeg, gif, webp, and ico
Keep existing html/js/css/json handling unchanged
2026-06-23 15:32:16 -05:00
Neil Dorin
a1d168c4ed feat: add HTTP GET handler to RoutingFeedbackWebSocket for client certificate confirmation 2026-06-22 22:48:16 -06:00
Neil Dorin
b65f895e06 feat: implement Routing Feedback WebSocket service and associated request handlers 2026-06-22 21:30:47 -06:00
Neil Dorin
be545bbfa4 fix: format constructor for IDeviceInfoProviderMessenger for improved readability 2026-06-18 16:41:47 -06:00
Neil Dorin
846fd711c5 fix: update namespace for ICourtSession interface to correct location 2026-06-18 10:57:17 -06:00
Neil Dorin
1588cc4667 feat: add ICourtSession interface for court session management and recording control 2026-06-18 10:37:25 -06:00
Neil Dorin
747b595b59 feat: add actions to accept and reject calls in IHasDialerMessenger 2026-06-17 22:25:38 -06:00
Neil Dorin
cff46532be feat: update Dial method to limit active calls based on maxCalls 2026-06-16 18:42:14 -06:00
Neil Dorin
0098673a5e feat: enhance audio codec phonebook functionality and messaging integration 2026-06-16 16:14:07 -06:00
Neil Dorin
c8faa19835 Merge remote-tracking branch 'origin/dev/v3-routing' into dev/v3-routing 2026-06-16 08:58:01 -06:00
Neil Dorin
fb0afc8c41 feat: implement DeviceActionMessenger for executing device actions via JSON API 2026-06-16 08:57:31 -06:00
Erik Meyer
27b3d9cf58 feat: updates IAudioCodecPhonebook interface 2026-06-15 13:46:32 -04:00
Neil Dorin
4403e91b79 fix: correct UserInterfaces type and initialize TouchpanelWrapperApp in VersionData constructor 2026-06-12 11:17:11 -06:00
Neil Dorin
1b2f86eb43 feat: add UserInterfaces and TouchpanelWrapperApp properties to VersionData class 2026-06-12 10:53:04 -06:00
Neil Dorin
2dd99da67f feat: implement IAudioCodecPhonebookMessenger for phonebook messaging functionality 2026-06-11 16:55:03 -06:00
Neil Dorin
22f5558e39 feat: add IAudioCodecInfoMessenger for messaging bridge implementation 2026-06-11 16:44:41 -06:00
Neil Dorin
c306fd717e refactor: remove unused GetInterfaces method and streamline constructor initialization 2026-06-10 22:58:57 -06:00
anthony-lopez-pd
72c948e760 fix: disambiguate DirectorySelectRow (101) and feedback (104) descriptions
Both analog 101 (FromSIMPL select) and analog 104 (ToSIMPL feedback echo)
were described "Directory Select Row and Feedback", implying the feedback
was on 101. Clarified: 101 = "Directory Select Row"; 104 = "Directory
Selected Row FB".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:16:23 -04:00
anthony-lopez-pd
dff50ea544 fix: correct PhoneHookState (digital 72) feedback description
PhoneHookState (ToSIMPL) was labeled "Dial Phone" — a copy of the
DialPhone input description on the same join. Changed to "Phone hook
state FB" to accurately describe the feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:58:30 -04:00
anthony-lopez-pd
4058040330 fix: remove dead DirectoryLineSelected join; fix SelfviewPositionFb description
DirectoryLineSelected (digital 101, FromSIMPL) had no handler anywhere in
VideoCodecBase.cs — directory selection is driven by analog 101
(DirectorySelectRow). DirectoryEntryIsContact (ToSIMPL) already documents
the output side of that join. Removed the dead inbound entry.

SelfviewPositionFb (serial 211) description "advance selfview position"
read like a command; updated to "Current selfview PIP position string FB"
to accurately reflect that it is a ToSIMPL feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:20:58 -04:00
anthony-lopez-pd
f2f4662c8d fix: remove dead SelectLayout and AvailableLayoutsFb serial-142 joins
Both entries were forward-looking stubs: IHasCodecLayouts has no
string-based layout-select method, so neither join was ever wired in
LinkVideoCodecCameraLayoutsToApi. String-based layout selection is
Zoom-specific (eLayoutStyle enum) and lives on serial 215 in the EPI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:45:39 -04:00
anthony-lopez-pd
b2ddce2cc6 fix(videocodec): correct join-map direction labels in VideoCodecControllerJoinMap
Capability metadata only — no wiring/behavior change. Verified against the actual
LinkToApi wiring and a SIMPL EISC bridge signal dump:

- CameraPresetSelect (a121), FarEndPresetSelect (a122): ToSIMPL -> ToFromSIMPL
  (driven as input via SetUShortSigAction, also fed back)
- SelectedContactMethodCount (a102): FromSIMPL -> ToSIMPL ("Reports..." feedback)
- CurrentDialString (s1): ToSIMPL -> ToFromSIMPL (read as the dial input by ManualDial,
  echoed back as FB)
- Participant audio/video/pin toggles (d501/801/1101): ToSIMPL -> FromSIMPL (commands)

Makes the generated join-map docs accurate for every video-codec EPI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:30:23 -04:00
Neil Dorin
041bb5c50e feat: Add null or empty check for AppUrl in SetAppUrl method 2026-06-08 16:57:34 -06:00
Neil Dorin
a27f604660 feat: Add IHasCameraMuteMessenger and IHasCameraPresetsMessenger for camera control messaging 2026-06-08 14:33:47 -06:00
anthony-lopez-pd
48e64a291f feat(videocodec): add IsCohost to Participant model
Participant exposed IsHost/IsMyself but no co-host role. Several SDKs
(e.g. the ZRC SDK behind epi-zoom-room) report a distinct cohost; add an
IsCohost flag so codecs can surface the Me/Host/Cohost role the mobile
control UI expects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:02:23 -04:00
Neil Dorin
a90b5da61b feat: Extend IDspPresets interface to inherit from IKeyed 2026-06-04 17:24:45 -06:00
Neil Dorin
bb029221c0 feat: Add DevelopmentServerAddress property for direct communication and update URL handling in MobileControl classes 2026-06-03 17:00:43 -06:00
Neil Dorin
153922c3fe refactor: Remove commented-out Config properties from Device class and add spacing in EssentialsDevice class 2026-06-03 14:09:47 -06:00
Neil Dorin
a1617fbeb2 fix: Remove unnecessary message from NotImplementedException in DevicePropertiesConverter 2026-06-03 12:22:06 -06:00
anthony-lopez-pd
cd89cf1cc9 fix: Correct typo in LevelControlListItem class name 2026-06-02 09:55:27 -04:00
Neil Dorin
0244b0e585 feat: Add IBasicVolumeControlsMessenger and update MessengerFactoryRegistry for volume control integration 2026-05-27 19:04:32 -06:00
Neil Dorin
101d6c6e79 feat: Enhance token validation in MobileControlWebsocketServer to handle missing tokens 2026-05-20 13:30:42 -06:00
Neil Dorin
ec7db1b510 feat: Remove IRoutingSinkWithFeedbackMessenger class and its associated actions 2026-05-14 15:38:59 -06:00
Neil Dorin
f64b595fd7 feat: Refactor routing interfaces and feedback mechanisms
- Removed ITxRouting and ITxRoutingWithFeedback interfaces as they were redundant.
- Updated RouteDescriptor and RouteSwitchDescriptor to use new feedback interfaces.
- Modified RoutingFeedbackManager to utilize IRoutingSinkWithFeedback and IRoutingMidpointWithFeedback.
- Adjusted GenericAudioOut, BlueJeansPc, and other device classes to implement new feedback interfaces.
- Introduced new messenger classes for IRoutingSinkWithFeedback and IRoutingMidpointWithFeedback.
- Cleaned up unused messenger interfaces and consolidated routing logic.
- Updated MobileControlEssentialsRoomBridge to remove dependency on IHasCurrentSourceInfoChange.
2026-05-14 14:36:30 -06:00
Neil Dorin
b9dcec587d feat: Add new status actions for call history, codec cameras, room presets, content sharing, room event schedule, shutdown prompt, and tech password messengers 2026-05-13 17:00:23 -06:00
Neil Dorin
91aa0efa5f feat: Add new messengers for various device interfaces and remove VideoCodecBaseMessenger
- Implemented IHasMeetingInfoMessenger to handle meeting information updates.
- Created IHasReadyMessenger for devices indicating readiness status.
- Added IHasStandbyModeMessenger to manage standby mode actions.
- Introduced IPrivacyMessenger for privacy mode control.
- Developed IVideoCodecInfoMessenger to provide codec information.
- Removed the obsolete VideoCodecBaseMessenger class.
- Updated MessengerFactoryRegistry to register new messengers for IHasReady, IHasMeetingInfo, IHasStartMeeting, IHasStandbyMode, IPrivacy, and IVideoCodecInfo interfaces.

Co-authored-by: Copilot <copilot@github.com>
2026-05-08 12:10:56 -06:00
Neil Dorin
b41c30cdd0 feat: Add full status actions to IHasCodecSelfView and IHasDirectory messengers 2026-05-07 20:55:44 -06:00
Neil Dorin
ab4a243ffb feat: Add messengers for codec functionalities and directory management
- Implemented IHasCodecCamerasMessenger to handle camera selection and control actions.
- Created IHasCodecLayoutsMessenger for managing codec layouts.
- Developed IHasCodecSelfViewMessenger to manage self-view functionality.
- Added IHasDirectoryMessenger for directory operations including fetching and searching.
- Introduced IHasFarEndContentStatusMessenger to report far-end content status.
- Implemented IPasswordPromptMessenger for handling password prompts.
- Updated VideoCodecBaseMessenger to remove redundant directory and call history handling.
- Registered new messenger types in MessengerFactoryRegistry.
- Added necessary using directives in ControlSystem.cs for UC functionalities.

Co-authored-by: Copilot <copilot@github.com>
2026-05-07 20:51:14 -06:00
Neil Dorin
e19e69d5c0 refactor: Update solution configuration for Debug and Release builds across multiple platforms to ensure all tests run 2026-05-07 14:04:14 -06:00
Neil Dorin
bad08fa6db refactor: Extract portal URL display logic into a separate method for improved readability 2026-05-07 12:08:26 -06:00
Neil Dorin
9a385eef96 refactor: Rename LocalConfigPresent to LocalConfigPresentBanner and enhance debug logging in ControlSystem 2026-05-07 11:00:49 -06:00
Neil Dorin
ff1393b768 refactor: Simplify namespace declaration and formatting in IDisplayCurrentInput interface 2026-05-06 17:13:48 -06:00
Neil Dorin
f80aa95649 Refactor audio codec and display messaging interfaces
- Introduced IDialerCallStatus interface to streamline call status handling in audio codecs.
- Updated AudioCodecBase to implement IDialerCallStatus, providing a common structure for call status events.
- Added IDisplayCurrentInput interface for display devices to expose current input information.
- Created corresponding messenger classes for IDialerCallStatus and IDisplayCurrentInput to facilitate communication.
- Implemented CameraControlMessenger for camera devices to manage camera controls and presets.
- Added IWarmingCoolingMessenger for warming and cooling devices to handle status updates.
- Updated MessengerFactoryRegistry to register new messenger classes and ensure proper device messaging.
- Cleaned up CameraBase and removed unnecessary comments and whitespace in various files.

Co-authored-by: Copilot <copilot@github.com>
2026-05-06 17:13:02 -06:00
Neil Dorin
dfc1d2bb21 refactor: Rename DisplayBaseMessenger to IRoutingSinkWithSwitchingMessenger and update related references 2026-05-06 13:54:10 -06:00
Neil Dorin
bc70aa629c Enhance logging and mobile control functionality
- Added error log level change notification in Debug.cs.
- Updated appdebug command usage to include sink options and timeout parameters in Debug.cs.
- Implemented AddDefaultMessengersForDevice method in IMobileControl interface for better messenger registration.
- Modified EssentialsDevice to create mobile control messengers based on available interfaces.
- Introduced MessengerFactoryRegistry to streamline messenger creation for various device types.
- Refactored MobileControlSystemController to utilize the new messenger registration system and removed redundant setup methods.

Co-authored-by: Copilot <copilot@github.com>
2026-05-06 13:44:09 -06:00
Neil Dorin
b0525b0f97 Merge remote-tracking branch 'origin/dev/v3-testing' into dev/v3-testing 2026-05-01 14:44:52 -06:00
Neil Dorin
2e1bb8b286 feat: Enhance WebSocket URL generation to support dual-stack environments by using CSLAN IP if available 2026-05-01 14:44:07 -06:00
Andrew Welker
89a1b662b0 fix: return early if config devices are null, with an appropriate log message 2026-04-24 10:19:18 -05:00
Neil Dorin
dc656bddf1 feat: Implement login request handler with token generation and error handling 2026-04-16 21:31:42 -06:00
Neil Dorin
d1501d2dab feat: Enhance plugin version check with error handling and logging 2026-04-13 22:35:47 -06:00
Neil Dorin
af7411f43b fix: Correctly retrieve package ID from assembly metadata for version checks 2026-04-13 22:03:07 -06:00
Neil Dorin
7d60c41dbc feat: Rename LoadConfig2 to LoadConfig and implement plugin version checks against configuration 2026-04-13 21:50:24 -06:00
Neil Dorin
5f4a35d425 feat: Enhance initialization error handling and add console command for reporting exceptions 2026-04-13 21:11:35 -06:00
Neil Dorin
71cf7ef4df feat: Add console command to report initialization exceptions for improved diagnostics 2026-04-13 21:01:22 -06:00
Neil Dorin
ac3b73cf32 feat: Implement initialization exceptions handling and add API endpoint for retrieval 2026-04-13 20:57:15 -06:00
Neil Dorin
554916a74a refactor: Remove commented generic interface from IRouting for clarity 2026-04-13 20:57:10 -06:00
Neil Dorin
3d8ee22f45 feat: Refactor routing signal types and enhance web API for routing devices
- Renamed `UsbOutput` to `Usb` in `eRoutingSignalType.cs`.
- Removed unused `UsbInput` and `SecondaryAudio` signal types.
- Added new HTTP route for retrieving routing devices and tielines in `EssentialsWebApi.cs`.
- Implemented `GetRoutingDevicesAndTieLinesHandler` to handle requests for routing devices and tielines, including detailed port information.
- Updated `GenericSink` to remove `SecondaryAudio` from input port signal types.
- Created `ICurrentSourcesMessenger` to manage current source information and status updates.
- Introduced `IDeviceInfoProviderMessenger` for device information communication with debounce functionality.
- Updated `MobileControlSystemController` to use new messenger classes for current sources and device info.
- Added console commands for listing tielines and visualizing routes in `ControlSystem.cs`.
- Implemented methods for visualizing routes and current routes, including filtering options.
2026-04-10 09:49:16 -06:00
Neil Dorin
ec3e2bbc0b fix: Remove 'Debug 4.7.2' configuration from project files for consistency 2026-04-09 14:19:33 -06:00
Neil Dorin
b85f1dab6b feat: Add asset loading functionality and related tests for improved resource management 2026-04-09 14:00:27 -06:00
Neil Dorin
4ed5e648c0 feat: Load assets during ControlSystem initialization for enhanced resource management 2026-04-08 17:00:16 -06:00
Neil Dorin
35d44f91e4 feat: Implement devTools zip file extraction and cleanup process for improved debugging support 2026-04-08 16:53:29 -06:00
Neil Dorin
fa9dc3a2b1 feat: Add devToolsVersion to build configuration for enhanced version control 2026-04-08 16:46:45 -06:00
Neil Dorin
7e3c62b303 fix: Update actions/checkout and actions/setup-dotnet versions for improved compatibility 2026-04-08 15:54:15 -06:00
Neil Dorin
884b2c2e6e feat: Enhance documentation for MicrophonePrivacyController and its factory with detailed summaries and parameter descriptions 2026-04-08 15:49:38 -06:00
Neil Dorin
daf9b4bda0 refactor: Change CustomActivate and Initialize methods to protected access in multiple classes for better inheritance control 2026-04-08 15:47:58 -06:00
Neil Dorin
e818c9ca03 fix: Downgrade setup-dotnet action version to v5 for compatibility 2026-04-08 15:24:13 -06:00
Neil Dorin
5dd6d18fcc feat: Update CI workflow and project files for improved testing and documentation generation 2026-04-08 15:23:49 -06:00
Neil Dorin
37961b9eac feat: Enhance device testing and environment handling with new interfaces and fakes 2026-04-08 13:21:15 -06:00
Neil Dorin
24df4e7a03 feat: Add unit tests and fakes for Crestron environment and data store
- Introduced `FakeCrestronEnvironment` and `FakeEthernetHelper` for testing purposes.
- Implemented `InMemoryCrestronDataStore` to facilitate unit tests for data storage.
- Created `DebugServiceTests` to validate the behavior of debug-related services.
- Added project files for unit tests targeting .NET 9.0 with necessary dependencies.
- Developed production adapters (`CrestronConsoleAdapter`, `CrestronDataStoreAdapter`, `CrestronEnvironmentAdapter`, `CrestronEthernetAdapter`) to interface with the Crestron SDK.
- Updated `Debug` class to utilize injected service abstractions instead of direct SDK calls.
- Enhanced `ControlSystem` initialization to register service adapters before usage.
2026-04-08 12:41:15 -06:00
Neil Dorin
bfb9838743 refactor: Rename logging level switch variables for clarity and add fallback handler to WebApiServer to allow for serving debug app 2026-04-08 11:51:10 -06:00
Neil Dorin
c98b48ff87 refactor: Clean up and streamline Debug and WebApiServer classes for improved readability and performance 2026-04-07 15:53:00 -06:00
Neil Dorin
5c26438a1c refactor: Remove DebugWebsocketSink.cs to streamline the codebase 2026-03-31 19:29:58 -06:00
Neil Dorin
7bec96e68b refactor: Remove obsolete interfaces and classes to streamline the codebase 2026-03-30 12:11:21 -06:00
Neil Dorin
7076eafc21 Refator: Refactor timer implementation across multiple classes to use System.Timers.Timer instead of CTimer for improved consistency and performance.
- Updated RelayControlledShade to utilize Timer for output pulsing.
- Refactored MockVC to replace CTimer with Timer for call status simulation.
- Modified VideoCodecBase to enhance documentation and improve feedback handling.
- Removed obsolete IHasCamerasMessenger and updated related classes to use IHasCamerasWithControls.
- Adjusted PressAndHoldHandler to implement Timer for button hold actions.
- Enhanced logging throughout MobileControl and RoomBridges for better debugging and information tracking.
- Cleaned up unnecessary comments and improved exception handling in various classes.
2026-03-30 11:44:15 -06:00
Neil Dorin
b4d53dbe0e refactor: routing interfaces and implementations to support current sources
- Updated ICurrentSources interface to use IRoutingSource instead of SourceListItem.
- Introduced CurrentSourcesChangedEventArgs for detailed event notifications.
- Modified IRoutingOutputs and IRoutingSource interfaces to include JSON properties for serialization.
- Enhanced IRoutingSink and related interfaces to implement ICurrentSources for better source management.
- Refactored RoutingFeedbackManager to utilize new current source handling.
- Updated GenericAudioOut, GenericSink, and BlueJeansPc classes to implement new current source logic.
- Adjusted MobileControl messengers to accommodate changes in current source handling.
- Removed deprecated destination handling in MobileControlEssentialsRoomBridge.
2026-03-26 13:49:23 -06:00
Neil Dorin
43a9661e08 Refactor: Removing unused classes that reference Crestron HTTP classes
- Removed the HttpLogoServer class and its related functionality from ControlSystem.
- Updated ControlSystem to eliminate references to the logo server, including initialization and device checks.
- Cleaned up unused variables and methods related to logo server handling.
2026-03-23 11:20:44 -06:00
Neil Dorin
7137945c94 refactor: Remove GenericRESTfulClient and GenericRESTfulConstants classes to streamline codebase 2026-03-12 13:56:18 -06:00
Neil Dorin
ac393c4885 refactor(force-patch): Remove obsolete GenericHttpClient class in favor of built-in HttpClient 2026-03-10 17:35:23 -06:00
Neil Dorin
346a5e9e57 refactor: Refactor DeviceManager and related classes to improve thread safety and performance
- Replaced CCriticalSection with lock statements in DeviceManager for better thread management.
- Updated AddDevice and RemoveDevice methods to use Monitor for locking.
- Enhanced event handling for device activation and registration.
- Modified FileIO class to utilize Task for asynchronous file operations instead of CrestronInvoke.
- Improved feedback mechanisms in FeedbackBase and SystemMonitorController using Task.Run.
- Refactored GenericQueue to remove Crestron threading dependencies and utilize System.Threading.
- Updated BlueJeansPc and VideoCodecBase classes to use Task for asynchronous operations.
- Cleaned up unnecessary critical sections and improved code documentation across various files.
2026-03-10 17:30:59 -06:00
Nick Genovese
426ef4ad6b fix: add error handling when saving debug settings 2026-03-10 00:35:18 -06:00
Nick Genovese
bcb65c7fa4 ci(force-patch): force build 2026-03-10 00:35:18 -06:00
Andrew Welker
c2ff65dce3 build: remove unneeded references from PD Core project 2026-03-10 00:35:18 -06:00
Andrew Welker
3feab2593d fix: use correct line endings for verbatim strings 2026-03-10 00:35:18 -06:00
Andrew Welker
5d90fafbd7 chore: remove duplication namespace declaration
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-10 00:31:20 -06:00
Andrew Welker
7f60dcb4cf chore: update local build version 2026-03-10 00:30:57 -06:00
Andrew Welker
574e4dfb0f feat: modify factory loading
Updating IDeviceFactory to resolve [FEATURE]-Refactor Plugin loading mechanism  #1065.
This change should be backwards-compatible with existing plugins that use the EssentialsPluginDeviceFactory<T> class,
as the interfaces are implemented by the various base classes.

In addition, the correct assembly name is now printed when a type is loaded.
2026-03-10 00:30:36 -06:00
Andrew Welker
76759d35cc feat: remove context file for storing values 2026-03-10 00:22:13 -06:00
Andrew Welker
3ece4f0b7b chore: move all files to file-scoped namespace 2026-03-10 00:21:06 -06:00
Andrew Welker
aaa5b0532b feat: modify plugin loading process 2026-03-06 10:34:53 -07:00
Andrew Welker
04c4557528 docs: update XML docs 2026-03-05 17:19:20 -07:00
Andrew Welker
9febbf2557 chore: fix issues related to remving crestron usings 2026-03-05 15:51:29 -07:00
Andrew Welker
eafade9569 docs: add XML comments 2026-03-05 15:43:42 -07:00
Andrew Welker
4e5b8f3897 wip: package updates 2026-03-05 14:54:54 -07:00
jtalborough
7bd3ccd54b fix: adjust installation steps for prerequisites based on environment detection 2026-03-05 14:23:45 -07:00
jtalborough
790b5a6fb9 fix: update condition for CPZ copy target and remove obsolete workflows 2026-03-05 14:23:45 -07:00
jtalborough
fbcd9c84da wip: address performance issues in plugin loading and versioning 2026-03-05 14:22:53 -07:00
jtalborough
4a2c9fe311 fix: update target framework to net8 and bump PepperDashCore version to 2.0.0-alpha-462
BREAKING CHANGE: Target Framework is now .NET 8:
2026-03-05 13:10:22 -07:00
jtalborough
bdd398e2e6 feat: implement WebSocket classes and update culture settings; bump PepperDashCore version 2026-03-05 13:10:22 -07:00
jtalborough
8be5481ac9 fix: update target frameworks and package references; change culture to InvariantCulture 2026-03-05 13:09:14 -07:00
jtalborough
dbab427d69 fix: update package references and clean up unused imports 2026-03-05 12:00:35 -07:00
Andrew Welker
db4f540710
Merge pull request #1394 from PepperDash/temp
merge temp into dev
2026-03-03 13:39:46 -05:00
Neil Dorin
ce8b08e312
Merge pull request #1371 from PepperDash/temp-to-dev 2025-12-23 12:14:03 -05:00
Andrew Welker
13e833b797
Merge pull request #1366 from PepperDash/main
Main -> Development
2025-12-09 15:48:09 -05:00
Andrew Welker
2be078da18
Merge pull request #1360 from PepperDash/temp-to-dev
Temp to dev
2025-11-25 13:42:11 -05:00
Neil Dorin
7330ae2e30
Merge pull request #1330 from PepperDash/temp-to-dev 2025-10-10 10:32:48 -04:00
Andrew Welker
a57dddba5e
Merge pull request #1341 from PepperDash/main
Update temp-to-dev branch
2025-10-10 10:31:28 -04:00
Neil Dorin
0bfec16622
Merge pull request #1328 from PepperDash/temp-to-dev
Temp to dev
2025-09-08 11:29:40 -06:00
Andrew Welker
94e7b8210f
Merge pull request #1323 from PepperDash/temp-to-dev
Temp to dev
2025-08-26 10:19:59 -04:00
671 changed files with 54295 additions and 55003 deletions

View file

@ -6,9 +6,13 @@ on:
- '**' - '**'
jobs: jobs:
runTests:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-run-tests.yml@main
secrets: inherit
getVersion: getVersion:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main
secrets: inherit secrets: inherit
needs: runTests
build-4Series: build-4Series:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main
secrets: inherit secrets: inherit
@ -20,3 +24,4 @@ jobs:
tag: ${{ needs.getVersion.outputs.tag }} tag: ${{ needs.getVersion.outputs.tag }}
channel: ${{ needs.getVersion.outputs.channel }} channel: ${{ needs.getVersion.outputs.channel }}
bypassPackageCheck: true bypassPackageCheck: true
devToolsVersion: ${{ vars.ESSENTIALSDEVTOOLSVERSION }}

View file

@ -26,9 +26,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Dotnet Setup - name: Dotnet Setup
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 8.x dotnet-version: 8.x

View file

@ -36,49 +36,189 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core.Tests", "src\PepperDash.Core.Tests\PepperDash.Core.Tests.csproj", "{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Tests", "src\PepperDash.Essentials.Tests\PepperDash.Essentials.Tests.csproj", "{841EE676-7784-4456-8F76-3697C6D432A6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
Debug 4.7.2|x64 = Debug 4.7.2|x64
Debug 4.7.2|x86 = Debug 4.7.2|x86
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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.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 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.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.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.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.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU {53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.Build.0 = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.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.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 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.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.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.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.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU {CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.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.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 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.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.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.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.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.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.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 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.ActiveCfg = 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}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.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.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.Build.0 = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.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.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 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.ActiveCfg = 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}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.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.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.Build.0 = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.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.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 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = 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}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.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.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.Build.0 = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.Build.0 = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.Build.0 = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.Build.0 = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.ActiveCfg = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.Build.0 = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.ActiveCfg = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.Build.0 = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.Build.0 = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.Build.0 = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.Build.0 = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.Build.0 = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.Build.0 = Release|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.ActiveCfg = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.Build.0 = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.ActiveCfg = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.Build.0 = Debug|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.Build.0 = Release|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.ActiveCfg = Release|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.Build.0 = Release|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.ActiveCfg = Release|Any CPU
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -90,6 +230,9 @@ Global
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206} {F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{B438694F-8FF7-464A-9EC8-10427374471F} = {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} {E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{841EE676-7784-4456-8F76-3697C6D432A6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3} SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}

View file

@ -32,7 +32,7 @@ If the `CustomActivate()` method is long, consider breaking it up into many smal
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! 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 ```cs
public override bool CustomActivate() protected override bool CustomActivate()
{ {
Debug.Console(0, this, "Final activation. Setting up actions and feedbacks"); Debug.Console(0, this, "Final activation. Setting up actions and feedbacks");
SetupFunctions(); SetupFunctions();
@ -52,7 +52,7 @@ We can see in the example below that during the `CustomActivate()` phase, we def
### **Example** ### **Example**
```cs ```cs
public override bool CustomActivate() protected override bool CustomActivate()
{ {
foreach (var i in Config.Inputs) foreach (var i in Config.Inputs)
{ {
@ -115,7 +115,7 @@ The main task that should be undertaken in the `Initialize()` method for any 3rd
### Example (from `PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.CiscoSparkCodec`) ### Example (from `PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.CiscoSparkCodec`)
```cs ```cs
public override void Initialize() protected override void Initialize()
{ {
var socket = Communication as ISocketStatus; var socket = Communication as ISocketStatus;
if (socket != null) if (socket != null)

View file

@ -12,12 +12,18 @@ When routes are to be executed, Essentials will use this connection graph to dec
### Classes Referenced ### Classes Referenced
* `PepperDash.Essentials.Core.Routing.IRoutingSource` * `PepperDash.Essentials.Core.IRoutingSource`
* `PepperDash.Essentials.Core.Routing.IRoutingOutputs` * `PepperDash.Essentials.Core.IRoutingOutputs`
* `PepperDash.Essentials.Core.Routing.IRoutingInputs` * `PepperDash.Essentials.Core.IRoutingInputs`
* `PepperDash.Essentials.Core.Routing.IRoutingInputsOutputs` * `PepperDash.Essentials.Core.IRoutingMidpoint`
* `PepperDash.Essentials.Core.Routing.IRoutingSinkNoSwitching` * `PepperDash.Essentials.Core.IRoutingMidpointWithFeedback`
* `PepperDash.Essentials.Core.Routing.IRoutingSinkWithSwitching` * `PepperDash.Essentials.Core.IRoutingSinkWithFeedback`
> **Note:** As of the v3 routing refactor, the older `IRoutingInputsOutputs`, `IRouting`,
> `IRoutingWithFeedback`, `IRoutingWithClear`, `IRoutingSink`, `IRoutingSinkWithInputPort`,
> `IRoutingSinkNoSwitching`, `IRoutingSinkWithSwitching`, and `IRoutingSinkWithSwitchingWithInputPort`
> interfaces have been removed and consolidated - see [Routing interfaces (v3)](#routing-interfaces-v3)
> below for the current mapping.
## Example system, a simple presentation system ## Example system, a simple presentation system
@ -56,11 +62,12 @@ Source devices in Essentials must implement `IRoutingOutputs` or `IRoutingSource
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. 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`. Passthrough midpoints (no active switching) must implement `IRoutingMidpoint`. Midpoints that actively
switch and report their current routes must implement `IRoutingMidpointWithFeedback`.
#### Sink #### 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`. 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 `IRoutingSinkWithFeedback`.
#### Tie-line #### Tie-line
@ -78,4 +85,95 @@ A tie-line is a logical representation of a physical cable connection between tw
### Interfaces ### Interfaces
Todo: Define Interfaces IRouting, IRoutingOutputs, IRoutingInputs #### Routing interfaces (v3)
The v3 routing refactor consolidated several older, overlapping interfaces into two feedback-aware
interfaces. If you're updating a device implemented against the pre-v3 API, use this table to find
its replacement:
| Old interface (pre-v3, removed) | Current interface |
| --- | --- |
| `IRoutingInputsOutputs` | `IRoutingMidpoint` |
| `IRouting`, `IRoutingWithFeedback`, `IRoutingWithClear` | `IRoutingMidpointWithFeedback` |
| `IRoutingSink`, `IRoutingSinkWithInputPort`, `IRoutingSinkWithSwitching`, `IRoutingSinkWithSwitchingWithInputPort` | `IRoutingSinkWithFeedback` |
**Base building-block interfaces** (unchanged):
* `IRoutingSource` - marker interface for a device that originates a signal path; extends `IRoutingOutputs`.
* `IRoutingOutputs` - exposes `RoutingPortCollection<RoutingOutputPort> OutputPorts`.
* `IRoutingInputs` - exposes `RoutingPortCollection<RoutingInputPort> InputPorts`.
**Midpoint interfaces:**
* `IRoutingMidpoint` - a passthrough device with both input and output ports but no active switching. Extends `IRoutingInputs` and `IRoutingOutputs`.
* `IRoutingMidpointWithFeedback` - a midpoint that actively switches and reports feedback. Extends `IRoutingMidpoint` and adds:
* `void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)`
* `void ClearRoute(object outputSelector, eRoutingSignalType signalType)`
* `List<RouteSwitchDescriptor> CurrentRoutes { get; }`
* `event RouteChangedEventHandler RouteChanged`
**Sink interface:**
* `IRoutingSinkWithFeedback` - a routing endpoint device that can switch inputs and provides feedback. Extends `IRoutingInputs`, `IKeyName`, and `ICurrentSources` (see below), and adds:
* `void ExecuteSwitch(object inputSelector)`
* `RoutingInputPort CurrentInputPort { get; }`
* `event InputChangedEventHandler InputChanged`
#### Current source tracking (`ICurrentSources`)
`ICurrentSources` is a new interface (implemented by `IRoutingSinkWithFeedback`) that reports which
source is actually feeding a sink right now, per signal type:
* `Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; }`
* `Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; }`
* `event EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged`
A new `RoutingFeedbackManager` subscribes to `RouteChanged` on every `IRoutingMidpointWithFeedback`
and `InputChanged` on every `IRoutingSinkWithFeedback` in the system, traces each change back through
the tie-line graph to the originating source (debounced ~500ms), and keeps each sink's
`ICurrentSources` state up to date automatically - developers no longer need to manually wire up
source tracking per device.
#### Multiview layout support
For decoders that can display several independently-routable tiles at once (e.g. a WyreStorm
NetworkHD-style multiview decoder), two additional interfaces are available:
* `IRoutingSinkWithLayouts` - extends `IRoutingSource` and exposes `Dictionary<int, IRoutingSinkWithFeedback> WindowTileSinks`, one per-tile "virtual sink" that can be routed independently like any other `IRoutingSinkWithFeedback`.
* `IRoutingSinkWithLayoutState` - extends `IRoutingSinkWithLayouts` and additionally reports the current canvas/tile geometry via `MultiviewLayoutState CurrentLayout { get; }` and `event EventHandler<MultiviewLayoutStateEventArgs> LayoutChanged`.
`MultiviewLayoutState` is a JSON-serializable, product-agnostic model of the canvas: `CanvasWidth`,
`CanvasHeight`, and a list of `MultiviewTileState` entries (`TileNumber`, `TileSinkKey`, `X`, `Y`,
`Width`, `Height`, `ZOrder`, `SourceDeviceKey`) - suitable for driving a routing diagram UI.
#### Real-time routing feedback over WebSocket
Essentials Core includes a built-in WebSocket service, `RoutingFeedbackWebsocket`
(`PepperDash.Essentials.Core.Web`), that broadcasts routing state changes to any connected client in
real time - this is what powers the live routing diagram in the
[Essentials Web Config App](https://github.com/PepperDash/essentials-web-config-app) and
[essentials-devtools](https://github.com/PepperDash/essentials-devtools).
A client starts (or reuses) a session by sending an HTTP `GET` to the `routingFeedbackSession` API
route (e.g. `https://[processor-ip]/cws/[appId]/api/routingFeedbackSession`), which starts the
WebSocket server on a random high port (if not already running), forwards that port on the CS LAN
adapter if present, and returns a JSON body with the connection URL(s):
```json
{ "url": "wss://[processor-ip]:[port]/routing/join/", "fallbackUrl": "wss://..." }
```
Once connected, the client receives a `snapshot` message describing every midpoint route, sink
input, and multiview layout currently active, followed by incremental update messages (each
debounced ~200ms) as things change:
| Message `type` | Sent when | Payload |
| --- | --- | --- |
| `snapshot` | On connect | `midpointRoutes`, `sinkRoutes`, `layouts` for the whole system |
| `midpointRouteChanged` | An `IRoutingMidpointWithFeedback`'s `RouteChanged` fires | `deviceKey`, `routes: [{ inputPortKey, outputPortKey, signalType }]` |
| `sinkInputChanged` | An `IRoutingSinkWithFeedback`'s `InputChanged` fires | `deviceKey`, `inputPortKey`, `sourceDeviceKey`, `signalType` |
| `layoutChanged` | An `IRoutingSinkWithLayoutState`'s `LayoutChanged` fires | `deviceKey`, `layout` (a `MultiviewLayoutState`) |
Per-tile sinks belonging to an `IRoutingSinkWithLayouts` device are reported to clients as qualified
input ports on their parent device's node, rather than as separate graph nodes, so a routing diagram
can render a multiview decoder as a single device with per-tile inputs.

View file

@ -0,0 +1,240 @@
# Mobile Control: architecture and client communication
## TL;DR
Mobile Control is a WebSocket-based messaging layer built into Essentials. Devices expose small
"messenger" classes that register JSON message paths (e.g. `/device/{key}/startMeeting`) with a
central `MobileControlSystemController`. Clients (typically a React app built on
[`@pepperdash/mobile-control-react-app-core`](https://github.com/PepperDash/mobile-control-react-app-core))
connect over WebSocket with a per-room token, send the same JSON envelope to invoke device methods,
and receive unsolicited state-update messages that a Redux store uses to keep device/room state in
sync in real time.
## Server-side architecture
### Projects involved
* `PepperDash.Essentials.MobileControl` - the `MobileControlSystemController` device, its
WebSocket server (`WebSocketServer/MobileControlWebsocketServer.cs`), room bridges, and
touchpanel controller.
* `PepperDash.Essentials.MobileControl.Messengers` (namespace `PepperDash.Essentials.AppServer.Messengers`) -
the `MessengerBase` class and the library of built-in messengers for common device interfaces
(`IHasStartMeetingMessenger`, `IHasMeetingInfoMessenger`, `ICommunicationMonitorMessenger`,
`IRoutingMidpointWithFeedbackMessenger`, etc.), plus the message envelope types.
* `PepperDash.Essentials.Core.DeviceTypeInterfaces` - the public contracts:
`IMobileControl`, `IMobileControlMessenger`, `IMobileControlMessengerWithSubscriptions`,
`IMobileControlRoomMessenger`, `IMobileControlAction`, `IMobileControlMessage`, and the
touchpanel-controller interfaces.
### Direct Server vs. Edge Server
Mobile Control supports two (non-exclusive) transport modes, configured on the `MobileControlConfig`
device:
* **Direct Server** - `MobileControlWebsocketServer` runs an HTTPS WebSocket server directly on the
processor hardware. Clients on the same network connect straight to the processor - lowest
latency, no external dependency. This is what `mobile-control-react-app-core`'s local dev flow
and the deployed `mcUserApp` React app use.
* **Edge Server** (API server / cloud gateway) - the processor instead makes an *outbound* WebSocket
connection to an external Mobile Control server, which relays messages between it and remote
clients. Useful when clients can't reach the processor directly (e.g. no local network access).
Both modes can be enabled simultaneously; outgoing messages are queued and sent down whichever
transport(s) are active.
### The WebSocket endpoint (Direct Server)
`MobileControlWebsocketServer` (`WebSocketServer/MobileControlWebsocketServer.cs`) hosts the
Direct Server:
* **Path**: `/mc/api/ui/join/` (`_wsPath`)
* **Port**: `50000 + <program slot number>` by default (e.g. program slot 2 → port `50002`), or a
custom port from config
* **User app**: static files are served from `/user/programX/mcUserApp` (`_appPath`) at the base
href `/mc/app` (`_userAppBaseHref`) - this is exactly the `mcUserApp` deployment directory used
when [deploying](../Get-started.md) a built React app.
### Connecting and authentication (tokens)
A client doesn't connect straight to the WebSocket - it first needs a **token** identifying which
room/UI-client slot it's joining:
1. **Get a token** - on the processor console, run:
```
mobileinfo:[programSlot]
```
This prints the Direct Server port, any existing UI-client tokens, and full connect URLs (e.g.
`http://[ip]:[port]/mc/app?token=[token]`). New tokens can also be added dynamically
(`mobileadduiclient:[programSlot] [roomKey]`) - see the console command's help text for details.
2. **Join a room** - the client calls `GET {apiPath}/ui/joinroom?token={token}` over HTTPS, which
validates the token and returns room data including a generated `clientId`.
3. **Open the WebSocket** - the client connects to
`wss://[processor-ip]:[port]/mc/api/ui/join/{token}?clientId={clientId}`.
Each connected client is tracked as a `UiClient`; disconnecting removes its subscriptions from
every messenger it was subscribed to.
### The message envelope
Every message sent in either direction is the same simple JSON envelope
(`MobileControlMessage`, namespace `PepperDash.Essentials.AppServer.Messengers`):
```json
{
"type": "/device/zoomRoom1/startMeeting",
"clientId": "abc-123",
"content": { "value": 30 }
}
```
* `type` - a path identifying the target device/room and action (client → server), or the
device/room whose state is being reported (server → client).
* `clientId` - which client sent the message, or (server → client) which single client a targeted
reply is meant for; omitted/ignored for broadcasts to all subscribers.
* `content` - a `JToken` payload. For simple values, `MobileControlSimpleContent<T>` wraps a single
`value` property (as in the example above); for state updates, `content` is the device's full
serialized state object.
### The messenger pattern
Each device that wants to be controllable/observable over Mobile Control gets a small **messenger**
class deriving from `MessengerBase` (`PepperDash.Essentials.AppServer.Messengers`):
```csharp
public class IHasStartMeetingMessenger : MessengerBase
{
// ...
protected override void RegisterActions()
{
AddAction("/fullStatus", (id, content) => SendFullStatus(id));
AddAction("/startMeeting", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<uint>>();
_startMeeting.StartMeeting(msg?.Value ?? _startMeeting.DefaultMeetingDurationMin);
});
AddAction("/leaveMeeting", (id, content) => _startMeeting.LeaveMeeting());
}
}
```
* The messenger is constructed with a **base `MessagePath`** (e.g. `/device/zoomRoom1`) and calls
`AddAction(subPath, handler)` for each supported action, relative to that base path.
* `RegisterWithAppServer(IMobileControl appServerController)` registers the messenger's
`HandleMessage` callback with the system controller via `IMobileControl.AddAction<T>(...)`.
* When an incoming message's `type` starts with the messenger's `MessagePath`, `HandleMessage`
strips that prefix (leaving e.g. `/startMeeting`) and dispatches to the matching registered
action. The sending client is automatically added to that messenger's subscriber list, so it
receives future unsolicited feedback from it.
* To push state out, messengers call `PostStatusMessage(DeviceStateMessageBase message, clientId)` -
omitting `clientId` broadcasts to every subscribed client; passing one targets a single client
(e.g. replying to a `/fullStatus` request).
### How devices opt in
`EssentialsDevice.CustomActivate()` calls a virtual `CreateMobileControlMessengers()` hook once all
devices have activated. A device (or a room) overrides this method to look up the `IMobileControl`
device and construct/register whichever messengers it needs:
```csharp
protected override void CreateMobileControlMessengers()
{
var mc = DeviceManager.AllDevices.OfType<IMobileControl>().FirstOrDefault();
if (mc == null) return;
var messenger = new IHasStartMeetingMessenger("zoomRoom1-startMeeting", "/device/zoomRoom1", this);
messenger.RegisterWithAppServer(mc);
}
```
Rooms typically implement `IMobileControlRoomMessenger` and register a room-level messenger at a
`/room/{roomKey}` path (via a `MobileControlBridgeBase`/`MobileControlEssentialsRoomBridge`), in
addition to any per-device messengers.
## Client-side architecture (React)
`@pepperdash/mobile-control-react-app-core` provides the client building blocks used by apps like
[beincourt-pv2-react-app](https://github.com/pepperdash-beincourt/beincourt-pv2-react-app):
### Connection lifecycle
The real work happens in a Redux middleware (`src/lib/store/middleware/websocketMiddleware.ts`);
`WebsocketProvider`/`WebsocketContext` are a thin, backward-compatible React Context wrapper around
it that simply dispatches Redux actions (`wsConnect`, `wsSendMessage`, `wsReconnect`, ...).
1. On mount, `wsConnect()` is dispatched.
2. The middleware reads `apiPath` from the app's local config (`_config.local.json`/
`_config.default.json`) and the connection `token` (from the URL's `?token=` query param).
3. It calls `GET {apiPath}/ui/joinroom?token={token}` to validate the token and get room data
(including a `clientId`).
4. It opens a WebSocket at `{apiPath with ws(s) scheme}/ui/join/{token}?clientId={clientId}`.
5. On specific close codes (e.g. `4000` user code changed, `4002` room combination changed) it stops
auto-reconnecting and surfaces an error; otherwise it automatically retries.
### Sending messages
```ts
const { sendMessage } = useWebsocketContext();
sendMessage('/device/zoomRoom1/startMeeting', { value: 30 });
```
`sendMessage` serializes `{ type, clientId, content }` (the same envelope described above) and
sends it over the open WebSocket.
### Receiving messages / state hydration
Incoming messages are routed by their `type` prefix:
| Prefix | Handling |
| --- | --- |
| `/system/*` | Internal system messages (user code, touchpanel key, room-combination/device-interface changes, initial sync complete, ...) |
| `/event/*` | Dispatched to any handlers registered via `addEventHandler(eventType, key, callback)` |
| `/room/*` | `dispatch(roomsActions.setRoomState(message))` |
| `/device/*` | `dispatch(devicesActions.setDeviceState(message))` |
Device/room state lives in Redux slices keyed by device/room key, so any component can read the
latest known state for a given key.
### The hook pattern
Each supported device interface has a small hook that combines reading state with calling actions,
e.g. `useIHasStartMeeting`:
```ts
export function useIHasStartMeeting(key: string): IHasStartMeetingReturn | undefined {
const { sendMessage } = useWebsocketContext();
const state = useGetDevice<IHasStartMeetingState>(key); // reads devices[key] from Redux
return useMemo(() => {
if (!state) return undefined;
const path = `/device/${key}`;
return {
state,
startMeeting: (durationMin?: number) => sendMessage(`${path}/startMeeting`, { value: durationMin }),
leaveMeeting: () => sendMessage(`${path}/leaveMeeting`, null),
};
}, [key, sendMessage, state]);
}
```
Components use these hooks to read live device state and call device actions without needing to
know anything about the WebSocket transport underneath.
## Full round-trip example: starting a meeting
1. **UI**: user clicks "Start Meeting"; component calls `startMeeting(30)` from `useIHasStartMeeting('zoomRoom1')`.
2. **Client → server**: `sendMessage('/device/zoomRoom1/startMeeting', { value: 30 })` sends
`{"type":"/device/zoomRoom1/startMeeting","clientId":"abc-123","content":{"value":30}}` over the WebSocket.
3. **Server routing**: the system controller matches `/device/zoomRoom1` against the registered
`IHasStartMeetingMessenger`'s `MessagePath`, strips the prefix to get `/startMeeting`, and invokes
its registered action, which calls `StartMeeting(30)` on the underlying device.
4. **Device fires feedback**: the device's meeting-started feedback fires.
5. **Server → clients**: the messenger calls `PostStatusMessage(...)`, broadcasting
`{"type":"/device/zoomRoom1","clientId":null,"content":{ ...state }}` to every client subscribed
to that messenger.
6. **Client receives**: the message's `type` starts with `/device/`, so
`dispatch(devicesActions.setDeviceState(message))` merges the new state into Redux under
`devices['zoomRoom1']`.
7. **UI updates**: `useIHasStartMeeting('zoomRoom1')`'s `state` reflects the new meeting info on the
next render.

View file

@ -38,6 +38,8 @@
href: technical-docs/Plugins.md href: technical-docs/Plugins.md
- name: Communication Basics - name: Communication Basics
href: technical-docs/Communication-Basics.md href: technical-docs/Communication-Basics.md
- name: Mobile Control
href: technical-docs/Mobile-Control.md
- name: Debugging - name: Debugging
href: technical-docs/Debugging.md href: technical-docs/Debugging.md
- name: Feedback Classes - name: Feedback Classes

7
runtimeconfig.json Normal file
View file

@ -0,0 +1,7 @@
{
"runtimeOptions": {
"configProperties": {
"System.Globalization.Invariant": false
}
}
}

View file

@ -1,11 +1,11 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>2.19.4-local</Version> <Version>3.0.0-local</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
<Authors>PepperDash Technology</Authors> <Authors>PepperDash Technology</Authors>
<Company>PepperDash Technology</Company> <Company>PepperDash Technology</Company>
<Product>PepperDash Essentials</Product> <Product>PepperDash Essentials</Product>
<Copyright>Copyright © 2025</Copyright> <Copyright>Copyright © 2026</Copyright>
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl> <RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>Crestron; 4series</PackageTags> <PackageTags>Crestron; 4series</PackageTags>
@ -20,4 +20,7 @@
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/> <None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
<None Include="..\..\README.md" Pack="true" PackagePath=""/> <None Include="..\..\README.md" Pack="true" PackagePath=""/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyMetadata Include="PackageId" Value="$(PackageId)" />
</ItemGroup>
</Project> </Project>

View file

@ -1,14 +1,14 @@
<Project> <Project>
<ItemGroup> <ItemGroup>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library'"> <None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != ''">
<Pack>true</Pack> <Pack>true</Pack>
<PackagePath>build;</PackagePath> <PackagePath>build;</PackagePath>
</None> </None>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program'"> <None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != '' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' )">
<Pack>true</Pack> <Pack>true</Pack>
<PackagePath>build;</PackagePath> <PackagePath>build;</PackagePath>
</None> </None>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary'"> <None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != ''">
<Pack>true</Pack> <Pack>true</Pack>
<PackagePath>build;</PackagePath> <PackagePath>build;</PackagePath>
</None> </None>

View file

@ -0,0 +1,36 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Allows pre-registration of Crestron service implementations before the <c>Debug</c>
/// static class initialises. Call <see cref="Register"/> from the composition root
/// (e.g. ControlSystem constructor) <em>before</em> any code touches <c>Debug.*</c>.
/// Test projects should call it with no-op / in-memory implementations so that the
/// <c>Debug</c> static constructor never tries to reach the real Crestron SDK.
/// </summary>
public static class DebugServiceRegistration
{
/// <summary>Gets the registered environment abstraction, or <c>null</c> if not registered.</summary>
public static ICrestronEnvironment? Environment { get; private set; }
/// <summary>Gets the registered console abstraction, or <c>null</c> if not registered.</summary>
public static ICrestronConsole? Console { get; private set; }
/// <summary>Gets the registered data-store abstraction, or <c>null</c> if not registered.</summary>
public static ICrestronDataStore? DataStore { get; private set; }
/// <summary>
/// Registers the service implementations that <c>Debug</c> will use when its
/// static constructor runs. Any parameter may be <c>null</c> to leave the
/// corresponding service unregistered (the <c>Debug</c> class will skip that
/// capability gracefully).
/// </summary>
public static void Register(
ICrestronEnvironment? environment,
ICrestronConsole? console,
ICrestronDataStore? dataStore)
{
Environment = environment;
Console = console;
DataStore = dataStore;
}
}

View file

@ -0,0 +1,100 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Mirrors Crestron's <c>eDevicePlatform</c> without requiring the Crestron SDK.
/// </summary>
public enum DevicePlatform
{
/// <summary>Hardware appliance (e.g. CP4, MC4).</summary>
Appliance,
/// <summary>Crestron Virtual Control / server runtime.</summary>
Server,
}
/// <summary>
/// Mirrors Crestron's <c>eRuntimeEnvironment</c>.
/// </summary>
public enum RuntimeEnvironment
{
/// <summary>SimplSharpPro program slot (hardware 4-series).</summary>
SimplSharpPro,
/// <summary>SimplSharp (older 3-series or server environments).</summary>
SimplSharp,
/// <summary>Any other environment — check for completeness.</summary>
Other,
}
/// <summary>
/// Mirrors Crestron's <c>ConsoleAccessLevelEnum</c>.
/// </summary>
public enum ConsoleAccessLevel
{
AccessAdministrator = 0,
AccessOperator = 1,
AccessProgrammer = 2,
}
/// <summary>
/// Mirrors Crestron's <c>eProgramStatusEventType</c>.
/// </summary>
public enum ProgramStatusEventType
{
Starting,
Stopping,
Paused,
Resumed,
}
/// <summary>
/// Mirrors the event type used by Crestron's <c>EthernetEventArgs</c>.
/// </summary>
public enum EthernetEventType
{
LinkDown = 0,
LinkUp = 1,
}
/// <summary>
/// Event args for Crestron ethernet link events.
/// </summary>
public class PepperDashEthernetEventArgs : EventArgs
{
public EthernetEventType EthernetEventType { get; }
public short EthernetAdapter { get; }
public PepperDashEthernetEventArgs(EthernetEventType eventType, short adapter)
{
EthernetEventType = eventType;
EthernetAdapter = adapter;
}
}
/// <summary>
/// Mirrors the set of <c>CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET</c> values
/// used across this codebase — does not aim to be exhaustive.
/// </summary>
public enum EthernetParameterType
{
GetCurrentIpAddress,
GetHostname,
GetDomainName,
GetLinkStatus,
GetCurrentDhcpState,
GetCurrentIpMask,
GetCurrentRouter,
GetMacAddress,
GetDnsServer,
}
/// <summary>
/// Mirrors Crestron's <c>SocketStatus</c> without requiring the Crestron SDK.
/// </summary>
public enum PepperDashSocketStatus
{
SocketNotConnected = 0,
SocketConnected = 2,
SocketConnectionInProgress = 6,
SocketConnectFailed = 11,
SocketDisconnecting = 12,
SocketBrokenRemotely = 7,
}

View file

@ -0,0 +1,32 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronConsole</c> to allow unit testing
/// without the Crestron SDK.
/// </summary>
public interface ICrestronConsole
{
/// <summary>Prints a line to the Crestron console/telnet output.</summary>
void PrintLine(string message);
/// <summary>Prints text (without newline) to the Crestron console/telnet output.</summary>
void Print(string message);
/// <summary>
/// Sends a response string to the console for the currently-executing console command.
/// </summary>
void ConsoleCommandResponse(string message);
/// <summary>
/// Registers a new command with the Crestron console.
/// </summary>
/// <param name="callback">Handler invoked when the command is typed.</param>
/// <param name="command">Command name (no spaces).</param>
/// <param name="helpText">Help text shown by the Crestron console.</param>
/// <param name="accessLevel">Minimum access level required to run the command.</param>
void AddNewConsoleCommand(
Action<string> callback,
string command,
string helpText,
ConsoleAccessLevel accessLevel);
}

View file

@ -0,0 +1,31 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronDataStore.CrestronDataStoreStatic</c>
/// to allow unit testing without the Crestron SDK.
/// </summary>
public interface ICrestronDataStore
{
/// <summary>Initialises the data store. Must be called once before any other operation.</summary>
void InitStore();
/// <summary>Reads an integer value from the local (program-slot) store.</summary>
/// <returns><c>true</c> if the value was found and read successfully.</returns>
bool TryGetLocalInt(string key, out int value);
/// <summary>Writes an integer value to the local (program-slot) store.</summary>
/// <returns><c>true</c> on success.</returns>
bool SetLocalInt(string key, int value);
/// <summary>Writes an unsigned integer value to the local (program-slot) store.</summary>
/// <returns><c>true</c> on success.</returns>
bool SetLocalUint(string key, uint value);
/// <summary>Reads a boolean value from the local (program-slot) store.</summary>
/// <returns><c>true</c> if the value was found and read successfully.</returns>
bool TryGetLocalBool(string key, out bool value);
/// <summary>Writes a boolean value to the local (program-slot) store.</summary>
/// <returns><c>true</c> on success.</returns>
bool SetLocalBool(string key, bool value);
}

View file

@ -0,0 +1,52 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronEnvironment</c> to allow unit testing
/// without the Crestron SDK.
/// </summary>
public interface ICrestronEnvironment
{
/// <summary>Gets the platform the program is executing on.</summary>
DevicePlatform DevicePlatform { get; }
/// <summary>Gets the current runtime environment.</summary>
RuntimeEnvironment RuntimeEnvironment { get; }
/// <summary>Gets the platform-appropriate newline string.</summary>
string NewLine { get; }
/// <summary>Gets the application number (program slot).</summary>
uint ApplicationNumber { get; }
/// <summary>Gets the room ID (used in Crestron Virtual Control / server environments).</summary>
uint RoomId { get; }
/// <summary>Raised when program status changes (starting, stopping, etc.).</summary>
event EventHandler<ProgramStatusEventArgs> ProgramStatusChanged;
/// <summary>Raised when the ethernet link changes state.</summary>
event EventHandler<PepperDashEthernetEventArgs> EthernetEventReceived;
/// <summary>Gets the application root directory path.</summary>
string GetApplicationRootDirectory();
/// <summary>
/// Returns <c>true</c> when running on real Crestron hardware.
/// Returns <c>false</c> in test / dev environments so that SDK-specific
/// sinks and enrichers can be safely skipped.
/// </summary>
bool IsHardwareRuntime { get; }
}
/// <summary>
/// Event args for <see cref="ICrestronEnvironment.ProgramStatusChanged"/>.
/// </summary>
public class ProgramStatusEventArgs : EventArgs
{
public ProgramStatusEventType EventType { get; }
public ProgramStatusEventArgs(ProgramStatusEventType eventType)
{
EventType = eventType;
}
}

View file

@ -0,0 +1,16 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronEthernetHelper</c> to allow unit testing
/// without the Crestron SDK.
/// </summary>
public interface IEthernetHelper
{
/// <summary>
/// Returns a network parameter string for the specified adapter.
/// </summary>
/// <param name="parameter">The parameter to retrieve.</param>
/// <param name="ethernetAdapterId">Ethernet adapter index (0 = LAN A).</param>
/// <returns>String value of the requested parameter.</returns>
string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId);
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>PepperDash.Core.Abstractions</RootNamespace>
<AssemblyName>PepperDash.Core.Abstractions</AssemblyName>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Deterministic>true</Deterministic>
<NeutralLanguage>en</NeutralLanguage>
<Title>PepperDash Core Abstractions</Title>
<Company>PepperDash Technologies</Company>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/PepperDash/PepperDashCore</RepositoryUrl>
<NullableContextOptions>enable</NullableContextOptions>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<InformationalVersion>$(Version)</InformationalVersion>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<!-- No Crestron SDK reference — this project must remain hardware-agnostic so test projects can reference it -->
</PropertyGroup>
</Project>

View file

@ -0,0 +1,323 @@
using FluentAssertions;
using PepperDash.Core;
using Xunit;
namespace PepperDash.Core.Tests.Devices;
/// <summary>
/// Tests for <see cref="Device"/> — the base class for all PepperDash devices.
/// These run without Crestron hardware; Debug is initialized with fakes via TestInitializer.
/// </summary>
public class DeviceTests
{
// -----------------------------------------------------------------------
// Construction
// -----------------------------------------------------------------------
[Fact]
public void Constructor_SingleArg_SetsKey()
{
var device = new Device("my-device");
device.Key.Should().Be("my-device");
}
[Fact]
public void Constructor_SingleArg_SetsNameToEmpty()
{
var device = new Device("my-device");
device.Name.Should().BeEmpty();
}
[Fact]
public void Constructor_TwoArg_SetsKeyAndName()
{
var device = new Device("my-device", "My Device");
device.Key.Should().Be("my-device");
device.Name.Should().Be("My Device");
}
[Fact]
public void Constructor_KeyWithDot_StillSetsKey()
{
// The dot triggers a debug log warning but must not prevent construction.
var device = new Device("parent.child");
device.Key.Should().Be("parent.child");
}
// -----------------------------------------------------------------------
// ToString
// -----------------------------------------------------------------------
[Fact]
public void ToString_WithName_FormatsKeyDashName()
{
var device = new Device("cam-01", "Front Camera");
device.ToString().Should().Be("cam-01 - Front Camera");
}
[Fact]
public void ToString_WithoutName_UsesDashPlaceholder()
{
var device = new Device("cam-01");
device.ToString().Should().Be("cam-01 - ---");
}
// -----------------------------------------------------------------------
// DefaultDevice
// -----------------------------------------------------------------------
[Fact]
public void DefaultDevice_IsNotNull()
{
Device.DefaultDevice.Should().NotBeNull();
}
[Fact]
public void DefaultDevice_HasKeyDefault()
{
Device.DefaultDevice.Key.Should().Be("Default");
}
// -----------------------------------------------------------------------
// CustomActivate / Activate / Deactivate / Initialize
// -----------------------------------------------------------------------
[Fact]
public void CustomActivate_DefaultReturnTrue()
{
var device = new TestDevice("d1");
device.CallCustomActivate().Should().BeTrue();
}
[Fact]
public void Deactivate_DefaultReturnsTrue()
{
var device = new Device("d1");
device.Deactivate().Should().BeTrue();
}
[Fact]
public void Activate_CallsCustomActivate_AndReturnsItsResult()
{
var stub = new ActivateTrackingDevice("d1", result: false);
stub.Activate().Should().BeFalse();
stub.CustomActivateCalled.Should().BeTrue();
}
[Fact]
public void Activate_TrueWhenCustomActivateReturnsTrue()
{
var stub = new ActivateTrackingDevice("d1", result: true);
stub.Activate().Should().BeTrue();
}
[Fact]
public void Initialize_DoesNotThrow()
{
var device = new TestDevice("d1");
var act = () => device.CallInitialize();
act.Should().NotThrow();
}
// -----------------------------------------------------------------------
// PreActivate
// -----------------------------------------------------------------------
[Fact]
public void PreActivate_NoActions_DoesNotThrow()
{
var device = new TestDevice("d1");
var act = () => device.PreActivate();
act.Should().NotThrow();
}
[Fact]
public void PreActivate_RunsRegisteredActionsInOrder()
{
var device = new TestDevice("d1");
var order = new List<int>();
device.AddPreActivationAction(() => order.Add(1));
device.AddPreActivationAction(() => order.Add(2));
device.AddPreActivationAction(() => order.Add(3));
device.PreActivate();
order.Should().Equal(1, 2, 3);
}
[Fact]
public void PreActivate_ContinuesAfterFaultingAction()
{
var device = new TestDevice("d1");
var reached = false;
device.AddPreActivationAction(() => throw new InvalidOperationException("boom"));
device.AddPreActivationAction(() => reached = true);
var act = () => device.PreActivate();
act.Should().NotThrow("exceptions in individual actions must be caught internally");
reached.Should().BeTrue("actions after a faulting action must still run");
}
// -----------------------------------------------------------------------
// PostActivate
// -----------------------------------------------------------------------
[Fact]
public void PostActivate_NoActions_DoesNotThrow()
{
var device = new TestDevice("d1");
var act = () => device.PostActivate();
act.Should().NotThrow();
}
[Fact]
public void PostActivate_RunsRegisteredActionsInOrder()
{
var device = new TestDevice("d1");
var order = new List<int>();
device.AddPostActivationAction(() => order.Add(1));
device.AddPostActivationAction(() => order.Add(2));
device.PostActivate();
order.Should().Equal(1, 2);
}
[Fact]
public void PostActivate_ContinuesAfterFaultingAction()
{
var device = new TestDevice("d1");
var reached = false;
device.AddPostActivationAction(() => throw new Exception("boom"));
device.AddPostActivationAction(() => reached = true);
var act = () => device.PostActivate();
act.Should().NotThrow();
reached.Should().BeTrue();
}
// -----------------------------------------------------------------------
// Pre and Post actions are independent lists
// -----------------------------------------------------------------------
[Fact]
public void PreActivationActions_DoNotRunOnPostActivate()
{
var device = new TestDevice("d1");
var preRan = false;
device.AddPreActivationAction(() => preRan = true);
device.PostActivate();
preRan.Should().BeFalse();
}
[Fact]
public void PostActivationActions_DoNotRunOnPreActivate()
{
var device = new TestDevice("d1");
var postRan = false;
device.AddPostActivationAction(() => postRan = true);
device.PreActivate();
postRan.Should().BeFalse();
}
// -----------------------------------------------------------------------
// OnFalse
// -----------------------------------------------------------------------
[Fact]
public void OnFalse_FiresAction_WhenBoolIsFalse()
{
var device = new Device("d1");
var fired = false;
device.OnFalse(false, () => fired = true);
fired.Should().BeTrue();
}
[Fact]
public void OnFalse_DoesNotFireAction_WhenBoolIsTrue()
{
var device = new Device("d1");
var fired = false;
device.OnFalse(true, () => fired = true);
fired.Should().BeFalse();
}
[Fact]
public void OnFalse_DoesNotFireAction_ForNonBoolType()
{
var device = new Device("d1");
var fired = false;
device.OnFalse("not a bool", () => fired = true);
device.OnFalse(0, () => fired = true);
device.OnFalse(null!, () => fired = true);
fired.Should().BeFalse();
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
/// <summary>
/// Exposes protected Device members so test methods can call them directly.
/// </summary>
private class TestDevice : Device
{
public TestDevice(string key) : base(key) { }
public TestDevice(string key, string name) : base(key, name) { }
public void AddPreActivationAction(Action act) => base.AddPreActivationAction(act);
public void AddPostActivationAction(Action act) => base.AddPostActivationAction(act);
public bool CallCustomActivate() => base.CustomActivate();
public void CallInitialize() => base.Initialize();
}
/// <summary>
/// Records whether CustomActivate was invoked and returns a configured result.
/// Used to verify Activate() correctly delegates to CustomActivate().
/// </summary>
private sealed class ActivateTrackingDevice : Device
{
private readonly bool _result;
public bool CustomActivateCalled { get; private set; }
public ActivateTrackingDevice(string key, bool result = true) : base(key)
{
_result = result;
}
protected override bool CustomActivate()
{
CustomActivateCalled = true;
return _result;
}
}
}

View file

@ -0,0 +1,38 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// No-op ICrestronConsole that captures output for test assertions.
/// </summary>
public class CapturingCrestronConsole : ICrestronConsole
{
public List<string> Lines { get; } = new();
public List<string> CommandResponses { get; } = new();
public List<(string Command, string HelpText)> RegisteredCommands { get; } = new();
public void PrintLine(string message) => Lines.Add(message);
public void Print(string message) => Lines.Add(message);
public void ConsoleCommandResponse(string message) => CommandResponses.Add(message);
public void AddNewConsoleCommand(
Action<string> callback,
string command,
string helpText,
ConsoleAccessLevel accessLevel)
{
RegisteredCommands.Add((command, helpText));
}
}
/// <summary>
/// Minimal no-op ICrestronConsole that discards all output. Useful when you only
/// care about the system under test and not what it logs.
/// </summary>
public class NoOpCrestronConsole : ICrestronConsole
{
public void PrintLine(string message) { }
public void Print(string message) { }
public void ConsoleCommandResponse(string message) { }
public void AddNewConsoleCommand(Action<string> _, string __, string ___, ConsoleAccessLevel ____) { }
}

View file

@ -0,0 +1,49 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// Configurable ICrestronEnvironment for unit tests.
/// Defaults: Appliance / SimplSharpPro / ApplicationNumber=1.
/// </summary>
public class FakeCrestronEnvironment : ICrestronEnvironment
{
public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance;
public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro;
public string NewLine { get; set; } = "\r\n";
public uint ApplicationNumber { get; set; } = 1;
public uint RoomId { get; set; } = 0;
public event EventHandler<ProgramStatusEventArgs>? ProgramStatusChanged;
public event EventHandler<PepperDashEthernetEventArgs>? EthernetEventReceived;
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
/// <inheritdoc/>
public bool IsHardwareRuntime => false;
/// <summary>Simulates a program status event for tests.</summary>
public void RaiseProgramStatus(ProgramStatusEventType type) =>
ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type));
/// <summary>Simulates an ethernet event for tests.</summary>
public void RaiseEthernetEvent(EthernetEventType type, short adapter = 0) =>
EthernetEventReceived?.Invoke(this, new PepperDashEthernetEventArgs(type, adapter));
}
/// <summary>
/// No-op IEthernetHelper that returns configurable values.
/// </summary>
public class FakeEthernetHelper : IEthernetHelper
{
private readonly Dictionary<EthernetParameterType, string> _values = new();
public FakeEthernetHelper Seed(EthernetParameterType param, string value)
{
_values[param] = value;
return this;
}
public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) =>
_values.TryGetValue(parameter, out var v) ? v : string.Empty;
}

View file

@ -0,0 +1,59 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// In-memory ICrestronDataStore backed by a dictionary.
/// Use in unit tests to verify that keys are read from and written to the store correctly.
/// </summary>
public class InMemoryCrestronDataStore : ICrestronDataStore
{
private readonly Dictionary<string, object> _store = new();
public bool Initialized { get; private set; }
public void InitStore() => Initialized = true;
public bool TryGetLocalInt(string key, out int value)
{
if (_store.TryGetValue(key, out var raw) && raw is int i)
{
value = i;
return true;
}
value = 0;
return false;
}
public bool SetLocalInt(string key, int value)
{
_store[key] = value;
return true;
}
public bool SetLocalUint(string key, uint value)
{
_store[key] = (int)value;
return true;
}
public bool TryGetLocalBool(string key, out bool value)
{
if (_store.TryGetValue(key, out var raw) && raw is bool b)
{
value = b;
return true;
}
value = false;
return false;
}
public bool SetLocalBool(string key, bool value)
{
_store[key] = value;
return true;
}
/// <summary>Seeds a key for testing read paths.</summary>
public void Seed(string key, object value) => _store[key] = value;
}

View file

@ -0,0 +1,171 @@
using FluentAssertions;
using PepperDash.Core.Abstractions;
using PepperDash.Core.Tests.Fakes;
using Xunit;
namespace PepperDash.Core.Tests.Logging;
/// <summary>
/// Tests for Debug-related service interfaces and implementations.
/// These tests verify the behaviour of the abstractions in isolation (no Crestron SDK required).
/// </summary>
public class DebugServiceTests
{
// -----------------------------------------------------------------------
// ICrestronDataStore — InMemoryCrestronDataStore
// -----------------------------------------------------------------------
[Fact]
public void DataStore_InitStore_SetsInitializedFlag()
{
var store = new InMemoryCrestronDataStore();
store.Initialized.Should().BeFalse("not yet initialized");
store.InitStore();
store.Initialized.Should().BeTrue();
}
[Fact]
public void DataStore_SetAndGetLocalInt_RoundTrips()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalInt("MyKey", 42).Should().BeTrue();
store.TryGetLocalInt("MyKey", out var value).Should().BeTrue();
value.Should().Be(42);
}
[Fact]
public void DataStore_TryGetLocalInt_ReturnsFalse_WhenKeyAbsent()
{
var store = new InMemoryCrestronDataStore();
store.TryGetLocalInt("Missing", out var value).Should().BeFalse();
value.Should().Be(0);
}
[Fact]
public void DataStore_SetAndGetLocalBool_RoundTrips()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalBool("FlagKey", true).Should().BeTrue();
store.TryGetLocalBool("FlagKey", out var value).Should().BeTrue();
value.Should().BeTrue();
}
[Fact]
public void DataStore_TryGetLocalBool_ReturnsFalse_WhenKeyAbsent()
{
var store = new InMemoryCrestronDataStore();
store.TryGetLocalBool("Missing", out var value).Should().BeFalse();
value.Should().BeFalse();
}
[Fact]
public void DataStore_SetLocalUint_CanBeReadBackAsInt()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalUint("UintKey", 3u).Should().BeTrue();
store.TryGetLocalInt("UintKey", out var value).Should().BeTrue();
value.Should().Be(3);
}
[Fact]
public void DataStore_Seed_AllowsTestSetupOfReadPaths()
{
var store = new InMemoryCrestronDataStore();
store.Seed("MyLevel", 2);
store.TryGetLocalInt("MyLevel", out var level).Should().BeTrue();
level.Should().Be(2);
}
// -----------------------------------------------------------------------
// DebugServiceRegistration
// -----------------------------------------------------------------------
[Fact]
public void ServiceRegistration_Register_StoresAllThreeServices()
{
var env = new FakeCrestronEnvironment();
var console = new NoOpCrestronConsole();
var store = new InMemoryCrestronDataStore();
DebugServiceRegistration.Register(env, console, store);
DebugServiceRegistration.Environment.Should().BeSameAs(env);
DebugServiceRegistration.Console.Should().BeSameAs(console);
DebugServiceRegistration.DataStore.Should().BeSameAs(store);
}
[Fact]
public void ServiceRegistration_Register_AcceptsNullsWithoutThrowing()
{
var act = () => DebugServiceRegistration.Register(null, null, null);
act.Should().NotThrow();
}
// -----------------------------------------------------------------------
// ICrestronEnvironment — FakeCrestronEnvironment
// -----------------------------------------------------------------------
[Fact]
public void FakeEnvironment_DefaultsToAppliance()
{
var env = new FakeCrestronEnvironment();
env.DevicePlatform.Should().Be(DevicePlatform.Appliance);
}
[Fact]
public void FakeEnvironment_RaiseProgramStatus_FiresEvent()
{
var env = new FakeCrestronEnvironment();
ProgramStatusEventType? received = null;
env.ProgramStatusChanged += (_, e) => received = e.EventType;
env.RaiseProgramStatus(ProgramStatusEventType.Stopping);
received.Should().Be(ProgramStatusEventType.Stopping);
}
[Fact]
public void FakeEnvironment_RaiseEthernetEvent_FiresEvent()
{
var env = new FakeCrestronEnvironment();
EthernetEventType? received = null;
env.EthernetEventReceived += (_, e) => received = e.EthernetEventType;
env.RaiseEthernetEvent(EthernetEventType.LinkUp, adapter: 0);
received.Should().Be(EthernetEventType.LinkUp);
}
// -----------------------------------------------------------------------
// ICrestronConsole — CapturingCrestronConsole
// -----------------------------------------------------------------------
[Fact]
public void CapturingConsole_PrintLine_CapturesMessage()
{
var console = new CapturingCrestronConsole();
console.PrintLine("hello world");
console.Lines.Should().ContainSingle().Which.Should().Be("hello world");
}
[Fact]
public void CapturingConsole_AddNewConsoleCommand_RecordsCommandName()
{
var console = new CapturingCrestronConsole();
console.AddNewConsoleCommand(_ => { }, "appdebug", "Sets debug level", ConsoleAccessLevel.AccessOperator);
console.RegisteredCommands.Should().ContainSingle()
.Which.Command.Should().Be("appdebug");
}
}

View file

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
</ItemGroup>
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
<!-- PepperDash.Core is referenced so we can test Device and other concrete types.
DebugServiceRegistration.Register() is called via a [ModuleInitializer] before any
test runs, so Debug's static constructor uses fakes and never touches the Crestron SDK. -->
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,29 @@
using System.Runtime.CompilerServices;
using PepperDash.Core.Abstractions;
using PepperDash.Core.Tests.Fakes;
namespace PepperDash.Core.Tests;
/// <summary>
/// Runs once before any type in this assembly is accessed.
/// Registers fake Crestron service implementations with <see cref="DebugServiceRegistration"/>
/// so that the <c>Debug</c> static constructor uses them instead of the real Crestron SDK.
/// This must remain a module initializer (not a test fixture) because the static constructor
/// fires the first time <em>any</em> type in PepperDash.Core is referenced — before xUnit
/// has a chance to run fixture setup code.
/// </summary>
internal static class TestInitializer
{
[ModuleInitializer]
internal static void Initialize()
{
DebugServiceRegistration.Register(
new FakeCrestronEnvironment
{
DevicePlatform = DevicePlatform.Server, // avoids any appliance-only code paths
RuntimeEnvironment = RuntimeEnvironment.Other, // skips console command registration
},
new NoOpCrestronConsole(),
new InMemoryCrestronDataStore());
}
}

View file

@ -0,0 +1,35 @@
using System;
using Crestron.SimplSharp;
using PdCore = PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates ICrestronConsole calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronConsoleAdapter : PdCore.ICrestronConsole
{
public void PrintLine(string message) => CrestronConsole.PrintLine(message);
public void Print(string message) => CrestronConsole.Print(message);
public void ConsoleCommandResponse(string message) =>
CrestronConsole.ConsoleCommandResponse(message);
public void AddNewConsoleCommand(
Action<string> callback,
string command,
string helpText,
PdCore.ConsoleAccessLevel accessLevel)
{
var crestronLevel = accessLevel switch
{
PdCore.ConsoleAccessLevel.AccessAdministrator => ConsoleAccessLevelEnum.AccessAdministrator,
PdCore.ConsoleAccessLevel.AccessProgrammer => ConsoleAccessLevelEnum.AccessProgrammer,
_ => ConsoleAccessLevelEnum.AccessOperator,
};
// Wrap Action<string> in a lambda — Crestron's delegate is not a standard Action<string>.
CrestronConsole.AddNewConsoleCommand(s => callback(s), command, helpText, crestronLevel);
}
}

View file

@ -0,0 +1,42 @@
using Crestron.SimplSharp.CrestronDataStore;
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates ICrestronDataStore calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronDataStoreAdapter : ICrestronDataStore
{
public void InitStore() => CrestronDataStoreStatic.InitCrestronDataStore();
public bool TryGetLocalInt(string key, out int value)
{
var err = CrestronDataStoreStatic.GetLocalIntValue(key, out value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool SetLocalInt(string key, int value)
{
var err = CrestronDataStoreStatic.SetLocalIntValue(key, value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool SetLocalUint(string key, uint value)
{
var err = CrestronDataStoreStatic.SetLocalUintValue(key, value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool TryGetLocalBool(string key, out bool value)
{
var err = CrestronDataStoreStatic.GetLocalBoolValue(key, out value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool SetLocalBool(string key, bool value)
{
var err = CrestronDataStoreStatic.SetLocalBoolValue(key, value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
}

View file

@ -0,0 +1,71 @@
using System;
using Crestron.SimplSharp;
using PdCore = PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates ICrestronEnvironment calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment
{
// Subscribe once in constructor and re-raise as our event types.
private event EventHandler<PdCore.ProgramStatusEventArgs>? _programStatusChanged;
private event EventHandler<PdCore.PepperDashEthernetEventArgs>? _ethernetEventReceived;
public CrestronEnvironmentAdapter()
{
CrestronEnvironment.ProgramStatusEventHandler += type =>
_programStatusChanged?.Invoke(this, new PdCore.ProgramStatusEventArgs(MapProgramStatus(type)));
CrestronEnvironment.EthernetEventHandler += args =>
_ethernetEventReceived?.Invoke(this, new PdCore.PepperDashEthernetEventArgs(
args.EthernetEventType == eEthernetEventType.LinkDown
? PdCore.EthernetEventType.LinkDown
: PdCore.EthernetEventType.LinkUp,
(short)args.EthernetAdapter));
}
public PdCore.DevicePlatform DevicePlatform =>
CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
? PdCore.DevicePlatform.Appliance
: PdCore.DevicePlatform.Server;
public PdCore.RuntimeEnvironment RuntimeEnvironment =>
CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro
? PdCore.RuntimeEnvironment.SimplSharpPro
: PdCore.RuntimeEnvironment.Other;
public string NewLine => CrestronEnvironment.NewLine;
public uint ApplicationNumber => InitialParametersClass.ApplicationNumber;
public uint RoomId => uint.TryParse(InitialParametersClass.RoomId, out var r) ? r : 0;
public event EventHandler<PdCore.ProgramStatusEventArgs> ProgramStatusChanged
{
add => _programStatusChanged += value;
remove => _programStatusChanged -= value;
}
public event EventHandler<PdCore.PepperDashEthernetEventArgs> EthernetEventReceived
{
add => _ethernetEventReceived += value;
remove => _ethernetEventReceived -= value;
}
public string GetApplicationRootDirectory() =>
Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory();
/// <inheritdoc/>
public bool IsHardwareRuntime => true;
private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) =>
type switch
{
eProgramStatusEventType.Stopping => PdCore.ProgramStatusEventType.Stopping,
eProgramStatusEventType.Paused => PdCore.ProgramStatusEventType.Paused,
eProgramStatusEventType.Resumed => PdCore.ProgramStatusEventType.Resumed,
_ => PdCore.ProgramStatusEventType.Starting,
};
}

View file

@ -0,0 +1,39 @@
using System;
using Crestron.SimplSharp;
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates IEthernetHelper calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronEthernetAdapter : IEthernetHelper
{
public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId)
{
var crestronParam = parameter switch
{
EthernetParameterType.GetCurrentIpAddress =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS,
EthernetParameterType.GetHostname =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME,
EthernetParameterType.GetDomainName =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME,
EthernetParameterType.GetLinkStatus =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS,
EthernetParameterType.GetCurrentDhcpState =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE,
EthernetParameterType.GetCurrentIpMask =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK,
EthernetParameterType.GetCurrentRouter =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER,
EthernetParameterType.GetMacAddress =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS,
EthernetParameterType.GetDnsServer =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER,
_ => throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null),
};
return CrestronEthernetHelper.GetEthernetParameter(crestronParam, ethernetAdapterId);
}
}

View file

@ -1,43 +0,0 @@
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

@ -8,8 +8,8 @@ using Crestron.SimplSharp;
using PepperDash.Core; using PepperDash.Core;
namespace PepperDash.Core namespace PepperDash.Core;
{
/// <summary> /// <summary>
/// Defines the string event handler for line events on the gather /// Defines the string event handler for line events on the gather
/// </summary> /// </summary>
@ -175,4 +175,3 @@ namespace PepperDash.Core
Stop(); Stop();
} }
} }
}

View file

@ -1,16 +1,17 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using System.Timers;
using PepperDash.Core; using PepperDash.Core;
namespace 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>
/// 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> /// <summary>
/// Device Key that this instance configures /// Device Key that this instance configures
/// </summary> /// </summary>
@ -19,10 +20,10 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Timer to disable automatically if not manually disabled /// Timer to disable automatically if not manually disabled
/// </summary> /// </summary>
private CTimer DebugExpiryPeriod; private Timer DebugExpiryPeriod;
/// <summary> /// <summary>
/// Gets or sets the DebugSetting /// The current debug setting
/// </summary> /// </summary>
public eStreamDebuggingSetting DebugSetting { get; private set; } public eStreamDebuggingSetting DebugSetting { get; private set; }
@ -36,14 +37,14 @@ namespace PepperDash.Core
{ {
get get
{ {
return _DebugTimeoutInMs / 60000; return _DebugTimeoutInMs/60000;
} }
} }
/// <summary> /// <summary>
/// Gets or sets the RxStreamDebuggingIsEnabled /// Indicates that receive stream debugging is enabled
/// </summary> /// </summary>
public bool RxStreamDebuggingIsEnabled { get; private set; } public bool RxStreamDebuggingIsEnabled{ get; private set; }
/// <summary> /// <summary>
/// Indicates that transmit stream debugging is enabled /// Indicates that transmit stream debugging is enabled
@ -64,9 +65,6 @@ namespace PepperDash.Core
/// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues /// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues
/// </summary> /// </summary>
/// <param name="setting"></param> /// <param name="setting"></param>
/// <summary>
/// SetDebuggingWithDefaultTimeout method
/// </summary>
public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting) public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting)
{ {
if (setting == eStreamDebuggingSetting.Off) if (setting == eStreamDebuggingSetting.Off)
@ -83,9 +81,6 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="setting"></param> /// <param name="setting"></param>
/// <param name="minutes"></param> /// <param name="minutes"></param>
/// <summary>
/// SetDebuggingWithSpecificTimeout method
/// </summary>
public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes) public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes)
{ {
if (setting == eStreamDebuggingSetting.Off) if (setting == eStreamDebuggingSetting.Off)
@ -98,7 +93,9 @@ namespace PepperDash.Core
StopDebugTimer(); StopDebugTimer();
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs); DebugExpiryPeriod = new Timer(_DebugTimeoutInMs) { AutoReset = false };
DebugExpiryPeriod.Elapsed += (s, e) => DisableDebugging();
DebugExpiryPeriod.Start();
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx) if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
RxStreamDebuggingIsEnabled = true; RxStreamDebuggingIsEnabled = true;
@ -134,5 +131,29 @@ namespace PepperDash.Core
DebugExpiryPeriod.Dispose(); DebugExpiryPeriod.Dispose();
DebugExpiryPeriod = null; DebugExpiryPeriod = null;
} }
}
} }
/// <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

@ -1,15 +1,20 @@
using System; extern alias NewtonsoftJson;
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace PepperDash.Core using System;
using Crestron.SimplSharp;
using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute;
using JsonIgnore = NewtonsoftJson::Newtonsoft.Json.JsonIgnoreAttribute;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using NullValueHandling = NewtonsoftJson::Newtonsoft.Json.NullValueHandling;
using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter;
namespace PepperDash.Core;
/// <summary>
/// Config properties that indicate how to communicate with a device for control
/// </summary>
public class ControlPropertiesConfig
{ {
/// <summary>
/// Represents a ControlPropertiesConfig
/// </summary>
public class ControlPropertiesConfig
{
/// <summary> /// <summary>
/// The method of control /// The method of control
/// </summary> /// </summary>
@ -89,5 +94,4 @@ namespace PepperDash.Core
public ControlPropertiesConfig() public ControlPropertiesConfig()
{ {
} }
}
} }

View file

@ -16,26 +16,26 @@ using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
namespace PepperDash.Core namespace PepperDash.Core;
{
/// <summary>
/// Delegate for notifying of socket status changes
/// </summary>
/// <param name="client"></param>
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
/// <summary> /// <summary>
/// EventArgs class for socket status changes /// Delegate for notifying of socket status changes
/// </summary> /// </summary>
public class GenericSocketStatusChageEventArgs : EventArgs /// <param name="client"></param>
{ public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
/// <summary>
/// EventArgs class for socket status changes
/// </summary>
public class GenericSocketStatusChageEventArgs : EventArgs
{
/// <summary> /// <summary>
/// Gets or sets the Client /// Gets or sets the Client
/// </summary> /// </summary>
public ISocketStatus Client { get; private set; } public ISocketStatus Client { get; private set; }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="client"></param> /// <param name="client"></param>
public GenericSocketStatusChageEventArgs(ISocketStatus client) public GenericSocketStatusChageEventArgs(ISocketStatus client)
@ -46,26 +46,26 @@ namespace PepperDash.Core
/// S+ Constructor /// S+ Constructor
/// </summary> /// </summary>
public GenericSocketStatusChageEventArgs() { } public GenericSocketStatusChageEventArgs() { }
} }
/// <summary> /// <summary>
/// Delegate for notifying of TCP Server state changes /// Delegate for notifying of TCP Server state changes
/// </summary> /// </summary>
/// <param name="state"></param> /// <param name="state"></param>
public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state); public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
/// <summary> /// <summary>
/// EventArgs class for TCP Server state changes /// EventArgs class for TCP Server state changes
/// </summary> /// </summary>
public class GenericTcpServerStateChangedEventArgs : EventArgs public class GenericTcpServerStateChangedEventArgs : EventArgs
{ {
/// <summary> /// <summary>
/// Gets or sets the State /// Gets or sets the State
/// </summary> /// </summary>
public ServerState State { get; private set; } public ServerState State { get; private set; }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="state"></param> /// <param name="state"></param>
public GenericTcpServerStateChangedEventArgs(ServerState state) public GenericTcpServerStateChangedEventArgs(ServerState state)
@ -76,35 +76,37 @@ namespace PepperDash.Core
/// S+ Constructor /// S+ Constructor
/// </summary> /// </summary>
public GenericTcpServerStateChangedEventArgs() { } 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>
/// Delegate for TCP Server socket status changes /// Gets or sets the Socket
/// </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> /// </summary>
public object Socket { get; private set; } public object Socket { get; private set; }
/// <summary> /// <summary>
/// /// Gets or sets the index of the client from which the status change was received
/// </summary> /// </summary>
public uint ReceivedFromClientIndex { get; private set; } public uint ReceivedFromClientIndex { get; private set; }
/// <summary> /// <summary>
/// /// Gets or sets the ClientStatus
/// </summary> /// </summary>
public SocketStatus ClientStatus { get; set; } public SocketStatus ClientStatus { get; set; }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="socket"></param> /// <param name="socket"></param>
/// <param name="clientStatus"></param> /// <param name="clientStatus"></param>
@ -115,7 +117,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="socket"></param> /// <param name="socket"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
@ -130,20 +132,20 @@ namespace PepperDash.Core
/// S+ Constructor /// S+ Constructor
/// </summary> /// </summary>
public GenericTcpServerSocketStatusChangeEventArgs() { } public GenericTcpServerSocketStatusChangeEventArgs() { }
} }
/// <summary>
/// EventArgs for TCP server com method receive text
/// </summary>
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
{
/// <summary> /// <summary>
/// EventArgs for TCP server com method receive text /// Gets or sets the index of the client from which the text was received
/// </summary>
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
{
/// <summary>
///
/// </summary> /// </summary>
public uint ReceivedFromClientIndex { get; private set; } public uint ReceivedFromClientIndex { get; private set; }
/// <summary> /// <summary>
/// /// Gets the index of the client from which the text was received as a ushort
/// </summary> /// </summary>
public ushort ReceivedFromClientIndexShort public ushort ReceivedFromClientIndexShort
{ {
@ -159,7 +161,7 @@ namespace PepperDash.Core
public string Text { get; private set; } public string Text { get; private set; }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text) public GenericTcpServerCommMethodReceiveTextArgs(string text)
@ -168,7 +170,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
@ -181,43 +183,45 @@ namespace PepperDash.Core
/// S+ Constructor /// S+ Constructor
/// </summary> /// </summary>
public GenericTcpServerCommMethodReceiveTextArgs() { } public GenericTcpServerCommMethodReceiveTextArgs() { }
} }
/// <summary>
/// EventArgs for TCP server client ready for communication
/// </summary>
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
{
/// <summary> /// <summary>
/// EventArgs for TCP server client ready for communication /// Gets or sets IsReady
/// </summary>
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
{
/// <summary>
///
/// </summary> /// </summary>
public bool IsReady; public bool IsReady;
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="isReady"></param> /// <param name="isReady"></param>
public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady) public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
{ {
IsReady = isReady; IsReady = isReady;
} }
/// <summary> /// <summary>
/// S+ Constructor /// S+ Constructor
/// </summary> /// </summary>
public GenericTcpServerClientReadyForcommunicationsEventArgs() { } public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
} }
/// <summary>
/// EventArgs for UDP connected
/// </summary>
public class GenericUdpConnectedEventArgs : EventArgs
{
/// <summary> /// <summary>
/// EventArgs for UDP connected /// Gets or sets the UConnected
/// </summary>
public class GenericUdpConnectedEventArgs : EventArgs
{
/// <summary>
///
/// </summary> /// </summary>
public ushort UConnected; public ushort UConnected;
/// <summary> /// <summary>
/// /// Gets or sets the Connected status
/// </summary> /// </summary>
public bool Connected; public bool Connected;
@ -227,7 +231,7 @@ namespace PepperDash.Core
public GenericUdpConnectedEventArgs() { } public GenericUdpConnectedEventArgs() { }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="uconnected"></param> /// <param name="uconnected"></param>
public GenericUdpConnectedEventArgs(ushort uconnected) public GenericUdpConnectedEventArgs(ushort uconnected)
@ -236,7 +240,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="connected"></param> /// <param name="connected"></param>
public GenericUdpConnectedEventArgs(bool connected) public GenericUdpConnectedEventArgs(bool connected)
@ -244,8 +248,5 @@ namespace PepperDash.Core
Connected = connected; Connected = connected;
} }
}
} }

View file

@ -3,17 +3,18 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// A class to handle secure TCP/IP communications with a server
/// </summary>
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{ {
/// <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"; private const string SplusKey = "Uninitialized Secure Tcp _client";
/// <summary> /// <summary>
/// Stream debugging /// Stream debugging
@ -79,7 +80,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the Port /// Port on server
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; }
@ -149,7 +150,7 @@ namespace PepperDash.Core
public string ConnectionFailure { get { return ClientStatus.ToString(); } } public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary> /// <summary>
/// Gets or sets the AutoReconnect /// bool to track if auto reconnect should be set on the socket
/// </summary> /// </summary>
public bool AutoReconnect { get; set; } public bool AutoReconnect { get; set; }
@ -181,14 +182,14 @@ namespace PepperDash.Core
} }
// private Timer for auto reconnect // private Timer for auto reconnect
private CTimer RetryTimer; private Timer RetryTimer;
#endregion #endregion
#region GenericSecureTcpIpClient properties #region GenericSecureTcpIpClient properties
/// <summary> /// <summary>
/// Gets or sets the SharedKeyRequired /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary> /// </summary>
public bool SharedKeyRequired { get; set; } public bool SharedKeyRequired { get; set; }
@ -207,7 +208,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the SharedKey /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
/// </summary> /// </summary>
public string SharedKey { get; set; } public string SharedKey { get; set; }
@ -222,7 +223,7 @@ namespace PepperDash.Core
bool IsTryingToConnect; bool IsTryingToConnect;
/// <summary> /// <summary>
/// Gets or sets the IsReadyForCommunication /// Bool showing if socket is ready for communication after shared key exchange
/// </summary> /// </summary>
public bool IsReadyForCommunication { get; set; } public bool IsReadyForCommunication { get; set; }
@ -264,12 +265,12 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer; Timer HeartbeatSendTimer;
CTimer HeartbeatAckTimer; Timer HeartbeatAckTimer;
// Used to force disconnection on a dead connect attempt // Used to force disconnection on a dead connect attempt
CTimer ConnectFailTimer; Timer ConnectFailTimer;
CTimer WaitForSharedKey; Timer WaitForSharedKey;
private int ConnectionCount; private int ConnectionCount;
bool ProgramIsStopping; bool ProgramIsStopping;
@ -277,7 +278,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Queue lock /// Queue lock
/// </summary> /// </summary>
CCriticalSection DequeueLock = new CCriticalSection(); private readonly object _dequeueLock = new();
/// <summary> /// <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 /// 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
@ -342,7 +343,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Initialize method /// Just to help S+ set the key
/// </summary> /// </summary>
public void Initialize(string key) public void Initialize(string key)
{ {
@ -357,7 +358,7 @@ namespace PepperDash.Core
{ {
if (config == null) if (config == null)
{ {
Debug.Console(0, this, "Could not initialize client with key: {0}", Key); this.LogWarning( "Could not initialize client with key: {0}", Key);
return; return;
} }
try try
@ -397,7 +398,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex); this.LogError("Exception initializing client with key: {0}\rException: {1}", Key, ex);
} }
} }
@ -410,7 +411,7 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection"); this.LogInformation("Program stopping. Closing _client connection");
ProgramIsStopping = true; ProgramIsStopping = true;
Disconnect(); Disconnect();
} }
@ -421,9 +422,6 @@ namespace PepperDash.Core
/// Deactivate the client /// Deactivate the client
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// Deactivate method
/// </summary>
public override bool Deactivate() public override bool Deactivate()
{ {
if (_client != null) if (_client != null)
@ -435,22 +433,22 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Connect method /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
ConnectionCount++; ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); this.LogVerbose("Attempting connect Count:{0}", ConnectionCount);
if (IsConnected) if (IsConnected)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); this.LogInformation("Already connected. Ignoring.");
return; return;
} }
if (IsTryingToConnect) if (IsTryingToConnect)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); this.LogInformation("Already trying to connect. Ignoring.");
return; return;
} }
try try
@ -463,17 +461,17 @@ namespace PepperDash.Core
} }
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); this.LogWarning("DynamicTcpClient: No address set");
return; return;
} }
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); this.LogWarning("DynamicTcpClient: Invalid port");
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); this.LogWarning("DynamicTcpClient: No Shared Key set");
return; return;
} }
@ -494,9 +492,10 @@ namespace PepperDash.Core
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o => ConnectFailTimer = new Timer(30000) { AutoReset = false };
ConnectFailTimer.Elapsed += (s, e) =>
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect) if (IsTryingToConnect)
{ {
IsTryingToConnect = false; IsTryingToConnect = false;
@ -508,12 +507,13 @@ namespace PepperDash.Core
//SecureClient.DisconnectFromServer(); //SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect(); //CheckClosedAndTryReconnect();
} }
}, 30000); };
ConnectFailTimer.Start();
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
_client.ConnectToServerAsync(o => _client.ConnectToServerAsync(o =>
{ {
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null) if (ConnectFailTimer != null)
{ {
@ -523,22 +523,22 @@ namespace PepperDash.Core
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); this.LogVerbose("_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive); o.ReceiveDataAsync(Receive);
if (SharedKeyRequired) if (SharedKeyRequired)
{ {
WaitingForSharedKeyResponse = true; WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer => WaitForSharedKey = new Timer(15000) { AutoReset = false };
WaitForSharedKey.Elapsed += (s, e) =>
{ {
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
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 // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer(); o.DisconnectFromServer();
//CheckClosedAndTryReconnect(); //CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event //OnClientReadyForcommunications(false); // Should send false event
}, 15000); };
WaitForSharedKey.Start();
} }
else else
{ {
@ -552,21 +552,21 @@ namespace PepperDash.Core
} }
else else
{ {
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message); this.LogError("_client connection exception: {0}", ex.Message);
IsTryingToConnect = false; IsTryingToConnect = false;
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
} }
/// <summary> /// <summary>
/// Disconnect method ///
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
@ -589,20 +589,20 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// DisconnectClient method /// Does the actual disconnect business
/// </summary> /// </summary>
public void DisconnectClient() public void DisconnectClient()
{ {
if (_client == null) return; if (_client == null) return;
Debug.Console(1, this, "Disconnecting client"); this.LogInformation("Disconnecting client");
if (IsConnected) if (IsConnected)
_client.DisconnectFromServer(); _client.DisconnectFromServer();
// close up client. ALWAYS use this when disconnecting. // close up client. ALWAYS use this when disconnecting.
IsTryingToConnect = false; IsTryingToConnect = false;
Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : ""); this.LogVerbose("Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
_client.SocketStatusChange -= Client_SocketStatusChange; _client.SocketStatusChange -= Client_SocketStatusChange;
_client.Dispose(); _client.Dispose();
_client = null; _client = null;
@ -623,14 +623,14 @@ namespace PepperDash.Core
{ {
if (_client != null) if (_client != null)
{ {
Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); this.LogVerbose("Cleaning up remotely closed/failed connection.");
Disconnect(); Disconnect();
} }
if (!DisconnectCalledByUser && AutoReconnect) if (!DisconnectCalledByUser && AutoReconnect)
{ {
var halfInterval = AutoReconnectIntervalMs / 2; var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null) if (RetryTimer != null)
{ {
RetryTimer.Stop(); RetryTimer.Stop();
@ -638,7 +638,9 @@ namespace PepperDash.Core
} }
if (AutoReconnectTriggered != null) if (AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs()); AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime); RetryTimer = new Timer(rndTime) { AutoReset = false };
RetryTimer.Elapsed += (s, e) => Connect();
RetryTimer.Start();
} }
} }
@ -657,13 +659,13 @@ namespace PepperDash.Core
{ {
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str); this.LogVerbose("_client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str))) if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{ {
if (SharedKeyRequired && str == "SharedKey:") if (SharedKeyRequired && str == "SharedKey:")
{ {
Debug.Console(2, this, "Server asking for shared key, sending"); this.LogVerbose("Server asking for shared key, sending");
SendText(SharedKey + "\n"); SendText(SharedKey + "\n");
} }
else if (SharedKeyRequired && str == "Shared Key Match") else if (SharedKeyRequired && str == "Shared Key Match")
@ -671,7 +673,7 @@ namespace PepperDash.Core
StopWaitForSharedKeyTimer(); StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication"); this.LogVerbose("Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange OnClientReadyForcommunications(true); // Successful key exchange
} }
else else
@ -691,7 +693,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); this.LogError("Error receiving data: {1}. Error: {0}", str, ex.Message);
} }
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive); client.ReceiveDataAsync(Receive);
@ -699,9 +701,8 @@ namespace PepperDash.Core
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null) if (handler != null)
{ {
var gotLock = DequeueLock.TryEnter(); if (System.Threading.Monitor.TryEnter(_dequeueLock))
if (gotLock) System.Threading.Tasks.Task.Run(() => DequeueEvent());
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 else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
@ -711,7 +712,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically. /// It will dequeue items as they are enqueued automatically.
/// </summary> /// </summary>
void DequeueEvent() void DequeueEvent()
@ -731,13 +732,10 @@ namespace PepperDash.Core
} }
catch (Exception e) catch (Exception e)
{ {
this.LogException(e, "DequeueEvent error"); this.LogError(e, "DequeueEvent error: {0}", e.Message);
}
// 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();
} }
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
System.Threading.Monitor.Exit(_dequeueLock);
} }
void HeartbeatStart() void HeartbeatStart()
@ -748,11 +746,15 @@ namespace PepperDash.Core
if (HeartbeatSendTimer == null) if (HeartbeatSendTimer == null)
{ {
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
HeartbeatSendTimer.Start();
} }
if (HeartbeatAckTimer == null) if (HeartbeatAckTimer == null)
{ {
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
} }
} }
@ -762,13 +764,13 @@ namespace PepperDash.Core
if (HeartbeatSendTimer != null) if (HeartbeatSendTimer != null)
{ {
Debug.Console(2, this, "Stoping Heartbeat Send"); this.LogVerbose("Stoping Heartbeat Send");
HeartbeatSendTimer.Stop(); HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null; HeartbeatSendTimer = null;
} }
if (HeartbeatAckTimer != null) if (HeartbeatAckTimer != null)
{ {
Debug.Console(2, this, "Stoping Heartbeat Ack"); this.LogVerbose("Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop(); HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null; HeartbeatAckTimer = null;
} }
@ -777,7 +779,7 @@ namespace PepperDash.Core
void SendHeartbeat(object notused) void SendHeartbeat(object notused)
{ {
this.SendText(HeartbeatString); this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat"); this.LogVerbose("Sending Heartbeat");
} }
@ -796,13 +798,17 @@ namespace PepperDash.Core
{ {
if (HeartbeatAckTimer != null) if (HeartbeatAckTimer != null)
{ {
HeartbeatAckTimer.Reset(HeartbeatInterval * 2); HeartbeatAckTimer.Stop();
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
HeartbeatAckTimer.Start();
} }
else else
{ {
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
} }
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText; return remainingText;
} }
} }
@ -810,7 +816,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogError(ex, "Error checking heartbeat: {0}", ex.Message);
} }
return received; return received;
} }
@ -824,7 +830,7 @@ namespace PepperDash.Core
if (IsConnected) if (IsConnected)
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection"); SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
@ -832,7 +838,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex); this.LogError(ex, "Heartbeat timeout Error on _client: {0}, {1}", Key, ex.Message);
} }
} }
@ -849,7 +855,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// SendText method /// General send method
/// </summary> /// </summary>
public void SendText(string text) public void SendText(string text)
{ {
@ -865,20 +871,20 @@ namespace PepperDash.Core
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0) if (n <= 0)
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
} }
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); this.LogError(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
} }
} }
} }
/// <summary> /// <summary>
/// SendBytes method ///
/// </summary> /// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
@ -891,7 +897,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); this.LogError(ex, "Error sending bytes. Error: {0}", ex.Message);
} }
} }
} }
@ -910,7 +916,7 @@ namespace PepperDash.Core
} }
try try
{ {
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange(); OnConnectionChange();
// The client could be null or disposed by this time... // The client could be null or disposed by this time...
@ -923,7 +929,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); this.LogError(ex, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
} }
} }
@ -953,6 +959,4 @@ namespace PepperDash.Core
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
} }
#endregion #endregion
}
} }

View file

@ -15,17 +15,20 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Generic secure TCP/IP client for server
/// </summary>
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{ {
/// <summary>
/// Generic secure TCP/IP client for server
/// </summary>
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary> /// <summary>
/// Band aid delegate for choked server /// Band aid delegate for choked server
/// </summary> /// </summary>
@ -80,7 +83,7 @@ namespace PepperDash.Core
public string Hostname { get; set; } public string Hostname { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Port /// The port number on which the server is listening.
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; }
@ -113,7 +116,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the SharedKey /// SharedKey is sent for verification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
/// </summary> /// </summary>
public string SharedKey { get; set; } public string SharedKey { get; set; }
@ -123,7 +126,7 @@ namespace PepperDash.Core
private bool WaitingForSharedKeyResponse { get; set; } private bool WaitingForSharedKeyResponse { get; set; }
/// <summary> /// <summary>
/// Gets or sets the BufferSize /// Defaults to 2000
/// </summary> /// </summary>
public int BufferSize { get; set; } public int BufferSize { get; set; }
@ -221,7 +224,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// private Timer for auto reconnect /// private Timer for auto reconnect
/// </summary> /// </summary>
CTimer RetryTimer; System.Timers.Timer RetryTimer;
/// <summary> /// <summary>
@ -253,13 +256,13 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer; System.Timers.Timer HeartbeatSendTimer;
CTimer HeartbeatAckTimer; System.Timers.Timer HeartbeatAckTimer;
/// <summary> /// <summary>
/// Used to force disconnection on a dead connect attempt /// Used to force disconnection on a dead connect attempt
/// </summary> /// </summary>
CTimer ConnectFailTimer; System.Timers.Timer ConnectFailTimer;
CTimer WaitForSharedKey; System.Timers.Timer WaitForSharedKey;
private int ConnectionCount; private int ConnectionCount;
/// <summary> /// <summary>
/// Internal secure client /// Internal secure client
@ -271,7 +274,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Queue lock /// Queue lock
/// </summary> /// </summary>
CCriticalSection DequeueLock = new CCriticalSection(); private readonly object _dequeueLock = new();
/// <summary> /// <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 /// 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
@ -336,8 +339,9 @@ namespace PepperDash.Core
#region Methods #region Methods
/// <summary> /// <summary>
/// Initialize method /// Initializes the client's key property, which is used to identify this client instance.
/// </summary> /// </summary>
/// <param name="key">The unique key that identifies this client instance.</param>
public void Initialize(string key) public void Initialize(string key)
{ {
Key = key; Key = key;
@ -346,7 +350,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary> /// </summary>
/// <param name="clientConfigObject"></param> /// <param name="clientConfigObject">The configuration object containing the client's settings.</param>
public void Initialize(TcpClientConfigObject clientConfigObject) public void Initialize(TcpClientConfigObject clientConfigObject)
{ {
try try
@ -387,7 +391,7 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); this.LogInformation("Program stopping. Closing Client connection");
ProgramIsStopping = true; ProgramIsStopping = true;
Disconnect(); Disconnect();
} }
@ -395,22 +399,22 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Connect method /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
ConnectionCount++; ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); this.LogVerbose("Attempting connect Count:{0}", ConnectionCount);
if (IsConnected) if (IsConnected)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); this.LogInformation("Already connected. Ignoring.");
return; return;
} }
if (IsTryingToConnect) if (IsTryingToConnect)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); this.LogInformation("Already trying to connect. Ignoring.");
return; return;
} }
try try
@ -423,17 +427,17 @@ namespace PepperDash.Core
} }
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); this.LogWarning("DynamicTcpClient: No address set");
return; return;
} }
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); this.LogWarning("DynamicTcpClient: Invalid port");
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); this.LogWarning("DynamicTcpClient: No Shared Key set");
return; return;
} }
@ -454,9 +458,10 @@ namespace PepperDash.Core
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o => ConnectFailTimer = new System.Timers.Timer(30000) { AutoReset = false };
ConnectFailTimer.Elapsed += (s, e) =>
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect) if (IsTryingToConnect)
{ {
IsTryingToConnect = false; IsTryingToConnect = false;
@ -468,12 +473,13 @@ namespace PepperDash.Core
//SecureClient.DisconnectFromServer(); //SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect(); //CheckClosedAndTryReconnect();
} }
}, 30000); };
ConnectFailTimer.Start();
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o => Client.ConnectToServerAsync(o =>
{ {
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null) if (ConnectFailTimer != null)
{ {
@ -483,22 +489,22 @@ namespace PepperDash.Core
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive); o.ReceiveDataAsync(Receive);
if (SharedKeyRequired) if (SharedKeyRequired)
{ {
WaitingForSharedKeyResponse = true; WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer => WaitForSharedKey = new System.Timers.Timer(15000) { AutoReset = false };
WaitForSharedKey.Elapsed += (s, e) =>
{ {
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
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 // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer(); o.DisconnectFromServer();
//CheckClosedAndTryReconnect(); //CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event //OnClientReadyForcommunications(false); // Should send false event
}, 15000); };
WaitForSharedKey.Start();
} }
else else
{ {
@ -512,21 +518,21 @@ namespace PepperDash.Core
} }
else else
{ {
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); this.LogError("Client connection exception: {0}", ex.Message);
IsTryingToConnect = false; IsTryingToConnect = false;
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
} }
/// <summary> /// <summary>
/// Disconnect method ///
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
@ -556,7 +562,7 @@ namespace PepperDash.Core
if (Client != null) if (Client != null)
{ {
//SecureClient.DisconnectFromServer(); //SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange; Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose(); Client.Dispose();
Client = null; Client = null;
@ -578,22 +584,24 @@ namespace PepperDash.Core
{ {
if (Client != null) if (Client != null)
{ {
Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); this.LogVerbose("Cleaning up remotely closed/failed connection.");
Cleanup(); Cleanup();
} }
if (!DisconnectCalledByUser && AutoReconnect) if (!DisconnectCalledByUser && AutoReconnect)
{ {
var halfInterval = AutoReconnectIntervalMs / 2; var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null) if (RetryTimer != null)
{ {
RetryTimer.Stop(); RetryTimer.Stop();
RetryTimer = null; RetryTimer = null;
} }
if(AutoReconnectTriggered != null) if (AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs()); AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime); RetryTimer = new System.Timers.Timer(rndTime) { AutoReset = false };
RetryTimer.Elapsed += (s, e) => Connect();
RetryTimer.Start();
} }
} }
@ -612,13 +620,13 @@ namespace PepperDash.Core
{ {
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str))) if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{ {
if (SharedKeyRequired && str == "SharedKey:") if (SharedKeyRequired && str == "SharedKey:")
{ {
Debug.Console(2, this, "Server asking for shared key, sending"); this.LogVerbose("Server asking for shared key, sending");
SendText(SharedKey + "\n"); SendText(SharedKey + "\n");
} }
else if (SharedKeyRequired && str == "Shared Key Match") else if (SharedKeyRequired && str == "Shared Key Match")
@ -626,7 +634,7 @@ namespace PepperDash.Core
StopWaitForSharedKeyTimer(); StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication"); this.LogVerbose("Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange OnClientReadyForcommunications(true); // Successful key exchange
} }
else else
@ -646,7 +654,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); this.LogError("Error receiving data: {1}. Error: {0}", ex.Message, str);
} }
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive); client.ReceiveDataAsync(Receive);
@ -654,9 +662,8 @@ namespace PepperDash.Core
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null) if (handler != null)
{ {
var gotLock = DequeueLock.TryEnter(); if (Monitor.TryEnter(_dequeueLock))
if (gotLock) Task.Run(() => DequeueEvent());
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 else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
@ -666,7 +673,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically. /// It will dequeue items as they are enqueued automatically.
/// </summary> /// </summary>
void DequeueEvent() void DequeueEvent()
@ -686,28 +693,29 @@ namespace PepperDash.Core
} }
catch (Exception e) catch (Exception e)
{ {
this.LogException(e, "DequeueEvent error"); this.LogError("DequeueEvent error: {0}", e.Message, e);
}
// 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();
} }
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
Monitor.Exit(_dequeueLock);
} }
void HeartbeatStart() void HeartbeatStart()
{ {
if (HeartbeatEnabled) if (HeartbeatEnabled)
{ {
Debug.Console(2, this, "Starting Heartbeat"); this.LogVerbose("Starting Heartbeat");
if (HeartbeatSendTimer == null) if (HeartbeatSendTimer == null)
{ {
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); HeartbeatSendTimer = new System.Timers.Timer(HeartbeatInterval) { AutoReset = true };
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
HeartbeatSendTimer.Start();
} }
if (HeartbeatAckTimer == null) if (HeartbeatAckTimer == null)
{ {
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
} }
} }
@ -717,13 +725,13 @@ namespace PepperDash.Core
if (HeartbeatSendTimer != null) if (HeartbeatSendTimer != null)
{ {
Debug.Console(2, this, "Stoping Heartbeat Send"); this.LogVerbose("Stopping Heartbeat Send");
HeartbeatSendTimer.Stop(); HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null; HeartbeatSendTimer = null;
} }
if (HeartbeatAckTimer != null) if (HeartbeatAckTimer != null)
{ {
Debug.Console(2, this, "Stoping Heartbeat Ack"); this.LogVerbose("Stopping Heartbeat Ack");
HeartbeatAckTimer.Stop(); HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null; HeartbeatAckTimer = null;
} }
@ -732,7 +740,7 @@ namespace PepperDash.Core
void SendHeartbeat(object notused) void SendHeartbeat(object notused)
{ {
this.SendText(HeartbeatString); this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat"); this.LogVerbose("Sending Heartbeat");
} }
@ -751,13 +759,17 @@ namespace PepperDash.Core
{ {
if (HeartbeatAckTimer != null) if (HeartbeatAckTimer != null)
{ {
HeartbeatAckTimer.Reset(HeartbeatInterval * 2); HeartbeatAckTimer.Stop();
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
HeartbeatAckTimer.Start();
} }
else else
{ {
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
} }
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText; return remainingText;
} }
} }
@ -765,7 +777,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogError("Error checking heartbeat: {0}", ex.Message, ex);
} }
return received; return received;
} }
@ -779,7 +791,7 @@ namespace PepperDash.Core
if (IsConnected) if (IsConnected)
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection"); SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
@ -787,7 +799,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); this.LogError("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
} }
} }
@ -804,7 +816,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// SendText method /// General send method
/// </summary> /// </summary>
public void SendText(string text) public void SendText(string text)
{ {
@ -820,20 +832,20 @@ namespace PepperDash.Core
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0) if (n <= 0)
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
} }
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); this.LogError("Error sending text: {1}. Error: {0}", text, ex.Message);
} }
} }
} }
/// <summary> /// <summary>
/// SendBytes method ///
/// </summary> /// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
@ -846,7 +858,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); this.LogError("Error sending bytes. Error: {0}", ex.Message, ex);
} }
} }
} }
@ -865,7 +877,7 @@ namespace PepperDash.Core
} }
try try
{ {
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); this.LogDebug("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange(); OnConnectionChange();
// The client could be null or disposed by this time... // The client could be null or disposed by this time...
@ -878,7 +890,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); this.LogError("Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
} }
} }
@ -904,6 +916,4 @@ namespace PepperDash.Core
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
} }
#endregion #endregion
}
} }

View file

@ -1,29 +1,19 @@
/*PepperDash Technology Corp. using System;
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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Generic secure TCP/IP server
/// </summary>
public class GenericSecureTcpIpServer : Device
{ {
/// <summary>
/// Generic secure TCP/IP server
/// </summary>
public class GenericSecureTcpIpServer : Device
{
#region Events #region Events
/// <summary> /// <summary>
/// Event for Receiving text /// Event for Receiving text
@ -58,7 +48,7 @@ namespace PepperDash.Core
public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
/// <summary> /// <summary>
/// Delegate for ServerHasChokedCallbackDelegate ///
/// </summary> /// </summary>
public delegate void ServerHasChokedCallbackDelegate(); public delegate void ServerHasChokedCallbackDelegate();
@ -69,12 +59,17 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Server listen lock /// Server listen lock
/// </summary> /// </summary>
CCriticalSection ServerCCSection = new CCriticalSection(); private readonly object _serverLock = new();
/// <summary> /// <summary>
/// Queue lock /// Queue lock
/// </summary> /// </summary>
CCriticalSection DequeueLock = new CCriticalSection(); private readonly object _dequeueLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <summary> /// <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 /// 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
@ -96,7 +91,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Timer to operate the bandaid monitor client in a loop. /// Timer to operate the bandaid monitor client in a loop.
/// </summary> /// </summary>
CTimer MonitorClientTimer; Timer MonitorClientTimer;
/// <summary> /// <summary>
/// ///
@ -104,7 +99,7 @@ namespace PepperDash.Core
int MonitorClientFailureCount; int MonitorClientFailureCount;
/// <summary> /// <summary>
/// Gets or sets the MonitorClientMaxFailureCount /// 3 by default
/// </summary> /// </summary>
public int MonitorClientMaxFailureCount { get; set; } public int MonitorClientMaxFailureCount { get; set; }
@ -190,7 +185,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the Port /// Port Server should listen on
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; }
@ -223,7 +218,8 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the SharedKey /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module.
/// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
/// </summary> /// </summary>
public string SharedKey { get; set; } public string SharedKey { get; set; }
@ -247,7 +243,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the HeartbeatRequiredIntervalMs /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary> /// </summary>
public int HeartbeatRequiredIntervalMs { get; set; } public int HeartbeatRequiredIntervalMs { get; set; }
@ -257,12 +253,12 @@ namespace PepperDash.Core
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
/// <summary> /// <summary>
/// Gets or sets the HeartbeatStringToMatch /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
/// </summary> /// </summary>
public string HeartbeatStringToMatch { get; set; } public string HeartbeatStringToMatch { get; set; }
//private timers for Heartbeats per client //private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>(); Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
//flags to show the secure server is waiting for client at index to send the shared key //flags to show the secure server is waiting for client at index to send the shared key
List<uint> WaitingForSharedKey = new List<uint>(); List<uint> WaitingForSharedKey = new List<uint>();
@ -275,7 +271,7 @@ namespace PepperDash.Core
public List<uint> ConnectedClientsIndexes = new List<uint>(); public List<uint> ConnectedClientsIndexes = new List<uint>();
/// <summary> /// <summary>
/// Gets or sets the BufferSize /// Defaults to 2000
/// </summary> /// </summary>
public int BufferSize { get; set; } public int BufferSize { get; set; }
@ -338,7 +334,7 @@ namespace PepperDash.Core
#region Methods - Server Actions #region Methods - Server Actions
/// <summary> /// <summary>
/// KillServer method /// Disconnects all clients and stops the server
/// </summary> /// </summary>
public void KillServer() public void KillServer()
{ {
@ -355,9 +351,6 @@ namespace PepperDash.Core
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key) public void Initialize(string key)
{ {
Key = key; Key = key;
@ -397,22 +390,23 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Listen method /// Start listening on the specified port
/// </summary> /// </summary>
public void Listen() public void Listen()
{ {
ServerCCSection.Enter(); lock (_serverLock)
{
try try
{ {
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); this.LogError("Server '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key)); ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); this.LogError("Server '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key)); ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return; return;
} }
@ -436,43 +430,42 @@ namespace PepperDash.Core
SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING) if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error starting WaitForConnectionAsync {0}", status); this.LogError("Error starting WaitForConnectionAsync {0}", status);
} }
else else
{ {
ServerStopped = false; ServerStopped = false;
} }
OnServerStateChange(SecureServer.State); OnServerStateChange(SecureServer.State);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus); this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
ServerCCSection.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
ServerCCSection.Leave(); this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
} }
} // end lock
} }
/// <summary> /// <summary>
/// StopListening method /// Stop Listeneing
/// </summary> /// </summary>
public void StopListening() public void StopListening()
{ {
try try
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); this.LogVerbose("Stopping Listener");
if (SecureServer != null) if (SecureServer != null)
{ {
SecureServer.Stop(); SecureServer.Stop();
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); this.LogVerbose("Server State: {0}", SecureServer.State);
OnServerStateChange(SecureServer.State); OnServerStateChange(SecureServer.State);
} }
ServerStopped = true; ServerStopped = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
} }
} }
@ -480,27 +473,24 @@ namespace PepperDash.Core
/// Disconnects Client /// Disconnects Client
/// </summary> /// </summary>
/// <param name="client"></param> /// <param name="client"></param>
/// <summary>
/// DisconnectClient method
/// </summary>
public void DisconnectClient(uint client) public void DisconnectClient(uint client)
{ {
try try
{ {
SecureServer.Disconnect(client); SecureServer.Disconnect(client);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); this.LogVerbose("Disconnected client index: {0}", client);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
} }
} }
/// <summary> /// <summary>
/// DisconnectAllClientsForShutdown method /// Disconnect All Clients
/// </summary> /// </summary>
public void DisconnectAllClientsForShutdown() public void DisconnectAllClientsForShutdown()
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); this.LogInformation("Disconnecting All Clients");
if (SecureServer != null) if (SecureServer != null)
{ {
SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange; SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
@ -512,17 +502,17 @@ namespace PepperDash.Core
try try
{ {
SecureServer.Disconnect(i); SecureServer.Disconnect(i);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); this.LogInformation("Disconnected client index: {0}", i);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
} }
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); this.LogInformation("Server Status: {0}", SecureServer.ServerSocketStatus);
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); this.LogInformation("Disconnected All Clients");
ConnectedClientsIndexes.Clear(); ConnectedClientsIndexes.Clear();
if (!ProgramIsStopping) if (!ProgramIsStopping)
@ -538,13 +528,10 @@ namespace PepperDash.Core
/// Broadcast text from server to all connected clients /// Broadcast text from server to all connected clients
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <summary>
/// BroadcastText method
/// </summary>
public void BroadcastText(string text) public void BroadcastText(string text)
{ {
CCriticalSection CCBroadcast = new CCriticalSection(); lock (_broadcastLock)
CCBroadcast.Enter(); {
try try
{ {
if (ConnectedClientsIndexes.Count > 0) if (ConnectedClientsIndexes.Count > 0)
@ -560,13 +547,12 @@ namespace PepperDash.Core
} }
} }
} }
CCBroadcast.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
CCBroadcast.Leave(); this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
} }
} // end lock
} }
/// <summary> /// <summary>
@ -574,9 +560,6 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <summary>
/// SendTextToClient method
/// </summary>
public void SendTextToClient(string text, uint clientIndex) public void SendTextToClient(string text, uint clientIndex)
{ {
try try
@ -590,7 +573,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
} }
} }
@ -608,13 +591,19 @@ namespace PepperDash.Core
if (noDelimiter.Contains(HeartbeatStringToMatch)) if (noDelimiter.Contains(HeartbeatStringToMatch))
{ {
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); {
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else else
{ {
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat // Return Heartbeat
SendTextToClient(HeartbeatStringToMatch, clientIndex); SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText; return remainingText;
@ -623,19 +612,25 @@ namespace PepperDash.Core
else else
{ {
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); {
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else else
{ {
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
} }
return received; return received;
} }
@ -645,16 +640,13 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// GetClientIPAddress method
/// </summary>
public string GetClientIPAddress(uint clientIndex) public string GetClientIPAddress(uint clientIndex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); this.LogInformation("GetClientIPAddress Index: {0}", clientIndex);
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
{ {
var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); this.LogInformation("GetClientIPAddress IPAddreess: {0}", ipa);
return ipa; return ipa;
} }
@ -677,14 +669,13 @@ namespace PepperDash.Core
clientIndex = (uint)o; clientIndex = (uint)o;
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", this.LogInformation("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
var discoResult = SecureServer.Disconnect(clientIndex); var discoResult = SecureServer.Disconnect(clientIndex);
//Debug.Console(1, this, "{0}", discoResult);
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{ {
@ -713,11 +704,9 @@ namespace PepperDash.Core
try try
{ {
// Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber); this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
if (ConnectedClientsIndexes.Contains(clientIndex)) if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex); ConnectedClientsIndexes.Remove(clientIndex);
@ -739,12 +728,12 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex.Message);
} }
//Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state
//after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag
//is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event.
CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null); System.Threading.Tasks.Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
} }
#endregion #endregion
@ -759,7 +748,7 @@ namespace PepperDash.Core
{ {
try try
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", this.LogInformation("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0) if (clientIndex != 0)
@ -779,7 +768,7 @@ namespace PepperDash.Core
} }
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); this.LogInformation("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
} }
else else
{ {
@ -789,7 +778,10 @@ namespace PepperDash.Core
{ {
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
{ {
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
} }
} }
@ -798,19 +790,19 @@ namespace PepperDash.Core
} }
else else
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); this.LogError("Client attempt faulty.");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
} }
// Rearm the listner // Rearm the listner
SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback); SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING) if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Socket status connect callback status {0}", status); this.LogError("Socket status connect callback status {0}", status);
if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS) if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
{ {
// There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact. // There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
@ -847,7 +839,7 @@ namespace PepperDash.Core
if (received != SharedKey) if (received != SharedKey)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
mySecureTCPServer.SendData(clientIndex, b, b.Length); mySecureTCPServer.SendData(clientIndex, b, b.Length);
mySecureTCPServer.Disconnect(clientIndex); mySecureTCPServer.Disconnect(clientIndex);
@ -858,7 +850,7 @@ namespace PepperDash.Core
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
OnServerClientReadyForCommunications(clientIndex); OnServerClientReadyForCommunications(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); this.LogInformation("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
} }
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
{ {
@ -871,7 +863,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex.Message);
} }
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
@ -879,9 +871,8 @@ namespace PepperDash.Core
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null) if (handler != null)
{ {
var gotLock = DequeueLock.TryEnter(); if (System.Threading.Monitor.TryEnter(_dequeueLock))
if (gotLock) System.Threading.Tasks.Task.Run(() => DequeueEvent());
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
} }
} }
else else
@ -891,7 +882,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. /// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically. /// It will dequeue items as they are enqueued automatically.
/// </summary> /// </summary>
void DequeueEvent() void DequeueEvent()
@ -911,13 +902,10 @@ namespace PepperDash.Core
} }
catch (Exception e) catch (Exception e)
{ {
this.LogException(e, "DequeueEvent error"); this.LogError(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();
} }
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
System.Threading.Monitor.Exit(_dequeueLock);
} }
#endregion #endregion
@ -988,7 +976,7 @@ namespace PepperDash.Core
if (MonitorClient != null) if (MonitorClient != null)
MonitorClient.Disconnect(); MonitorClient.Disconnect();
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); this.LogInformation("Program stopping. Closing server");
KillServer(); KillServer();
} }
} }
@ -1014,7 +1002,9 @@ namespace PepperDash.Core
{ {
return; return;
} }
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); MonitorClientTimer = new Timer(60000) { AutoReset = false };
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
MonitorClientTimer.Start();
} }
/// <summary> /// <summary>
@ -1029,7 +1019,7 @@ namespace PepperDash.Core
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); this.LogInformation("Starting monitor check");
MonitorClient.Connect(); MonitorClient.Connect();
// From here MonitorCLient either connects or hangs, MonitorClient will call back // From here MonitorCLient either connects or hangs, MonitorClient will call back
@ -1056,7 +1046,7 @@ namespace PepperDash.Core
{ {
if (args.IsReady) if (args.IsReady)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); this.LogInformation("Monitor client connection success. Disconnecting in 2s");
MonitorClientTimer.Stop(); MonitorClientTimer.Stop();
MonitorClientTimer = null; MonitorClientTimer = null;
MonitorClientFailureCount = 0; MonitorClientFailureCount = 0;
@ -1077,13 +1067,13 @@ namespace PepperDash.Core
StopMonitorClient(); StopMonitorClient();
if (MonitorClientFailureCount < MonitorClientMaxFailureCount) if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
StartMonitorClient(); StartMonitorClient();
} }
else else
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, this.LogError(
"\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************",
MonitorClientMaxFailureCount); MonitorClientMaxFailureCount);
@ -1094,5 +1084,4 @@ namespace PepperDash.Core
} }
} }
#endregion #endregion
}
} }

View file

@ -1,23 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading; using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Org.BouncyCastle.Utilities;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
using Renci.SshNet; using Renci.SshNet;
using Renci.SshNet.Common; using Renci.SshNet.Common;
namespace PepperDash.Core namespace PepperDash.Core;
{
/// <summary>
/// SSH Client
/// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SPlusKey = "Uninitialized SshClient";
/// <summary>
///
/// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SPlusKey = "Uninitialized SshClient";
/// <summary> /// <summary>
/// Object to enable stream debugging /// Object to enable stream debugging
/// </summary> /// </summary>
@ -38,8 +36,13 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange; public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
///// <summary>
/////
///// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
/// <summary> /// <summary>
/// Gets or sets the Hostname /// Address of server
/// </summary> /// </summary>
public string Hostname { get; set; } public string Hostname { get; set; }
@ -49,12 +52,12 @@ namespace PepperDash.Core
public int Port { get; set; } public int Port { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Username /// Username for server
/// </summary> /// </summary>
public string Username { get; set; } public string Username { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Password /// And... Password for server. That was worth documenting!
/// </summary> /// </summary>
public string Password { get; set; } public string Password { get; set; }
@ -64,7 +67,7 @@ namespace PepperDash.Core
public bool IsConnected public bool IsConnected
{ {
// returns false if no client or not connected // returns false if no client or not connected
get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
} }
/// <summary> /// <summary>
@ -76,30 +79,20 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Socket status change event /// SSH Client
/// </summary> /// </summary>
public SocketStatus ClientStatus public SocketStatus ClientStatus
{ {
get { lock (_stateLock) { return _ClientStatus; } } get { return _ClientStatus; }
private set private set
{ {
bool shouldFireEvent = false; if (_ClientStatus == value)
lock (_stateLock) return;
{
if (_ClientStatus != value)
{
_ClientStatus = value; _ClientStatus = value;
shouldFireEvent = true;
}
}
// Fire event outside lock to avoid deadlock
if (shouldFireEvent)
OnConnectionChange(); OnConnectionChange();
} }
} }
SocketStatus _ClientStatus;
private SocketStatus _ClientStatus;
private bool _ConnectEnabled;
/// <summary> /// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
@ -107,7 +100,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public ushort UStatus public ushort UStatus
{ {
get { lock (_stateLock) { return (ushort)_ClientStatus; } } get { return (ushort)_ClientStatus; }
} }
/// <summary> /// <summary>
@ -118,11 +111,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Will be set and unset by connect and disconnect only /// Will be set and unset by connect and disconnect only
/// </summary> /// </summary>
public bool ConnectEnabled public bool ConnectEnabled { get; private set; }
{
get { lock (_stateLock) { return _ConnectEnabled; } }
private set { lock (_stateLock) { _ConnectEnabled = value; } }
}
/// <summary> /// <summary>
/// S+ helper for AutoReconnect /// S+ helper for AutoReconnect
@ -134,24 +123,20 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the AutoReconnectIntervalMs /// Millisecond value, determines the timeout period in between reconnect attempts.
/// Set to 5000 by default
/// </summary> /// </summary>
public int AutoReconnectIntervalMs { get; set; } public int AutoReconnectIntervalMs { get; set; }
private SshClient client; SshClient Client;
private ShellStream shellStream; ShellStream TheStream;
private readonly Timer reconnectTimer; Timer ReconnectTimer;
//Lock object to prevent simulatneous connect/disconnect operations private System.Threading.SemaphoreSlim connectLock = new System.Threading.SemaphoreSlim(1);
//private CCriticalSection connectLock = new CCriticalSection();
private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1);
// Thread-safety lock for state changes private bool DisconnectLogged = false;
private readonly object _stateLock = new object();
private bool disconnectLogged = false;
/// <summary> /// <summary>
/// When true, turns off echo for the SSH session /// When true, turns off echo for the SSH session
@ -173,13 +158,14 @@ namespace PepperDash.Core
Password = password; Password = password;
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
reconnectTimer = new Timer(o => ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
ReconnectTimer.Elapsed += (s, e) =>
{ {
if (ConnectEnabled) // Now thread-safe property access if (ConnectEnabled)
{ {
Connect(); Connect();
} }
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); };
} }
/// <summary> /// <summary>
@ -191,23 +177,24 @@ namespace PepperDash.Core
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
reconnectTimer = new Timer(o => ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
ReconnectTimer.Elapsed += (s, e) =>
{ {
if (ConnectEnabled) // Now thread-safe property access if (ConnectEnabled)
{ {
Connect(); Connect();
} }
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); };
} }
/// <summary> /// <summary>
/// Handles closing this up when the program shuts down /// Handles closing this up when the program shuts down
/// </summary> /// </summary>
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
if (client != null) if (Client != null)
{ {
this.LogDebug("Program stopping. Closing connection"); this.LogDebug("Program stopping. Closing connection");
Disconnect(); Disconnect();
@ -216,7 +203,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Connect method /// Connect to the server, using the provided properties.
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
@ -242,10 +229,13 @@ namespace PepperDash.Core
this.LogDebug("Attempting connect"); this.LogDebug("Attempting connect");
// Cancel reconnect if running. // Cancel reconnect if running.
StopReconnectTimer(); if (ReconnectTimer != null)
{
ReconnectTimer.Stop();
}
// Cleanup the old client if it already exists // Cleanup the old client if it already exists
if (client != null) if (Client != null)
{ {
this.LogDebug("Cleaning up disconnected client"); this.LogDebug("Cleaning up disconnected client");
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
@ -258,14 +248,14 @@ namespace PepperDash.Core
this.LogDebug("Creating new SshClient"); this.LogDebug("Creating new SshClient");
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
client = new SshClient(connectionInfo); Client = new SshClient(connectionInfo);
client.ErrorOccurred += Client_ErrorOccurred; Client.ErrorOccurred += Client_ErrorOccurred;
//Attempt to connect //Attempt to connect
ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
try try
{ {
client.Connect(); Client.Connect();
var modes = new Dictionary<TerminalModes, uint>(); var modes = new Dictionary<TerminalModes, uint>();
@ -274,16 +264,16 @@ namespace PepperDash.Core
modes.Add(TerminalModes.ECHO, 0); modes.Add(TerminalModes.ECHO, 0);
} }
shellStream = client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes); TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes);
if (shellStream.DataAvailable) if (TheStream.DataAvailable)
{ {
// empty the buffer if there is data // empty the buffer if there is data
shellStream.Read(); string str = TheStream.Read();
} }
shellStream.DataReceived += Stream_DataReceived; TheStream.DataReceived += Stream_DataReceived;
this.LogInformation("Connected"); this.LogInformation("Connected");
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
disconnectLogged = false; DisconnectLogged = false;
} }
catch (SshConnectionException e) catch (SshConnectionException e)
{ {
@ -291,57 +281,77 @@ namespace PepperDash.Core
if (ie is SocketException) if (ie is SocketException)
{ {
this.LogError("CONNECTION failure: Cannot reach host"); this.LogException(ie, "CONNECTION failure: Cannot reach host");
this.LogVerbose(ie, "Exception details: ");
} }
if (ie is System.Net.Sockets.SocketException socketException) if (ie is System.Net.Sockets.SocketException socketException)
{ {
this.LogError("Connection failure: Cannot reach {host} on {port}", this.LogException(ie, "Connection failure: Cannot reach {host} on {port}",
Hostname, Port); Hostname, Port);
this.LogVerbose(socketException, "SocketException details: ");
} }
if (ie is SshAuthenticationException) if (ie is SshAuthenticationException)
{ {
this.LogError("Authentication failure for username {userName}", Username); this.LogException(ie, "Authentication failure for username {userName}", Username);
this.LogVerbose(ie, "AuthenticationException details: ");
} }
else else
{ this.LogException(ie, "Error on connect");
this.LogError("Error on connect: {error}", ie.Message);
this.LogVerbose(ie, "Exception details: ");
}
disconnectLogged = true; DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
} }
} }
catch (SshOperationTimeoutException ex) catch (SshOperationTimeoutException ex)
{ {
this.LogWarning("Connection attempt timed out: {message}", ex.Message); this.LogWarning("Connection attempt timed out: {message}", ex.Message);
disconnectLogged = true; DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
}
}
catch (System.Net.Sockets.SocketException e)
{
// SSH.NET only wraps socket failures in SshConnectionException once the SSH
// protocol handshake begins - a failure before that (e.g. DNS resolution failure,
// "Name or service not known", or connection refused) surfaces as a raw
// SocketException here instead. This is an expected/routine "device offline"
// condition (same as the SshConnectionException case above), so log it at Warning
// instead of falling through to the generic handler's LogException/Error-level
// stack trace dump.
this.LogWarning("Connection failure: Cannot reach {host} on {port} ({message})", Hostname, Port, e.Message);
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
} }
} }
catch (Exception e) catch (Exception e)
{ {
this.LogError("Unhandled exception on connect: {error}", e.Message); this.LogException(e, "Unhandled exception on connect");
this.LogVerbose(e, "Exception details: "); DisconnectLogged = true;
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
} }
} }
} }
@ -359,7 +369,7 @@ namespace PepperDash.Core
{ {
ConnectEnabled = false; ConnectEnabled = false;
// Stop trying reconnects, if we are // Stop trying reconnects, if we are
StopReconnectTimer(); ReconnectTimer.Stop();
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
} }
@ -373,12 +383,12 @@ namespace PepperDash.Core
try try
{ {
if (client != null) if (Client != null)
{ {
client.ErrorOccurred -= Client_ErrorOccurred; Client.ErrorOccurred -= Client_ErrorOccurred;
client.Disconnect(); Client.Disconnect();
client.Dispose(); Client.Dispose();
client = null; Client = null;
ClientStatus = status; ClientStatus = status;
this.LogDebug("Disconnected"); this.LogDebug("Disconnected");
} }
@ -392,16 +402,16 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Kills the stream /// Kills the stream
/// </summary> /// </summary>
private void KillStream() void KillStream()
{ {
try try
{ {
if (shellStream != null) if (TheStream != null)
{ {
shellStream.DataReceived -= Stream_DataReceived; TheStream.DataReceived -= Stream_DataReceived;
shellStream.Close(); TheStream.Close();
shellStream.Dispose(); TheStream.Dispose();
shellStream = null; TheStream = null;
this.LogDebug("Disconnected stream"); this.LogDebug("Disconnected stream");
} }
} }
@ -414,7 +424,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Handles the keyboard interactive authentication, should it be required. /// Handles the keyboard interactive authentication, should it be required.
/// </summary> /// </summary>
private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{ {
foreach (AuthenticationPrompt prompt in e.Prompts) foreach (AuthenticationPrompt prompt in e.Prompts)
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
@ -424,7 +434,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing. /// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
/// </summary> /// </summary>
private void Stream_DataReceived(object sender, ShellDataEventArgs e) void Stream_DataReceived(object sender, ShellDataEventArgs e)
{ {
if (((ShellStream)sender).Length <= 0L) if (((ShellStream)sender).Length <= 0L)
{ {
@ -437,14 +447,18 @@ namespace PepperDash.Core
if (bytesHandler != null) if (bytesHandler != null)
{ {
var bytes = Encoding.UTF8.GetBytes(response); var bytes = Encoding.UTF8.GetBytes(response);
this.PrintReceivedBytes(bytes); if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
if (textHandler != null) if (textHandler != null)
{ {
this.PrintReceivedText(response); if (StreamDebugging.RxStreamDebuggingIsEnabled)
this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response));
textHandler(this, new GenericCommMethodReceiveTextArgs(response)); textHandler(this, new GenericCommMethodReceiveTextArgs(response));
} }
@ -456,9 +470,9 @@ namespace PepperDash.Core
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
/// event /// event
/// </summary> /// </summary>
private void Client_ErrorOccurred(object sender, ExceptionEventArgs e) void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{ {
CrestronInvoke.BeginInvoke(o => System.Threading.Tasks.Task.Run(() =>
{ {
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException) if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
this.LogError("Disconnected by remote"); this.LogError("Disconnected by remote");
@ -476,7 +490,9 @@ namespace PepperDash.Core
if (AutoReconnect && ConnectEnabled) if (AutoReconnect && ConnectEnabled)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
} }
}); });
} }
@ -484,9 +500,10 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Helper for ConnectionChange event /// Helper for ConnectionChange event
/// </summary> /// </summary>
private void OnConnectionChange() void OnConnectionChange()
{ {
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this)); if (ConnectionChange != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
} }
#region IBasicCommunication Members #region IBasicCommunication Members
@ -494,17 +511,21 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Sends text to the server /// Sends text to the server
/// </summary> /// </summary>
/// <param name="text">The text to send</param> /// <param name="text"></param>
public void SendText(string text) public void SendText(string text)
{ {
try try
{ {
if (client != null && shellStream != null && IsConnected) if (Client != null && TheStream != null && IsConnected)
{ {
this.PrintSentText(text); if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation(
"Sending {length} characters of text: '{text}'",
text.Length,
ComTextHelper.GetDebugText(text));
shellStream.Write(text); TheStream.Write(text);
shellStream.Flush(); TheStream.Flush();
} }
else else
{ {
@ -516,7 +537,8 @@ namespace PepperDash.Core
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
StartReconnectTimer(); ReconnectTimer.Stop();
ReconnectTimer.Start();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -527,17 +549,18 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Sends Bytes to the server /// Sends Bytes to the server
/// </summary> /// </summary>
/// <param name="bytes">The bytes to send</param> /// <param name="bytes"></param>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
try try
{ {
if (client != null && shellStream != null && IsConnected) if (Client != null && TheStream != null && IsConnected)
{ {
this.PrintSentBytes(bytes); if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
shellStream.Write(bytes, 0, bytes.Length); TheStream.Write(bytes, 0, bytes.Length);
shellStream.Flush(); TheStream.Flush();
} }
else else
{ {
@ -549,7 +572,8 @@ namespace PepperDash.Core
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
StartReconnectTimer(); ReconnectTimer.Stop();
ReconnectTimer.Start();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -558,89 +582,10 @@ namespace PepperDash.Core
} }
#endregion #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> /// <summary>
/// Represents a SshConnectionChangeEventArgs /// Fired when connection changes
/// </summary> /// </summary>
public class SshConnectionChangeEventArgs : EventArgs public class SshConnectionChangeEventArgs : EventArgs
{ {
@ -650,17 +595,17 @@ namespace PepperDash.Core
public bool IsConnected { get; private set; } public bool IsConnected { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the UIsConnected /// Connection Status represented as a ushort
/// </summary> /// </summary>
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } } public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
/// <summary> /// <summary>
/// Gets or sets the Client /// The client
/// </summary> /// </summary>
public GenericSshClient Client { get; private set; } public GenericSshClient Client { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the Status /// Socket Status as represented by
/// </summary> /// </summary>
public ushort Status { get { return Client.UStatus; } } public ushort Status { get { return Client.UStatus; } }

View file

@ -1,18 +1,25 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using Timer = System.Timers.Timer;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
using PepperDash.Core.Logging;
using System.Threading.Tasks;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// A class to handle basic TCP/IP communications with a server
/// </summary>
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{ {
/// <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"; private const string SplusKey = "Uninitialized TcpIpClient";
/// <summary> /// <summary>
/// Object to enable stream debugging /// Object to enable stream debugging
@ -59,7 +66,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the Port /// Port on server
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; }
@ -124,12 +131,6 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } } public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Ushort representation of client status
/// </summary>
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary> /// <summary>
/// Connection failure reason /// Connection failure reason
/// </summary> /// </summary>
@ -168,10 +169,10 @@ namespace PepperDash.Core
} }
//Lock object to prevent simulatneous connect/disconnect operations //Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection(); private readonly object _connectLock = new();
// private Timer for auto reconnect // private Timer for auto reconnect
private CTimer RetryTimer; private Timer RetryTimer;
/// <summary> /// <summary>
/// Constructor /// Constructor
@ -190,10 +191,7 @@ namespace PepperDash.Core
Port = port; Port = port;
BufferSize = bufferSize; BufferSize = bufferSize;
RetryTimer = new CTimer(o => SetupRetryTimer();
{
Reconnect();
}, Timeout.Infinite);
} }
/// <summary> /// <summary>
@ -208,10 +206,7 @@ namespace PepperDash.Core
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
BufferSize = 2000; BufferSize = 2000;
RetryTimer = new CTimer(o => SetupRetryTimer();
{
Reconnect();
}, Timeout.Infinite);
} }
/// <summary> /// <summary>
@ -225,14 +220,19 @@ namespace PepperDash.Core
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
BufferSize = 2000; BufferSize = 2000;
RetryTimer = new CTimer(o => SetupRetryTimer();
{
Reconnect();
}, Timeout.Infinite);
} }
private void SetupRetryTimer()
{
RetryTimer = new Timer { AutoReset = false, Enabled = false };
RetryTimer.Elapsed += (s, e) => Reconnect();
}
/// <summary> /// <summary>
/// Initialize method /// Just to help S+ set the key
/// </summary> /// </summary>
public void Initialize(string key) public void Initialize(string key)
{ {
@ -246,7 +246,7 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
Debug.Console(1, this, "Program stopping. Closing connection"); this.LogInformation("Program stopping. Closing connection");
Deactivate(); Deactivate();
} }
} }
@ -255,9 +255,6 @@ namespace PepperDash.Core
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// Deactivate method
/// </summary>
public override bool Deactivate() public override bool Deactivate()
{ {
RetryTimer.Stop(); RetryTimer.Stop();
@ -271,29 +268,28 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Connect method /// Attempts to connect to the server
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); this.LogWarning("GenericTcpIpClient '{0}': No address set", Key);
return; return;
} }
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key); this.LogWarning("GenericTcpIpClient '{0}': Invalid port", Key);
return; return;
} }
} }
try lock (_connectLock)
{ {
connectLock.Enter();
if (IsConnected) if (IsConnected)
{ {
Debug.Console(1, this, "Connection already connected. Exiting Connect()"); this.LogInformation("Connection already connected. Exiting Connect()");
} }
else else
{ {
@ -306,10 +302,6 @@ namespace PepperDash.Core
_client.ConnectToServerAsync(ConnectToServerCallback); _client.ConnectToServerAsync(ConnectToServerCallback);
} }
} }
finally
{
connectLock.Leave();
}
} }
private void Reconnect() private void Reconnect()
@ -318,53 +310,43 @@ namespace PepperDash.Core
{ {
return; return;
} }
try lock (_connectLock)
{ {
connectLock.Enter();
if (IsConnected || DisconnectCalledByUser == true) if (IsConnected || DisconnectCalledByUser == true)
{ {
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()"); this.LogInformation("Reconnect no longer needed. Exiting Reconnect()");
} }
else else
{ {
Debug.Console(1, this, "Attempting reconnect now"); this.LogInformation("Attempting reconnect now");
_client.ConnectToServerAsync(ConnectToServerCallback); _client.ConnectToServerAsync(ConnectToServerCallback);
} }
} }
finally
{
connectLock.Leave();
}
} }
/// <summary> /// <summary>
/// Disconnect method /// Attempts to disconnect the client
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
try lock (_connectLock)
{ {
connectLock.Enter();
DisconnectCalledByUser = true; DisconnectCalledByUser = true;
// Stop trying reconnects, if we are // Stop trying reconnects, if we are
RetryTimer.Stop(); RetryTimer.Stop();
DisconnectClient(); DisconnectClient();
} }
finally
{
connectLock.Leave();
}
} }
/// <summary> /// <summary>
/// DisconnectClient method /// Does the actual disconnect business
/// </summary> /// </summary>
public void DisconnectClient() public void DisconnectClient()
{ {
if (_client != null) if (_client != null)
{ {
Debug.Console(1, this, "Disconnecting client"); this.LogInformation("Disconnecting client");
if (IsConnected) if (IsConnected)
_client.DisconnectFromServer(); _client.DisconnectFromServer();
} }
@ -378,12 +360,12 @@ namespace PepperDash.Core
{ {
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); this.LogInformation("Server connection result: {0}", c.ClientStatus);
WaitAndTryReconnect(); WaitAndTryReconnect();
} }
else else
{ {
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); this.LogInformation("Server connection result: {0}", c.ClientStatus);
} }
} }
@ -392,22 +374,19 @@ namespace PepperDash.Core
/// </summary> /// </summary>
void WaitAndTryReconnect() void WaitAndTryReconnect()
{ {
CrestronInvoke.BeginInvoke(o => Task.Run(() =>
{ {
try lock (_connectLock)
{ {
connectLock.Enter();
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null) if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
{ {
DisconnectClient(); DisconnectClient();
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus); this.LogInformation("Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Reset(AutoReconnectIntervalMs); RetryTimer.Stop();
RetryTimer.Interval = AutoReconnectIntervalMs;
RetryTimer.Start();
} }
} }
finally
{
connectLock.Leave();
}
}); });
} }
@ -426,7 +405,10 @@ namespace PepperDash.Core
var bytesHandler = BytesReceived; var bytesHandler = BytesReceived;
if (bytesHandler != null) if (bytesHandler != null)
{ {
this.PrintReceivedBytes(bytes); if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
@ -434,7 +416,10 @@ namespace PepperDash.Core
{ {
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
this.PrintReceivedText(str); if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
}
textHandler(this, new GenericCommMethodReceiveTextArgs(str)); textHandler(this, new GenericCommMethodReceiveTextArgs(str));
} }
@ -444,13 +429,14 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// SendText method /// General send method
/// </summary> /// </summary>
public void SendText(string text) public void SendText(string text)
{ {
var bytes = Encoding.GetEncoding(28591).GetBytes(text); var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array // Check debug level before processing byte array
this.PrintSentText(text); if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
if (_client != null) if (_client != null)
_client.SendData(bytes, bytes.Length); _client.SendData(bytes, bytes.Length);
} }
@ -472,12 +458,10 @@ namespace PepperDash.Core
/// Sends Bytes to the server /// Sends Bytes to the server
/// </summary> /// </summary>
/// <param name="bytes"></param> /// <param name="bytes"></param>
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
this.PrintSentBytes(bytes); if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (_client != null) if (_client != null)
_client.SendData(bytes, bytes.Length); _client.SendData(bytes, bytes.Length);
} }
@ -491,12 +475,12 @@ namespace PepperDash.Core
{ {
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
WaitAndTryReconnect(); WaitAndTryReconnect();
} }
else else
{ {
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive); _client.ReceiveDataAsync(Receive);
} }
@ -504,13 +488,13 @@ namespace PepperDash.Core
if (handler != null) if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
} }
} }
/// <summary> /// <summary>
/// Represents a TcpSshPropertiesConfig /// Configuration properties for TCP/SSH Connections
/// </summary> /// </summary>
public class TcpSshPropertiesConfig public class TcpSshPropertiesConfig
{ {
/// <summary> /// <summary>
/// Address to connect to /// Address to connect to
/// </summary> /// </summary>
@ -528,7 +512,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public string Username { get; set; } public string Username { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Password /// Passord credential
/// </summary> /// </summary>
public string Password { get; set; } public string Password { get; set; }
@ -563,7 +547,6 @@ namespace PepperDash.Core
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
Username = ""; Username = "";
Password = ""; Password = "";
DisableSshEcho = false;
}
} }
} }

View file

@ -11,21 +11,20 @@ PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */ ------------------------------------ */
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Generic TCP/IP client for server
/// </summary>
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{ {
/// <summary>
/// Generic TCP/IP client for server
/// </summary>
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary> /// <summary>
/// Band aid delegate for choked server /// Band aid delegate for choked server
/// </summary> /// </summary>
@ -69,7 +68,7 @@ namespace PepperDash.Core
public string Hostname { get; set; } public string Hostname { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Port /// Port on server
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; }
@ -102,7 +101,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the SharedKey /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
/// </summary> /// </summary>
public string SharedKey { get; set; } public string SharedKey { get; set; }
@ -112,7 +111,7 @@ namespace PepperDash.Core
private bool WaitingForSharedKeyResponse { get; set; } private bool WaitingForSharedKeyResponse { get; set; }
/// <summary> /// <summary>
/// Gets or sets the BufferSize /// Defaults to 2000
/// </summary> /// </summary>
public int BufferSize { get; set; } public int BufferSize { get; set; }
@ -210,7 +209,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// private Timer for auto reconnect /// private Timer for auto reconnect
/// </summary> /// </summary>
CTimer RetryTimer; Timer RetryTimer;
/// <summary> /// <summary>
@ -237,13 +236,13 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public int HeartbeatInterval = 50000; public int HeartbeatInterval = 50000;
CTimer HeartbeatSendTimer; Timer HeartbeatSendTimer;
CTimer HeartbeatAckTimer; Timer HeartbeatAckTimer;
/// <summary> /// <summary>
/// Used to force disconnection on a dead connect attempt /// Used to force disconnection on a dead connect attempt
/// </summary> /// </summary>
CTimer ConnectFailTimer; Timer ConnectFailTimer;
CTimer WaitForSharedKey; Timer WaitForSharedKey;
private int ConnectionCount; private int ConnectionCount;
/// <summary> /// <summary>
/// Internal secure client /// Internal secure client
@ -289,7 +288,7 @@ namespace PepperDash.Core
#region Methods #region Methods
/// <summary> /// <summary>
/// Initialize method /// Just to help S+ set the key
/// </summary> /// </summary>
public void Initialize(string key) public void Initialize(string key)
{ {
@ -303,7 +302,7 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection"); this.LogInformation("Program stopping. Closing Client connection");
ProgramIsStopping = true; ProgramIsStopping = true;
Disconnect(); Disconnect();
} }
@ -311,22 +310,22 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Connect method /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
ConnectionCount++; ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); this.LogDebug("Attempting connect Count:{0}", ConnectionCount);
if (IsConnected) if (IsConnected)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); this.LogInformation("Already connected. Ignoring.");
return; return;
} }
if (IsTryingToConnect) if (IsTryingToConnect)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); this.LogInformation("Already trying to connect. Ignoring.");
return; return;
} }
try try
@ -339,17 +338,17 @@ namespace PepperDash.Core
} }
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); this.LogWarning("DynamicTcpClient: No address set");
return; return;
} }
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); this.LogWarning("DynamicTcpClient: Invalid port");
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); this.LogWarning("DynamicTcpClient: No Shared Key set");
return; return;
} }
@ -370,9 +369,10 @@ namespace PepperDash.Core
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o => ConnectFailTimer = new Timer(30000) { AutoReset = false };
ConnectFailTimer.Elapsed += (s, e) =>
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect) if (IsTryingToConnect)
{ {
IsTryingToConnect = false; IsTryingToConnect = false;
@ -384,12 +384,13 @@ namespace PepperDash.Core
//SecureClient.DisconnectFromServer(); //SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect(); //CheckClosedAndTryReconnect();
} }
}, 30000); };
ConnectFailTimer.Start();
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); this.LogDebug("Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o => Client.ConnectToServerAsync(o =>
{ {
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); this.LogDebug("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null) if (ConnectFailTimer != null)
{ {
@ -399,22 +400,22 @@ namespace PepperDash.Core
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive); o.ReceiveDataAsync(Receive);
if (SharedKeyRequired) if (SharedKeyRequired)
{ {
WaitingForSharedKeyResponse = true; WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer => WaitForSharedKey = new Timer(15000) { AutoReset = false };
WaitForSharedKey.Elapsed += (s, e) =>
{ {
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
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 // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer(); o.DisconnectFromServer();
//CheckClosedAndTryReconnect(); //CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event //OnClientReadyForcommunications(false); // Should send false event
}, 15000); };
WaitForSharedKey.Start();
} }
else else
{ {
@ -428,21 +429,22 @@ namespace PepperDash.Core
} }
else else
{ {
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message); this.LogException(ex, "Client connection exception: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
IsTryingToConnect = false; IsTryingToConnect = false;
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
} }
/// <summary> /// <summary>
/// Disconnect method ///
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
@ -472,7 +474,7 @@ namespace PepperDash.Core
if (Client != null) if (Client != null)
{ {
//SecureClient.DisconnectFromServer(); //SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : ""); this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange; Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose(); Client.Dispose();
Client = null; Client = null;
@ -494,20 +496,22 @@ namespace PepperDash.Core
{ {
if (Client != null) if (Client != null)
{ {
Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); this.LogVerbose("Cleaning up remotely closed/failed connection.");
Cleanup(); Cleanup();
} }
if (!DisconnectCalledByUser && AutoReconnect) if (!DisconnectCalledByUser && AutoReconnect)
{ {
var halfInterval = AutoReconnectIntervalMs / 2; var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null) if (RetryTimer != null)
{ {
RetryTimer.Stop(); RetryTimer.Stop();
RetryTimer = null; RetryTimer = null;
} }
RetryTimer = new CTimer(o => Connect(), rndTime); RetryTimer = new Timer(rndTime) { AutoReset = false };
RetryTimer.Elapsed += (s, e) => Connect();
RetryTimer.Start();
} }
} }
@ -526,18 +530,18 @@ namespace PepperDash.Core
{ {
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str))) if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{ {
if (SharedKeyRequired && str == "SharedKey:") if (SharedKeyRequired && str == "SharedKey:")
{ {
Debug.Console(2, this, "Server asking for shared key, sending"); this.LogVerbose("Server asking for shared key, sending");
SendText(SharedKey + "\n"); SendText(SharedKey + "\n");
} }
else if (SharedKeyRequired && str == "Shared Key Match") else if (SharedKeyRequired && str == "Shared Key Match")
{ {
StopWaitForSharedKeyTimer(); StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication"); this.LogVerbose("Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange OnClientReadyForcommunications(true); // Successful key exchange
} }
else else
@ -553,7 +557,8 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); this.LogException(ex, "Error receiving data: {1}. Error: {0}", ex.Message, str);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
} }
} }
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
@ -564,15 +569,19 @@ namespace PepperDash.Core
{ {
if (HeartbeatEnabled) if (HeartbeatEnabled)
{ {
Debug.Console(2, this, "Starting Heartbeat"); this.LogVerbose("Starting Heartbeat");
if (HeartbeatSendTimer == null) if (HeartbeatSendTimer == null)
{ {
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
HeartbeatSendTimer.Start();
} }
if (HeartbeatAckTimer == null) if (HeartbeatAckTimer == null)
{ {
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
} }
} }
@ -582,13 +591,13 @@ namespace PepperDash.Core
if (HeartbeatSendTimer != null) if (HeartbeatSendTimer != null)
{ {
Debug.Console(2, this, "Stoping Heartbeat Send"); this.LogVerbose("Stoping Heartbeat Send");
HeartbeatSendTimer.Stop(); HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null; HeartbeatSendTimer = null;
} }
if (HeartbeatAckTimer != null) if (HeartbeatAckTimer != null)
{ {
Debug.Console(2, this, "Stoping Heartbeat Ack"); this.LogVerbose("Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop(); HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null; HeartbeatAckTimer = null;
} }
@ -597,7 +606,7 @@ namespace PepperDash.Core
void SendHeartbeat(object notused) void SendHeartbeat(object notused)
{ {
this.SendText(HeartbeatString); this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat"); this.LogVerbose("Sending Heartbeat");
} }
@ -616,13 +625,17 @@ namespace PepperDash.Core
{ {
if (HeartbeatAckTimer != null) if (HeartbeatAckTimer != null)
{ {
HeartbeatAckTimer.Reset(HeartbeatInterval * 2); HeartbeatAckTimer.Stop();
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
HeartbeatAckTimer.Start();
} }
else else
{ {
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
} }
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText; return remainingText;
} }
} }
@ -630,7 +643,8 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
} }
return received; return received;
} }
@ -644,7 +658,7 @@ namespace PepperDash.Core
if (IsConnected) if (IsConnected)
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection"); SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect(); CheckClosedAndTryReconnect();
} }
@ -652,7 +666,8 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); this.LogException(ex, "Heartbeat timeout Error on Client: {0}, {1}", Key, ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
} }
} }
@ -669,7 +684,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// SendText method /// General send method
/// </summary> /// </summary>
public void SendText(string text) public void SendText(string text)
{ {
@ -685,20 +700,21 @@ namespace PepperDash.Core
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0) if (n <= 0)
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
} }
}); });
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); this.LogException(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
} }
} }
} }
/// <summary> /// <summary>
/// SendBytes method ///
/// </summary> /// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
@ -711,7 +727,8 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); this.LogException(ex, "Error sending bytes. Error: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
} }
} }
} }
@ -730,7 +747,7 @@ namespace PepperDash.Core
} }
try try
{ {
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange(); OnConnectionChange();
@ -744,7 +761,8 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); this.LogException(ex, "Error in socket status change callback. Error: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
} }
} }
@ -770,6 +788,4 @@ namespace PepperDash.Core
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
} }
#endregion #endregion
}
} }

View file

@ -13,17 +13,18 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Timers;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Generic TCP/IP server device
/// </summary>
public class GenericTcpIpServer : Device
{ {
/// <summary>
/// Generic TCP/IP server device
/// </summary>
public class GenericTcpIpServer : Device
{
#region Events #region Events
/// <summary> /// <summary>
/// Event for Receiving text /// Event for Receiving text
@ -52,7 +53,7 @@ namespace PepperDash.Core
public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; } public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
/// <summary> /// <summary>
/// Delegate for ServerHasChokedCallbackDelegate ///
/// </summary> /// </summary>
public delegate void ServerHasChokedCallbackDelegate(); public delegate void ServerHasChokedCallbackDelegate();
@ -61,9 +62,14 @@ namespace PepperDash.Core
#region Properties/Variables #region Properties/Variables
/// <summary> /// <summary>
/// /// Server listen lock
/// </summary> /// </summary>
CCriticalSection ServerCCSection = new CCriticalSection(); object _serverLock = new();
/// <summary>
/// Broadcast lock
/// </summary>
private readonly object _broadcastLock = new();
/// <summary> /// <summary>
@ -74,7 +80,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Timer to operate the bandaid monitor client in a loop. /// Timer to operate the bandaid monitor client in a loop.
/// </summary> /// </summary>
CTimer MonitorClientTimer; Timer MonitorClientTimer;
/// <summary> /// <summary>
/// ///
@ -82,7 +88,7 @@ namespace PepperDash.Core
int MonitorClientFailureCount; int MonitorClientFailureCount;
/// <summary> /// <summary>
/// Gets or sets the MonitorClientMaxFailureCount /// 3 by default
/// </summary> /// </summary>
public int MonitorClientMaxFailureCount { get; set; } public int MonitorClientMaxFailureCount { get; set; }
@ -171,7 +177,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the Port /// Port Server should listen on
/// </summary> /// </summary>
public int Port { get; set; } public int Port { get; set; }
@ -204,7 +210,8 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the SharedKey /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module.
/// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
/// </summary> /// </summary>
public string SharedKey { get; set; } public string SharedKey { get; set; }
@ -228,7 +235,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Gets or sets the HeartbeatRequiredIntervalMs /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary> /// </summary>
public int HeartbeatRequiredIntervalMs { get; set; } public int HeartbeatRequiredIntervalMs { get; set; }
@ -238,12 +245,12 @@ namespace PepperDash.Core
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } } public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
/// <summary> /// <summary>
/// Gets or sets the HeartbeatStringToMatch /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
/// </summary> /// </summary>
public string HeartbeatStringToMatch { get; set; } public string HeartbeatStringToMatch { get; set; }
//private timers for Heartbeats per client //private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>(); Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
//flags to show the secure server is waiting for client at index to send the shared key //flags to show the secure server is waiting for client at index to send the shared key
List<uint> WaitingForSharedKey = new List<uint>(); List<uint> WaitingForSharedKey = new List<uint>();
@ -256,7 +263,7 @@ namespace PepperDash.Core
public List<uint> ConnectedClientsIndexes = new List<uint>(); public List<uint> ConnectedClientsIndexes = new List<uint>();
/// <summary> /// <summary>
/// Gets or sets the BufferSize /// Defaults to 2000
/// </summary> /// </summary>
public int BufferSize { get; set; } public int BufferSize { get; set; }
@ -319,7 +326,7 @@ namespace PepperDash.Core
#region Methods - Server Actions #region Methods - Server Actions
/// <summary> /// <summary>
/// KillServer method /// Disconnects all clients and stops the server
/// </summary> /// </summary>
public void KillServer() public void KillServer()
{ {
@ -336,9 +343,6 @@ namespace PepperDash.Core
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+ /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key) public void Initialize(string key)
{ {
Key = key; Key = key;
@ -367,33 +371,32 @@ namespace PepperDash.Core
} }
else else
{ {
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
} }
} }
catch catch
{ {
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key); this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
} }
} }
/// <summary> /// <summary>
/// Listen method /// Start listening on the specified port
/// </summary> /// </summary>
public void Listen() public void Listen()
{ {
ServerCCSection.Enter(); lock (_serverLock)
{
try try
{ {
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key); this.LogError("Server '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
return; return;
} }
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key); this.LogError("Server '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
return; return;
} }
if (IsListening) if (IsListening)
@ -419,39 +422,36 @@ namespace PepperDash.Core
ServerStopped = false; ServerStopped = false;
myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
OnServerStateChange(myTcpServer.State); OnServerStateChange(myTcpServer.State);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus); this.LogInformation("TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
// StartMonitorClient(); // StartMonitorClient();
ServerCCSection.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
ServerCCSection.Leave(); this.LogException(ex, "Error with Dynamic Server: {0}", ex.Message);
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
} }
} // end lock
} }
/// <summary> /// <summary>
/// StopListening method /// Stop Listening
/// </summary> /// </summary>
public void StopListening() public void StopListening()
{ {
try try
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); this.LogDebug("Stopping Listener");
if (myTcpServer != null) if (myTcpServer != null)
{ {
myTcpServer.Stop(); myTcpServer.Stop();
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); this.LogDebug("Server State: {0}", myTcpServer.State);
OnServerStateChange(myTcpServer.State); OnServerStateChange(myTcpServer.State);
} }
ServerStopped = true; ServerStopped = true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
} }
} }
@ -459,27 +459,24 @@ namespace PepperDash.Core
/// Disconnects Client /// Disconnects Client
/// </summary> /// </summary>
/// <param name="client"></param> /// <param name="client"></param>
/// <summary>
/// DisconnectClient method
/// </summary>
public void DisconnectClient(uint client) public void DisconnectClient(uint client)
{ {
try try
{ {
myTcpServer.Disconnect(client); myTcpServer.Disconnect(client);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); this.LogVerbose("Disconnected client index: {0}", client);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
} }
} }
/// <summary> /// <summary>
/// DisconnectAllClientsForShutdown method /// Disconnect All Clients
/// </summary> /// </summary>
public void DisconnectAllClientsForShutdown() public void DisconnectAllClientsForShutdown()
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients"); this.LogInformation("Disconnecting All Clients");
if (myTcpServer != null) if (myTcpServer != null)
{ {
myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
@ -491,17 +488,17 @@ namespace PepperDash.Core
try try
{ {
myTcpServer.Disconnect(i); myTcpServer.Disconnect(i);
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); this.LogVerbose("Disconnected client index: {0}", i);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
} }
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); this.LogVerbose("Server Status: {0}", myTcpServer.ServerSocketStatus);
} }
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); this.LogInformation("Disconnected All Clients");
ConnectedClientsIndexes.Clear(); ConnectedClientsIndexes.Clear();
if (!ProgramIsStopping) if (!ProgramIsStopping)
@ -517,13 +514,10 @@ namespace PepperDash.Core
/// Broadcast text from server to all connected clients /// Broadcast text from server to all connected clients
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <summary>
/// BroadcastText method
/// </summary>
public void BroadcastText(string text) public void BroadcastText(string text)
{ {
CCriticalSection CCBroadcast = new CCriticalSection(); lock (_broadcastLock)
CCBroadcast.Enter(); {
try try
{ {
if (ConnectedClientsIndexes.Count > 0) if (ConnectedClientsIndexes.Count > 0)
@ -539,13 +533,12 @@ namespace PepperDash.Core
} }
} }
} }
CCBroadcast.Leave();
} }
catch (Exception ex) catch (Exception ex)
{ {
CCBroadcast.Leave(); this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
} }
} // end lock
} }
/// <summary> /// <summary>
@ -553,9 +546,6 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <summary>
/// SendTextToClient method
/// </summary>
public void SendTextToClient(string text, uint clientIndex) public void SendTextToClient(string text, uint clientIndex)
{ {
try try
@ -569,7 +559,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
} }
} }
@ -587,13 +577,19 @@ namespace PepperDash.Core
if (noDelimiter.Contains(HeartbeatStringToMatch)) if (noDelimiter.Contains(HeartbeatStringToMatch))
{ {
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); {
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else else
{ {
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex); this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat // Return Heartbeat
SendTextToClient(HeartbeatStringToMatch, clientIndex); SendTextToClient(HeartbeatStringToMatch, clientIndex);
return remainingText; return remainingText;
@ -602,19 +598,25 @@ namespace PepperDash.Core
else else
{ {
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs); {
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else else
{ {
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs); var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer); heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
} }
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex); this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
} }
return received; return received;
} }
@ -624,16 +626,13 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="clientIndex"></param> /// <param name="clientIndex"></param>
/// <returns>IP address of the client</returns> /// <returns>IP address of the client</returns>
/// <summary>
/// GetClientIPAddress method
/// </summary>
public string GetClientIPAddress(uint clientIndex) public string GetClientIPAddress(uint clientIndex)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex); this.LogVerbose("GetClientIPAddress Index: {0}", clientIndex);
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex))) if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
{ {
var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa); this.LogVerbose("GetClientIPAddress IPAddreess: {0}", ipa);
return ipa; return ipa;
} }
@ -656,14 +655,13 @@ namespace PepperDash.Core
clientIndex = (uint)o; clientIndex = (uint)o;
address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}", this.LogWarning("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex); address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex); SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
var discoResult = myTcpServer.Disconnect(clientIndex); var discoResult = myTcpServer.Disconnect(clientIndex);
//Debug.Console(1, this, "{0}", discoResult);
if (HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{ {
@ -674,7 +672,8 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key); this.LogException(ex, "Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message);
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
} }
} }
@ -692,7 +691,7 @@ namespace PepperDash.Core
try try
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); this.LogInformation("SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
if (ConnectedClientsIndexes.Contains(clientIndex)) if (ConnectedClientsIndexes.Contains(clientIndex))
@ -711,7 +710,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex);
} }
onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
} }
@ -728,7 +727,7 @@ namespace PepperDash.Core
{ {
try try
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}", this.LogDebug("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
if (clientIndex != 0) if (clientIndex != 0)
@ -748,17 +747,21 @@ namespace PepperDash.Core
} }
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:"); byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { }); server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex)); this.LogDebug("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
} }
else else
{ {
this.LogDebug("Client at index {0} is ready for communications", clientIndex);
OnServerClientReadyForCommunications(clientIndex); OnServerClientReadyForCommunications(clientIndex);
} }
if (HeartbeatRequired) if (HeartbeatRequired)
{ {
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex)) if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
{ {
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs)); var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
} }
} }
@ -767,7 +770,7 @@ namespace PepperDash.Core
} }
else else
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty."); this.LogError("Client attempt faulty.");
if (!ServerStopped) if (!ServerStopped)
{ {
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
@ -777,15 +780,15 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
} }
//Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))",
// server.State, // server.State,
// MaxClients, // MaxClients,
// ServerStopped); // ServerStopped);
if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped) if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection"); this.LogDebug("Waiting for next connection");
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
} }
@ -816,7 +819,7 @@ namespace PepperDash.Core
if (received != SharedKey) if (received != SharedKey)
{ {
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting"); byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
myTCPServer.SendData(clientIndex, b, b.Length); myTCPServer.SendData(clientIndex, b, b.Length);
myTCPServer.Disconnect(clientIndex); myTCPServer.Disconnect(clientIndex);
return; return;
@ -826,7 +829,7 @@ namespace PepperDash.Core
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
myTCPServer.SendDataAsync(clientIndex, success, success.Length, null); myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
OnServerClientReadyForCommunications(clientIndex); OnServerClientReadyForCommunications(clientIndex);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); this.LogDebug("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
} }
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
@ -834,7 +837,7 @@ namespace PepperDash.Core
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex);
} }
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
@ -915,7 +918,7 @@ namespace PepperDash.Core
if (MonitorClient != null) if (MonitorClient != null)
MonitorClient.Disconnect(); MonitorClient.Disconnect();
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server"); this.LogInformation("Program stopping. Closing server");
KillServer(); KillServer();
} }
} }
@ -941,7 +944,9 @@ namespace PepperDash.Core
{ {
return; return;
} }
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000); MonitorClientTimer = new Timer(60000) { AutoReset = false };
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
MonitorClientTimer.Start();
} }
/// <summary> /// <summary>
@ -956,7 +961,7 @@ namespace PepperDash.Core
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange; //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm; MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check"); this.LogDebug("Starting monitor check");
MonitorClient.Connect(); MonitorClient.Connect();
// From here MonitorCLient either connects or hangs, MonitorClient will call back // From here MonitorCLient either connects or hangs, MonitorClient will call back
@ -983,7 +988,7 @@ namespace PepperDash.Core
{ {
if (args.IsReady) if (args.IsReady)
{ {
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s"); this.LogInformation("Monitor client connection success. Disconnecting in 2s");
MonitorClientTimer.Stop(); MonitorClientTimer.Stop();
MonitorClientTimer = null; MonitorClientTimer = null;
MonitorClientFailureCount = 0; MonitorClientFailureCount = 0;
@ -1004,13 +1009,13 @@ namespace PepperDash.Core
StopMonitorClient(); StopMonitorClient();
if (MonitorClientFailureCount < MonitorClientMaxFailureCount) if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
StartMonitorClient(); StartMonitorClient();
} }
else else
{ {
Debug.Console(2, this, Debug.ErrorLogLevel.Error, this.LogError(
"\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************",
MonitorClientMaxFailureCount); MonitorClientMaxFailureCount);
@ -1021,5 +1026,4 @@ namespace PepperDash.Core
} }
} }
#endregion #endregion
}
} }

View file

@ -1,20 +1,23 @@
 
extern alias NewtonsoftJson;
using System; using System;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json; using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Generic UDP Server device
/// </summary>
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
{ {
/// <summary>
/// Generic UDP Server device
/// </summary>
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
{
private const string SplusKey = "Uninitialized Udp Server"; private const string SplusKey = "Uninitialized Udp Server";
/// <summary> /// <summary>
/// Object to enable stream debugging /// Object to enable stream debugging
@ -57,7 +60,7 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// /// Represents a GenericUdpReceiveTextExtraArgs
/// </summary> /// </summary>
public ushort UStatus public ushort UStatus
{ {
@ -131,14 +134,14 @@ namespace PepperDash.Core
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="address"></param> /// <param name="address"></param>
/// <param name="port"></param> /// <param name="port"></param>
/// <param name="bufferSize"></param> /// <param name="buffefSize"></param>
public GenericUdpServer(string key, string address, int port, int bufferSize) public GenericUdpServer(string key, string address, int port, int buffefSize)
: base(key) : base(key)
{ {
StreamDebugging = new CommunicationStreamDebugging(key); StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address; Hostname = address;
Port = port; Port = port;
BufferSize = bufferSize; BufferSize = buffefSize;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
@ -150,9 +153,6 @@ namespace PepperDash.Core
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="address"></param> /// <param name="address"></param>
/// <param name="port"></param> /// <param name="port"></param>
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key, string address, ushort port) public void Initialize(string key, string address, ushort port)
{ {
Key = key; Key = key;
@ -183,50 +183,36 @@ namespace PepperDash.Core
if (programEventType != eProgramStatusEventType.Stopping) if (programEventType != eProgramStatusEventType.Stopping)
return; return;
Debug.Console(1, this, "Program stopping. Disabling Server"); this.LogInformation("Program stopping. Disabling Server");
Disconnect(); Disconnect();
} }
/// <summary> /// <summary>
/// Connect method /// Enables the UDP Server
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
if (Server == null) 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(); Server = new UDPServer();
} }
}
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key); this.LogWarning("GenericUdpServer '{0}': No address set", Key);
return; return;
} }
if (Port < 1 || Port > 65535) if (Port < 1 || Port > 65535)
{ {
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key); this.LogWarning("GenericUdpServer '{0}': Invalid port", Key);
return; return;
} }
} }
var status = Server.EnableUDPServer(Hostname, Port); var status = Server.EnableUDPServer(Hostname, Port);
Debug.Console(2, this, "SocketErrorCode: {0}", status); this.LogVerbose("SocketErrorCode: {0}", status);
if (status == SocketErrorCodes.SOCKET_OK) if (status == SocketErrorCodes.SOCKET_OK)
IsConnected = true; IsConnected = true;
@ -239,11 +225,11 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Disconnect method /// Disabled the UDP Server
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
if (Server != null) if(Server != null)
Server.DisableUDPServer(); Server.DisableUDPServer();
IsConnected = false; IsConnected = false;
@ -261,7 +247,7 @@ namespace PepperDash.Core
/// <param name="numBytes"></param> /// <param name="numBytes"></param>
void Receive(UDPServer server, int numBytes) void Receive(UDPServer server, int numBytes)
{ {
Debug.Console(2, this, "Received {0} bytes", numBytes); this.LogVerbose("Received {0} bytes", numBytes);
try try
{ {
@ -277,17 +263,21 @@ namespace PepperDash.Core
if (dataRecivedExtra != null) if (dataRecivedExtra != null)
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes)); dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
Debug.Console(2, this, "Bytes: {0}", bytes.ToString()); this.LogVerbose("Bytes: {0}", bytes.ToString());
var bytesHandler = BytesReceived; var bytesHandler = BytesReceived;
if (bytesHandler != null) if (bytesHandler != null)
{ {
this.PrintReceivedBytes(bytes); if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
if (textHandler != null) if (textHandler != null)
{ {
this.PrintReceivedText(str); if (StreamDebugging.RxStreamDebuggingIsEnabled)
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
textHandler(this, new GenericCommMethodReceiveTextArgs(str)); textHandler(this, new GenericCommMethodReceiveTextArgs(str));
} }
} }
@ -305,16 +295,14 @@ namespace PepperDash.Core
/// General send method /// General send method
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
/// <summary>
/// SendText method
/// </summary>
public void SendText(string text) public void SendText(string text)
{ {
var bytes = Encoding.GetEncoding(28591).GetBytes(text); var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (IsConnected && Server != null) if (IsConnected && Server != null)
{ {
this.PrintSentText(text); if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogVerbose("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
Server.SendData(bytes, bytes.Length); Server.SendData(bytes, bytes.Length);
} }
@ -324,22 +312,20 @@ namespace PepperDash.Core
/// ///
/// </summary> /// </summary>
/// <param name="bytes"></param> /// <param name="bytes"></param>
/// <summary>
/// SendBytes method
/// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
this.PrintSentBytes(bytes); if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (IsConnected && Server != null) if (IsConnected && Server != null)
Server.SendData(bytes, bytes.Length); Server.SendData(bytes, bytes.Length);
} }
} }
/// <summary> /// <summary>
/// Represents a GenericUdpReceiveTextExtraArgs ///
/// </summary> /// </summary>
public class GenericUdpReceiveTextExtraArgs : EventArgs public class GenericUdpReceiveTextExtraArgs : EventArgs
{ {
/// <summary> /// <summary>
@ -380,11 +366,11 @@ namespace PepperDash.Core
public GenericUdpReceiveTextExtraArgs() { } public GenericUdpReceiveTextExtraArgs() { }
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class UdpServerPropertiesConfig public class UdpServerPropertiesConfig
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -409,5 +395,4 @@ namespace PepperDash.Core
{ {
BufferSize = 32768; BufferSize = 32768;
} }
}
} }

View file

@ -1,12 +1,14 @@
using Newtonsoft.Json; extern alias NewtonsoftJson;
namespace PepperDash.Core using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
namespace PepperDash.Core;
/// <summary>
/// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat
/// </summary>
public class TcpClientConfigObject
{ {
/// <summary>
/// Represents a TcpClientConfigObject
/// </summary>
public class TcpClientConfigObject
{
/// <summary> /// <summary>
/// TcpSsh Properties /// TcpSsh Properties
/// </summary> /// </summary>
@ -55,5 +57,4 @@ namespace PepperDash.Core
/// </summary> /// </summary>
[JsonProperty("receiveQueueSize")] [JsonProperty("receiveQueueSize")]
public int ReceiveQueueSize { get; set; } public int ReceiveQueueSize { get; set; }
}
} }

View file

@ -4,13 +4,13 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core 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>
/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
/// </summary>
public class TcpServerConfigObject
{
/// <summary> /// <summary>
/// Uique key /// Uique key
/// </summary> /// </summary>
@ -56,5 +56,4 @@ namespace PepperDash.Core
/// Receive Queue size must be greater than 20 or defaults to 20 /// Receive Queue size must be greater than 20 or defaults to 20
/// </summary> /// </summary>
public int ReceiveQueueSize { get; set; } public int ReceiveQueueSize { get; set; }
}
} }

View file

@ -4,13 +4,13 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Crestron Control Methods for a comm object
/// </summary>
public enum eControlMethod
{ {
/// <summary>
/// Crestron Control Methods for a comm object
/// </summary>
public enum eControlMethod
{
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@ -76,12 +76,11 @@ namespace PepperDash.Core
/// </summary> /// </summary>
SecureTcpIp, SecureTcpIp,
/// <summary> /// <summary>
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction /// Crestron COM bridge
/// </summary> /// </summary>
ComBridge, ComBridge,
/// <summary> /// <summary>
/// InfinetEX control /// Crestron Infinet EX device
/// </summary> /// </summary>
InfinetEx InfinetEx
}
} }

View file

@ -1,28 +0,0 @@
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

@ -1,16 +1,23 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json; using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// An incoming communication stream
/// </summary>
public interface ICommunicationReceiver : IKeyed
{ {
/// <summary>
/// An incoming communication stream
/// </summary>
public interface ICommunicationReceiver : IKeyed
{
/// <summary> /// <summary>
/// Notifies of bytes received /// Notifies of bytes received
/// </summary> /// </summary>
@ -33,12 +40,12 @@ namespace PepperDash.Core
/// Disconnect from the device /// Disconnect from the device
/// </summary> /// </summary>
void Disconnect(); void Disconnect();
} }
/// <summary> /// <summary>
/// Defines the contract for IBasicCommunication /// Extends <see cref="ICommunicationReceiver"/> with methods for sending text and bytes to a device.
/// </summary> /// </summary>
public interface IBasicCommunication : ICommunicationReceiver public interface IBasicCommunication : ICommunicationReceiver
{ {
/// <summary> /// <summary>
/// Send text to the device /// Send text to the device
@ -53,25 +60,25 @@ namespace PepperDash.Core
void SendBytes(byte[] bytes); void SendBytes(byte[] bytes);
} }
/// <summary> /// <summary>
/// Represents a device that implements IBasicCommunication and IStreamDebugging /// Represents a device that implements IBasicCommunication and IStreamDebugging
/// </summary> /// </summary>
public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging
{ {
} }
/// <summary> /// <summary>
/// Represents a device with stream debugging capablities /// Represents a device with stream debugging capablities
/// </summary> /// </summary>
public interface IStreamDebugging : IKeyed public interface IStreamDebugging : IKeyed
{ {
/// <summary> /// <summary>
/// Object to enable stream debugging /// Object to enable stream debugging
/// </summary> /// </summary>
[JsonProperty("streamDebugging")] [JsonProperty("streamDebugging")]
CommunicationStreamDebugging StreamDebugging { get; } CommunicationStreamDebugging StreamDebugging { get; }
} }
/// <summary> /// <summary>
/// For IBasicCommunication classes that have SocketStatus. GenericSshClient, /// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
@ -88,21 +95,21 @@ namespace PepperDash.Core
/// The current socket status of the client /// The current socket status of the client
/// </summary> /// </summary>
[JsonProperty("clientStatus")] [JsonProperty("clientStatus")]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
SocketStatus ClientStatus { get; } SocketStatus ClientStatus { get; }
} }
/// <summary> /// <summary>
/// Describes a device that implements ISocketStatus and IStreamDebugging /// Describes a device that implements ISocketStatus and IStreamDebugging
/// </summary> /// </summary>
public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging
{ {
} }
/// <summary> /// <summary>
/// Describes a device that can automatically attempt to reconnect /// Describes a device that can automatically attempt to reconnect
/// </summary> /// </summary>
public interface IAutoReconnect public interface IAutoReconnect
{ {
/// <summary> /// <summary>
@ -140,17 +147,17 @@ namespace PepperDash.Core
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
/// <summary> /// <summary>
/// /// Event args for bytes received from a communication method
/// </summary> /// </summary>
public class GenericCommMethodReceiveBytesArgs : EventArgs public class GenericCommMethodReceiveBytesArgs : EventArgs
{ {
/// <summary> /// <summary>
/// Gets or sets the Bytes /// The bytes received
/// </summary> /// </summary>
public byte[] Bytes { get; private set; } public byte[] Bytes { get; private set; }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="bytes"></param> /// <param name="bytes"></param>
public GenericCommMethodReceiveBytesArgs(byte[] bytes) public GenericCommMethodReceiveBytesArgs(byte[] bytes)
@ -165,20 +172,21 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// /// Event args for text received
/// </summary> /// </summary>
public class GenericCommMethodReceiveTextArgs : EventArgs public class GenericCommMethodReceiveTextArgs : EventArgs
{ {
/// <summary> /// <summary>
/// /// The text received
/// </summary> /// </summary>
public string Text { get; private set; } public string Text { get; private set; }
/// <summary> /// <summary>
/// /// The delimiter used to determine the end of a message, if applicable
/// </summary> /// </summary>
public string Delimiter { get; private set; } public string Delimiter { get; private set; }
/// <summary> /// <summary>
/// /// Constructor
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
public GenericCommMethodReceiveTextArgs(string text) public GenericCommMethodReceiveTextArgs(string text)
@ -192,7 +200,7 @@ namespace PepperDash.Core
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="delimiter"></param> /// <param name="delimiter"></param>
public GenericCommMethodReceiveTextArgs(string text, string delimiter) public GenericCommMethodReceiveTextArgs(string text, string delimiter)
: this(text) :this(text)
{ {
Delimiter = delimiter; Delimiter = delimiter;
} }
@ -202,4 +210,42 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public GenericCommMethodReceiveTextArgs() { } public GenericCommMethodReceiveTextArgs() { }
} }
}
/// <summary>
/// Helper class to get escaped text for debugging communication streams
/// </summary>
public class ComTextHelper
{
/// <summary>
/// Gets escaped text for a byte array
/// </summary>
/// <param name="bytes"></param>
/// <returns></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></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></returns>
public static string GetDebugText(string text)
{
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
}
}

View file

@ -1,19 +1,21 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Linq; using System.Linq;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json; using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using Newtonsoft.Json.Linq; using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JToken = NewtonsoftJson::Newtonsoft.Json.Linq.JToken;
using PepperDash.Core; using PepperDash.Core;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Core.Config namespace PepperDash.Core.Config;
{
/// <summary>
/// <summary> /// Reads a Portal formatted config file
/// Reads a Portal formatted config file /// </summary>
/// </summary>
public class PortalConfigReader public class PortalConfigReader
{ {
const string template = "template"; const string template = "template";
@ -56,12 +58,12 @@ namespace PepperDash.Core.Config
var merged = MergeConfigs(jsonObj); var merged = MergeConfigs(jsonObj);
if (jsonObj[systemUrl] != null) if (jsonObj[systemUrl] != null)
{ {
merged[systemUrl] = jsonObj[systemUrl].Value<string>(); merged[systemUrl] = (string)jsonObj[systemUrl];
} }
if (jsonObj[templateUrl] != null) if (jsonObj[templateUrl] != null)
{ {
merged[templateUrl] = jsonObj[templateUrl].Value<string>(); merged[templateUrl] = (string)jsonObj[templateUrl];
} }
jsonObj = merged; jsonObj = merged;
@ -122,38 +124,37 @@ namespace PepperDash.Core.Config
Merge(template[destinationLists], system[destinationLists], destinationLists)); Merge(template[destinationLists], system[destinationLists], destinationLists));
if (system[cameraLists] == null) if (system["cameraLists"] == null)
merged.Add(cameraLists, template[cameraLists]); merged.Add("cameraLists", template["cameraLists"]);
else else
merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists)); merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists"));
if (system[audioControlPointLists] == null) if (system["audioControlPointLists"] == null)
merged.Add(audioControlPointLists, template[audioControlPointLists]); merged.Add("audioControlPointLists", template["audioControlPointLists"]);
else else
merged.Add(audioControlPointLists, merged.Add("audioControlPointLists",
Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists)); Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists"));
// Template tie lines take precedence. Config tool doesn't do them at system // Template tie lines take precedence. Config tool doesn't do them at system
// level anyway... // level anyway...
if (template[tieLines] != null) if (template["tieLines"] != null)
merged.Add(tieLines, template[tieLines]); merged.Add("tieLines", template["tieLines"]);
else if (system[tieLines] != null) else if (system["tieLines"] != null)
merged.Add(tieLines, system[tieLines]); merged.Add("tieLines", system["tieLines"]);
else else
merged.Add(tieLines, new JArray()); merged.Add(tieLines, new JArray());
if (template[joinMaps] != null) if (template["joinMaps"] != null)
merged.Add(joinMaps, template[joinMaps]); merged.Add("joinMaps", template["joinMaps"]);
else else
merged.Add(joinMaps, new JObject()); merged.Add("joinMaps", new JObject());
if (system[global] != null) if (system[global] != null)
merged.Add(global, Merge(template[global], system[global], global)); merged.Add(global, Merge(template[global], system[global], global));
else else
merged.Add(global, template[global]); merged.Add(global, template[global]);
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
return merged; return merged;
} }
@ -254,4 +255,3 @@ namespace PepperDash.Core.Config
return o1; return o1;
} }
} }
}

View file

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

View file

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

View file

@ -2,14 +2,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Core namespace PepperDash.Core;
//*********************************************************************************************************
/// <summary>
/// Represents a Device
/// </summary>
public class Device : IKeyName
{ {
//*********************************************************************************************************
/// <summary>
/// Represents a Device
/// </summary>
public class Device : IKeyName
{
/// <summary> /// <summary>
/// Unique Key /// Unique Key
@ -24,16 +24,6 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public bool Enabled { get; protected set; } 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> _PreActivationActions;
List<Action> _PostActivationActions; List<Action> _PostActivationActions;
@ -62,7 +52,6 @@ namespace PepperDash.Core
public Device(string key, string name) : this(key) public Device(string key, string name) : this(key)
{ {
Name = name; Name = name;
} }
//public Device(DeviceConfig config) //public Device(DeviceConfig config)
@ -75,7 +64,7 @@ namespace PepperDash.Core
/// Adds a pre activation action /// Adds a pre activation action
/// </summary> /// </summary>
/// <param name="act"></param> /// <param name="act"></param>
public void AddPreActivationAction(Action act) protected void AddPreActivationAction(Action act)
{ {
if (_PreActivationActions == null) if (_PreActivationActions == null)
_PreActivationActions = new List<Action>(); _PreActivationActions = new List<Action>();
@ -89,7 +78,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// AddPostActivationAction method /// AddPostActivationAction method
/// </summary> /// </summary>
public void AddPostActivationAction(Action act) protected void AddPostActivationAction(Action act)
{ {
if (_PostActivationActions == null) if (_PostActivationActions == null)
_PostActivationActions = new List<Action>(); _PostActivationActions = new List<Action>();
@ -156,7 +145,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// CustomActivate method /// CustomActivate method
/// </summary> /// </summary>
public virtual bool CustomActivate() { return true; } protected virtual bool CustomActivate() { return true; }
/// <summary> /// <summary>
/// Call to deactivate device - unlink events, etc. Overriding classes do not /// Call to deactivate device - unlink events, etc. Overriding classes do not
@ -168,7 +157,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize() /// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
/// </summary> /// </summary>
public virtual void Initialize() protected virtual void Initialize()
{ {
} }
@ -188,12 +177,8 @@ namespace PepperDash.Core
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is /// <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> /// 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> /// <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() public override string ToString()
{ {
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name); return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
} }
}
} }

View file

@ -1,12 +1,14 @@
using Crestron.SimplSharp; extern alias NewtonsoftJson;
using Newtonsoft.Json;
using Crestron.SimplSharp;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Core namespace PepperDash.Core;
{
/// <summary> /// <summary>
/// Represents a EthernetHelper /// Represents an EthernetHelper.
/// </summary> /// </summary>
public class EthernetHelper public class EthernetHelper
{ {
/// <summary> /// <summary>
@ -114,4 +116,3 @@ namespace PepperDash.Core
} }
} }
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core namespace PepperDash.Core;
{
/// <summary> /// <summary>
/// Bool change event args /// Bool change event args
/// </summary> /// </summary>
@ -169,4 +169,3 @@ namespace PepperDash.Core
Index = index; Index = index;
} }
} }
}

View file

@ -1,39 +0,0 @@
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

@ -1,256 +0,0 @@
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

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.JsonStandardObjects namespace PepperDash.Core.JsonStandardObjects;
{
/// <summary> /// <summary>
/// Constants for simpl modules /// Constants for simpl modules
/// </summary> /// </summary>
@ -74,4 +74,3 @@ namespace PepperDash.Core.JsonStandardObjects
Index = index; Index = index;
} }
} }
}

View file

@ -4,8 +4,8 @@ using Crestron.SimplSharp;
using PepperDash.Core.JsonToSimpl; using PepperDash.Core.JsonToSimpl;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Core.JsonStandardObjects namespace PepperDash.Core.JsonStandardObjects;
{
/// <summary> /// <summary>
/// Device class /// Device class
/// </summary> /// </summary>
@ -183,4 +183,3 @@ namespace PepperDash.Core.JsonStandardObjects
#endregion EventHandler Helpers #endregion EventHandler Helpers
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.JsonStandardObjects namespace PepperDash.Core.JsonStandardObjects;
{
/* /*
Convert JSON snippt to C#: http://json2csharp.com/# Convert JSON snippt to C#: http://json2csharp.com/#
@ -53,7 +53,7 @@ namespace PepperDash.Core.JsonStandardObjects
public class ComParamsConfig public class ComParamsConfig
{ {
/// <summary> /// <summary>
/// Gets or sets the baudRate ///
/// </summary> /// </summary>
public int baudRate { get; set; } public int baudRate { get; set; }
/// <summary> /// <summary>
@ -87,11 +87,11 @@ namespace PepperDash.Core.JsonStandardObjects
// convert properties for simpl // convert properties for simpl
/// <summary> /// <summary>
/// Gets or sets the simplBaudRate ///
/// </summary> /// </summary>
public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } } public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } }
/// <summary> /// <summary>
/// Gets or sets the simplDataBits ///
/// </summary> /// </summary>
public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } } public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } }
/// <summary> /// <summary>
@ -144,11 +144,11 @@ namespace PepperDash.Core.JsonStandardObjects
// convert properties for simpl // convert properties for simpl
/// <summary> /// <summary>
/// Gets or sets the simplPort ///
/// </summary> /// </summary>
public ushort simplPort { get { return Convert.ToUInt16(port); } } public ushort simplPort { get { return Convert.ToUInt16(port); } }
/// <summary> /// <summary>
/// Gets or sets the simplAutoReconnect ///
/// </summary> /// </summary>
public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } } public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } }
/// <summary> /// <summary>
@ -193,7 +193,7 @@ namespace PepperDash.Core.JsonStandardObjects
// convert properties for simpl // convert properties for simpl
/// <summary> /// <summary>
/// Gets or sets the simplControlPortNumber ///
/// </summary> /// </summary>
public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } } public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } }
@ -227,11 +227,11 @@ namespace PepperDash.Core.JsonStandardObjects
// convert properties for simpl // convert properties for simpl
/// <summary> /// <summary>
/// Gets or sets the simplDeviceId ///
/// </summary> /// </summary>
public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } } public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } }
/// <summary> /// <summary>
/// Gets or sets the simplEnabled ///
/// </summary> /// </summary>
public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } } public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } }
@ -254,4 +254,3 @@ namespace PepperDash.Core.JsonStandardObjects
/// </summary> /// </summary>
public List<DeviceConfig> devices { get; set; } public List<DeviceConfig> devices { get; set; }
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// Constants for Simpl modules /// Constants for Simpl modules
/// </summary> /// </summary>
@ -89,7 +89,7 @@ namespace PepperDash.Core.JsonToSimpl
public class SPlusValueWrapper public class SPlusValueWrapper
{ {
/// <summary> /// <summary>
/// Gets or sets the ValueType ///
/// </summary> /// </summary>
public SPlusType ValueType { get; private set; } public SPlusType ValueType { get; private set; }
/// <summary> /// <summary>
@ -140,4 +140,3 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
String String
} }
}

View file

@ -7,11 +7,11 @@ using Serilog.Events;
//using PepperDash.Core; //using PepperDash.Core;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// The global class to manage all the instances of JsonToSimplMaster /// The global class to manage all the instances of JsonToSimplMaster
/// </summary> /// </summary>
public class J2SGlobal public class J2SGlobal
{ {
static List<JsonToSimplMaster> Masters = new List<JsonToSimplMaster>(); static List<JsonToSimplMaster> Masters = new List<JsonToSimplMaster>();
@ -23,9 +23,6 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="master">New master to add</param> /// <param name="master">New master to add</param>
/// ///
/// <summary>
/// AddMaster method
/// </summary>
public static void AddMaster(JsonToSimplMaster master) public static void AddMaster(JsonToSimplMaster master)
{ {
if (master == null) if (master == null)
@ -60,4 +57,3 @@ namespace PepperDash.Core.JsonToSimpl
return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase)); return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase));
} }
} }
}

View file

@ -1,13 +1,15 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq; using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// Represents a JsonToSimplArrayLookupChild /// Used to interact with an array of values with the S+ modules
/// </summary> /// </summary>
public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase
{ {
/// <summary> /// <summary>
@ -77,9 +79,8 @@ namespace PepperDash.Core.JsonToSimpl
} }
/// <summary> /// <summary>
/// ProcessAll method /// Process all values
/// </summary> /// </summary>
/// <inheritdoc />
public override void ProcessAll() public override void ProcessAll()
{ {
if (FindInArray()) if (FindInArray())
@ -129,7 +130,7 @@ namespace PepperDash.Core.JsonToSimpl
var item = array.FirstOrDefault(o => var item = array.FirstOrDefault(o =>
{ {
var prop = o[SearchPropertyName]; var prop = o[SearchPropertyName];
return prop != null && prop.Value<string>() return prop != null && ((string)prop)
.Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase); .Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase);
}); });
if (item == null) if (item == null)
@ -160,4 +161,3 @@ namespace PepperDash.Core.JsonToSimpl
return false; return false;
} }
} }
}

View file

@ -1,13 +1,16 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq; using PepperDash.Core.Logging;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// Base class for JSON objects /// Base class for JSON objects
/// </summary> /// </summary>
public abstract class JsonToSimplChildObjectBase : IKeyed public abstract class JsonToSimplChildObjectBase : IKeyed
{ {
/// <summary> /// <summary>
@ -34,7 +37,7 @@ namespace PepperDash.Core.JsonToSimpl
public SPlusValuesDelegate SetAllPathsDelegate { get; set; } public SPlusValuesDelegate SetAllPathsDelegate { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Key /// Unique identifier for instance
/// </summary> /// </summary>
public string Key { get; protected set; } public string Key { get; protected set; }
@ -50,7 +53,7 @@ namespace PepperDash.Core.JsonToSimpl
public string PathSuffix { get; protected set; } public string PathSuffix { get; protected set; }
/// <summary> /// <summary>
/// Gets or sets the LinkedToObject /// Indicates if the instance is linked to an object
/// </summary> /// </summary>
public bool LinkedToObject { get; protected set; } public bool LinkedToObject { get; protected set; }
@ -89,16 +92,13 @@ namespace PepperDash.Core.JsonToSimpl
if (Master != null) if (Master != null)
Master.AddChild(this); Master.AddChild(this);
else else
Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId); this.LogWarning("JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
} }
/// <summary> /// <summary>
/// Sets the path prefix for the object /// Sets the path prefix for the object
/// </summary> /// </summary>
/// <param name="pathPrefix"></param> /// <param name="pathPrefix"></param>
/// <summary>
/// SetPathPrefix method
/// </summary>
public void SetPathPrefix(string pathPrefix) public void SetPathPrefix(string pathPrefix)
{ {
PathPrefix = pathPrefix; PathPrefix = pathPrefix;
@ -108,7 +108,7 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
public void SetBoolPath(ushort index, string path) public void SetBoolPath(ushort index, string path)
{ {
Debug.Console(1, "JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path); this.LogDebug("JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return; if (path == null || path.Trim() == string.Empty) return;
BoolPaths[index] = path; BoolPaths[index] = path;
} }
@ -118,7 +118,7 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
public void SetUshortPath(ushort index, string path) public void SetUshortPath(ushort index, string path)
{ {
Debug.Console(1, "JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path); this.LogDebug("JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return; if (path == null || path.Trim() == string.Empty) return;
UshortPaths[index] = path; UshortPaths[index] = path;
} }
@ -128,7 +128,7 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
public void SetStringPath(ushort index, string path) public void SetStringPath(ushort index, string path)
{ {
Debug.Console(1, "JSON Child[{0}] SetStringPath {1}={2}", Key, index, path); this.LogDebug("JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return; if (path == null || path.Trim() == string.Empty) return;
StringPaths[index] = path; StringPaths[index] = path;
} }
@ -141,13 +141,13 @@ namespace PepperDash.Core.JsonToSimpl
{ {
if (!LinkedToObject) if (!LinkedToObject)
{ {
Debug.Console(1, this, "Not linked to object in file. Skipping"); this.LogDebug("Not linked to object in file. Skipping");
return; return;
} }
if (SetAllPathsDelegate == null) if (SetAllPathsDelegate == null)
{ {
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring ProcessAll"); this.LogDebug("No SetAllPathsDelegate set. Ignoring ProcessAll");
return; return;
} }
SetAllPathsDelegate(); SetAllPathsDelegate();
@ -207,11 +207,11 @@ namespace PepperDash.Core.JsonToSimpl
bool Process(string path, out string response) bool Process(string path, out string response)
{ {
path = GetFullPath(path); path = GetFullPath(path);
Debug.Console(1, "JSON Child[{0}] Processing {1}", Key, path); this.LogDebug("JSON Child[{0}] Processing {1}", Key, path);
response = ""; response = "";
if (Master == null) if (Master == null)
{ {
Debug.Console(1, "JSONChild[{0}] cannot process without Master attached", Key); this.LogWarning("JSONChild[{0}] cannot process without Master attached", Key);
return false; return false;
} }
@ -233,8 +233,8 @@ namespace PepperDash.Core.JsonToSimpl
if (isCount) if (isCount)
response = (t.HasValues ? t.Children().Count() : 0).ToString(); response = (t.HasValues ? t.Children().Count() : 0).ToString();
else else
response = t.Value<string>(); response = (string)t;
Debug.Console(1, " ='{0}'", response); this.LogDebug(" ='{0}'", response);
return true; return true;
} }
} }
@ -260,13 +260,13 @@ namespace PepperDash.Core.JsonToSimpl
{ {
if (!LinkedToObject) if (!LinkedToObject)
{ {
Debug.Console(1, this, "Not linked to object in file. Skipping"); this.LogDebug("Not linked to object in file. Skipping");
return; return;
} }
if (SetAllPathsDelegate == null) if (SetAllPathsDelegate == null)
{ {
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster"); this.LogDebug("No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
return; return;
} }
SetAllPathsDelegate(); SetAllPathsDelegate();
@ -280,9 +280,6 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="theValue"></param> /// <param name="theValue"></param>
/// <summary>
/// USetBoolValue method
/// </summary>
public void USetBoolValue(ushort key, ushort theValue) public void USetBoolValue(ushort key, ushort theValue)
{ {
SetBoolValue(key, theValue == 1); SetBoolValue(key, theValue == 1);
@ -293,9 +290,6 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="theValue"></param> /// <param name="theValue"></param>
/// <summary>
/// SetBoolValue method
/// </summary>
public void SetBoolValue(ushort key, bool theValue) public void SetBoolValue(ushort key, bool theValue)
{ {
if (BoolPaths.ContainsKey(key)) if (BoolPaths.ContainsKey(key))
@ -307,9 +301,6 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="theValue"></param> /// <param name="theValue"></param>
/// <summary>
/// SetUShortValue method
/// </summary>
public void SetUShortValue(ushort key, ushort theValue) public void SetUShortValue(ushort key, ushort theValue)
{ {
if (UshortPaths.ContainsKey(key)) if (UshortPaths.ContainsKey(key))
@ -321,9 +312,6 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="theValue"></param> /// <param name="theValue"></param>
/// <summary>
/// SetStringValue method
/// </summary>
public void SetStringValue(ushort key, string theValue) public void SetStringValue(ushort key, string theValue)
{ {
if (StringPaths.ContainsKey(key)) if (StringPaths.ContainsKey(key))
@ -335,15 +323,12 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="keyPath"></param> /// <param name="keyPath"></param>
/// <param name="valueToSave"></param> /// <param name="valueToSave"></param>
/// <summary>
/// SetValueOnMaster method
/// </summary>
public void SetValueOnMaster(string keyPath, JValue valueToSave) public void SetValueOnMaster(string keyPath, JValue valueToSave)
{ {
var path = GetFullPath(keyPath); var path = GetFullPath(keyPath);
try try
{ {
Debug.Console(1, "JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave); this.LogDebug("JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
//var token = Master.JsonObject.SelectToken(path); //var token = Master.JsonObject.SelectToken(path);
//if (token != null) // The path exists in the file //if (token != null) // The path exists in the file
@ -351,7 +336,7 @@ namespace PepperDash.Core.JsonToSimpl
} }
catch (Exception e) catch (Exception e)
{ {
Debug.Console(1, "JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e); this.LogDebug("JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
} }
} }
@ -419,4 +404,3 @@ namespace PepperDash.Core.JsonToSimpl
} }
} }
} }
}

View file

@ -1,31 +1,36 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json.Linq; using PepperDash.Core.Logging;
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
/// <summary>
/// Represents a JSON file that can be read and written to
/// </summary>
public class JsonToSimplFileMaster : JsonToSimplMaster
{ {
/// <summary>
/// Represents a JSON file that can be read and written to
/// </summary>
public class JsonToSimplFileMaster : JsonToSimplMaster
{
/// <summary> /// <summary>
/// Sets the filepath as well as registers this with the Global.Masters list /// Sets the filepath as well as registers this with the Global.Masters list
/// </summary> /// </summary>
public string Filepath { get; private set; } public string Filepath { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the ActualFilePath /// Filepath to the actual file that will be read (Portal or local)
/// </summary> /// </summary>
public string ActualFilePath { get; private set; } public string ActualFilePath { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the Filename ///
/// </summary> /// </summary>
public string Filename { get; private set; } public string Filename { get; private set; }
/// <summary> /// <summary>
@ -125,7 +130,7 @@ namespace PepperDash.Core.JsonToSimpl
var fileName = Path.GetFileName(Filepath); var fileName = Path.GetFileName(Filepath);
OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange); OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName); this.LogInformation("Checking '{0}' for '{1}'", fileDirectory, fileName);
if (Directory.Exists(fileDirectory)) if (Directory.Exists(fileDirectory))
{ {
@ -139,7 +144,7 @@ namespace PepperDash.Core.JsonToSimpl
var msg = string.Format("JSON file not found: {0}", Filepath); var msg = string.Format("JSON file not found: {0}", Filepath);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg); CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg); this.LogError(msg);
return; return;
} }
@ -148,18 +153,18 @@ namespace PepperDash.Core.JsonToSimpl
ActualFilePath = actualFile.FullName; ActualFilePath = actualFile.FullName;
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange); OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange); OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Actual JSON file is {0}", ActualFilePath); this.LogInformation("Actual JSON file is {0}", ActualFilePath);
Filename = actualFile.Name; Filename = actualFile.Name;
OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange); OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange);
OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange); OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON Filename is {0}", Filename); this.LogInformation("JSON Filename is {0}", Filename);
FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator); FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator);
OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange); OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange);
OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange); OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON File Path is {0}", FilePathName); this.LogInformation("JSON File Path is {0}", FilePathName);
var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII); var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
@ -172,7 +177,7 @@ namespace PepperDash.Core.JsonToSimpl
else else
{ {
OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange); OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "'{0}' not found", fileDirectory); this.LogError("'{0}' not found", fileDirectory);
} }
} }
catch (Exception e) catch (Exception e)
@ -180,12 +185,12 @@ namespace PepperDash.Core.JsonToSimpl
var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message); var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange); OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg); CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg); this.LogException(e, "EvaluateFile Exception: {0}", e.Message);
var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace); var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange); OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(stackTrace); CrestronConsole.PrintLine(stackTrace);
ErrorLog.Error(stackTrace); this.LogVerbose("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
} }
} }
@ -194,9 +199,6 @@ namespace PepperDash.Core.JsonToSimpl
/// Sets the debug level /// Sets the debug level
/// </summary> /// </summary>
/// <param name="level"></param> /// <param name="level"></param>
/// <summary>
/// setDebugLevel method
/// </summary>
public void setDebugLevel(uint level) public void setDebugLevel(uint level)
{ {
Debug.SetDebugLevel(level); Debug.SetDebugLevel(level);
@ -212,63 +214,31 @@ namespace PepperDash.Core.JsonToSimpl
// Make each child update their values into master object // Make each child update their values into master object
foreach (var child in Children) foreach (var child in Children)
{ {
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster(); child.UpdateInputsForMaster();
} }
if (UnsavedValues == null || UnsavedValues.Count == 0) if (UnsavedValues == null || UnsavedValues.Count == 0)
{ {
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID); this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID);
return; return;
} }
lock (FileLock) lock (FileLock)
{ {
Debug.Console(1, "Saving"); this.LogInformation("Saving");
foreach (var path in UnsavedValues.Keys) foreach (var path in UnsavedValues.Keys)
{ {
var tokenToReplace = JsonObject.SelectToken(path); var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null) if (tokenToReplace != null)
{// It's found {// It's found
tokenToReplace.Replace(UnsavedValues[path]); tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path); this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path);
} }
else // No token. Let's make one else // No token. Let's make one
{ {
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net //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); this.LogWarning("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)) using (StreamWriter sw = new StreamWriter(ActualFilePath))
@ -281,12 +251,13 @@ namespace PepperDash.Core.JsonToSimpl
catch (Exception e) catch (Exception e)
{ {
string err = string.Format("Error writing JSON file:\r{0}", e); string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err); this.LogException(e, "Error writing JSON file: {0}", e.Message);
ErrorLog.Warn(err); this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
return; return;
} }
} }
} }
} }
}
} }

View file

@ -1,10 +1,10 @@
 
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// Represents a JsonToSimplFixedPathObject ///
/// </summary> /// </summary>
public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase
{ {
/// <summary> /// <summary>
@ -15,4 +15,3 @@ namespace PepperDash.Core.JsonToSimpl
this.LinkedToObject = true; this.LinkedToObject = true;
} }
} }
}

View file

@ -1,15 +1,20 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Newtonsoft.Json.Linq; using PepperDash.Core.Logging;
using Renci.SshNet.Messages;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// Represents a JsonToSimplGenericMaster /// Generic Master
/// </summary> /// </summary>
public class JsonToSimplGenericMaster : JsonToSimplMaster public class JsonToSimplGenericMaster : JsonToSimplMaster
{ {
/*****************************************************************************************/ /*****************************************************************************************/
/** Privates **/ /** Privates **/
@ -21,7 +26,7 @@ namespace PepperDash.Core.JsonToSimpl
static object WriteLock = new object(); static object WriteLock = new object();
/// <summary> /// <summary>
/// Gets or sets the SaveCallback /// Callback action for saving
/// </summary> /// </summary>
public Action<string> SaveCallback { get; set; } public Action<string> SaveCallback { get; set; }
@ -71,7 +76,8 @@ namespace PepperDash.Core.JsonToSimpl
} }
catch (Exception e) catch (Exception e)
{ {
Debug.Console(0, this, "JSON parsing failed:\r{0}", e); this.LogException(e, "JSON parsing failed:\r{0}", e.Message);
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
} }
} }
@ -86,37 +92,36 @@ namespace PepperDash.Core.JsonToSimpl
// Make each child update their values into master object // Make each child update their values into master object
foreach (var child in Children) foreach (var child in Children)
{ {
Debug.Console(1, this, "Master. checking child [{0}] for updates to save", child.Key); this.LogDebug("Master. checking child [{0}] for updates to save", child.Key);
child.UpdateInputsForMaster(); child.UpdateInputsForMaster();
} }
if (UnsavedValues == null || UnsavedValues.Count == 0) if (UnsavedValues == null || UnsavedValues.Count == 0)
{ {
Debug.Console(1, this, "Master. No updated values to save. Skipping"); this.LogDebug("Master. No updated values to save. Skipping");
return; return;
} }
lock (WriteLock) lock (WriteLock)
{ {
Debug.Console(1, this, "Saving"); this.LogDebug("Saving");
foreach (var path in UnsavedValues.Keys) foreach (var path in UnsavedValues.Keys)
{ {
var tokenToReplace = JsonObject.SelectToken(path); var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null) if (tokenToReplace != null)
{// It's found {// It's found
tokenToReplace.Replace(UnsavedValues[path]); tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, this, "Master Updating '{0}'", path); this.LogDebug("Master Updating '{0}'", path);
} }
else // No token. Let's make one else // No token. Let's make one
{ {
Debug.Console(1, "Master Cannot write value onto missing property: '{0}'", path); this.LogDebug("Master Cannot write value onto missing property: '{0}'", path);
} }
} }
} }
if (SaveCallback != null) if (SaveCallback != null)
SaveCallback(JsonObject.ToString()); SaveCallback(JsonObject.ToString());
else else
Debug.Console(0, this, "WARNING: No save callback defined."); this.LogDebug("WARNING: No save callback defined.");
} }
} }
}

View file

@ -1,16 +1,22 @@
extern alias NewtonsoftJson;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json; using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
using Newtonsoft.Json.Linq; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
using JsonSerializationException = NewtonsoftJson::Newtonsoft.Json.JsonSerializationException;
using JsonTextReader = NewtonsoftJson::Newtonsoft.Json.JsonTextReader;
using PepperDash.Core.Logging;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
{
/// <summary> /// <summary>
/// Abstract base class for JsonToSimpl interactions /// Abstract base class for JsonToSimpl interactions
/// </summary> /// </summary>
public abstract class JsonToSimplMaster : IKeyed public abstract class JsonToSimplMaster : IKeyed
{ {
/// <summary> /// <summary>
@ -39,7 +45,7 @@ namespace PepperDash.Core.JsonToSimpl
public string Key { get { return UniqueID; } } public string Key { get { return UniqueID; } }
/// <summary> /// <summary>
/// Gets or sets the UniqueID /// A unique ID
/// </summary> /// </summary>
public string UniqueID { get; protected set; } public string UniqueID { get; protected set; }
@ -83,7 +89,7 @@ namespace PepperDash.Core.JsonToSimpl
} }
/// <summary> /// <summary>
/// Gets or sets the JsonObject ///
/// </summary> /// </summary>
public JObject JsonObject { get; protected set; } public JObject JsonObject { get; protected set; }
@ -137,11 +143,10 @@ namespace PepperDash.Core.JsonToSimpl
{ {
if (UnsavedValues.ContainsKey(path)) if (UnsavedValues.ContainsKey(path))
{ {
Debug.Console(0, "Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path); this.LogWarning("Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path);
} }
else else
UnsavedValues.Add(path, value); UnsavedValues.Add(path, value);
//Debug.Console(0, "Master[{0}] Unsaved size={1}", UniqueID, UnsavedValues.Count);
} }
/// <summary> /// <summary>
@ -162,11 +167,7 @@ namespace PepperDash.Core.JsonToSimpl
/// <returns></returns> /// <returns></returns>
public static JObject ParseObject(string json) public static JObject ParseObject(string json)
{ {
#if NET6_0
using (var reader = new JsonTextReader(new System.IO.StringReader(json))) using (var reader = new JsonTextReader(new System.IO.StringReader(json)))
#else
using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json)))
#endif
{ {
var startDepth = reader.Depth; var startDepth = reader.Depth;
var obj = JObject.Load(reader); var obj = JObject.Load(reader);
@ -181,16 +182,10 @@ namespace PepperDash.Core.JsonToSimpl
/// </summary> /// </summary>
/// <param name="json"></param> /// <param name="json"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// ParseArray method
/// </summary>
public static JArray ParseArray(string json) public static JArray ParseArray(string json)
{ {
#if NET6_0
using (var reader = new JsonTextReader(new System.IO.StringReader(json))) using (var reader = new JsonTextReader(new System.IO.StringReader(json)))
#else
using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json)))
#endif
{ {
var startDepth = reader.Depth; var startDepth = reader.Depth;
var obj = JArray.Load(reader); var obj = JArray.Load(reader);
@ -249,4 +244,3 @@ namespace PepperDash.Core.JsonToSimpl
} }
} }
} }
}

View file

@ -1,25 +1,29 @@
using System; extern alias NewtonsoftJson;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json.Linq; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
using PepperDash.Core.Config; using PepperDash.Core.Config;
using PepperDash.Core.Logging;
namespace PepperDash.Core.JsonToSimpl namespace PepperDash.Core.JsonToSimpl;
/// <summary>
/// Portal File Master
/// </summary>
public class JsonToSimplPortalFileMaster : JsonToSimplMaster
{ {
/// <summary>
/// Portal File Master
/// </summary>
public class JsonToSimplPortalFileMaster : JsonToSimplMaster
{
/// <summary> /// <summary>
/// Sets the filepath as well as registers this with the Global.Masters list /// Sets the filepath as well as registers this with the Global.Masters list
/// </summary> /// </summary>
public string PortalFilepath { get; private set; } public string PortalFilepath { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the ActualFilePath /// File path of the actual file being read (Portal or local)
/// </summary> /// </summary>
public string ActualFilePath { get; private set; } public string ActualFilePath { get; private set; }
@ -58,7 +62,7 @@ namespace PepperDash.Core.JsonToSimpl
// If the portal file is xyz.json, then // If the portal file is xyz.json, then
// the file we want to check for first will be called xyz.local.json // the file we want to check for first will be called xyz.local.json
var localFilepath = Path.ChangeExtension(PortalFilepath, "local.json"); var localFilepath = Path.ChangeExtension(PortalFilepath, "local.json");
Debug.Console(0, this, "Checking for local file {0}", localFilepath); this.LogInformation("Checking for local file {0}", localFilepath);
var actualLocalFile = GetActualFileInfoFromPath(localFilepath); var actualLocalFile = GetActualFileInfoFromPath(localFilepath);
if (actualLocalFile != null) if (actualLocalFile != null)
@ -70,7 +74,7 @@ namespace PepperDash.Core.JsonToSimpl
// and create the local. // and create the local.
else else
{ {
Debug.Console(1, this, "Local JSON file not found {0}\rLoading portal JSON file", localFilepath); this.LogInformation("Local JSON file not found {0}\rLoading portal JSON file", localFilepath);
var actualPortalFile = GetActualFileInfoFromPath(portalFilepath); var actualPortalFile = GetActualFileInfoFromPath(portalFilepath);
if (actualPortalFile != null) if (actualPortalFile != null)
{ {
@ -83,14 +87,13 @@ namespace PepperDash.Core.JsonToSimpl
else else
{ {
var msg = string.Format("Portal JSON file not found: {0}", PortalFilepath); var msg = string.Format("Portal JSON file not found: {0}", PortalFilepath);
Debug.Console(1, this, msg); this.LogError(msg);
ErrorLog.Error(msg);
return; return;
} }
} }
// At this point we should have a local file. Do it. // At this point we should have a local file. Do it.
Debug.Console(1, "Reading local JSON file {0}", ActualFilePath); this.LogInformation("Reading local JSON file {0}", ActualFilePath);
string json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII); string json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
@ -104,8 +107,7 @@ namespace PepperDash.Core.JsonToSimpl
catch (Exception e) catch (Exception e)
{ {
var msg = string.Format("JSON parsing failed:\r{0}", e); var msg = string.Format("JSON parsing failed:\r{0}", e);
CrestronConsole.PrintLine(msg); this.LogError(msg);
ErrorLog.Error(msg);
return; return;
} }
} }
@ -146,30 +148,30 @@ namespace PepperDash.Core.JsonToSimpl
// Make each child update their values into master object // Make each child update their values into master object
foreach (var child in Children) foreach (var child in Children)
{ {
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key); this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster(); child.UpdateInputsForMaster();
} }
if (UnsavedValues == null || UnsavedValues.Count == 0) if (UnsavedValues == null || UnsavedValues.Count == 0)
{ {
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID); this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID);
return; return;
} }
lock (FileLock) lock (FileLock)
{ {
Debug.Console(1, "Saving"); this.LogInformation("Saving");
foreach (var path in UnsavedValues.Keys) foreach (var path in UnsavedValues.Keys)
{ {
var tokenToReplace = JsonObject.SelectToken(path); var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null) if (tokenToReplace != null)
{// It's found {// It's found
tokenToReplace.Replace(UnsavedValues[path]); tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path); this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path);
} }
else // No token. Let's make one else // No token. Let's make one
{ {
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net //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); this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
} }
} }
@ -183,12 +185,11 @@ namespace PepperDash.Core.JsonToSimpl
catch (Exception e) catch (Exception e)
{ {
string err = string.Format("Error writing JSON file:\r{0}", e); string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err); this.LogException(e, "Error writing JSON file: {0}", e.Message);
ErrorLog.Warn(err); this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
return; return;
} }
} }
} }
} }
} }
}

View file

@ -7,13 +7,13 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace PepperDash.Core.Logging namespace PepperDash.Core.Logging;
/// <summary>
/// Enriches log events with Crestron-specific context properties, such as the application name based on the device platform.
/// </summary>
public class CrestronEnricher : ILogEventEnricher
{ {
/// <summary>
/// Represents a CrestronEnricher
/// </summary>
public class CrestronEnricher : ILogEventEnricher
{
static readonly string _appName; static readonly string _appName;
static CrestronEnricher() static CrestronEnricher()
@ -31,13 +31,14 @@ namespace PepperDash.Core.Logging
/// <summary> /// <summary>
/// Enrich method /// Enriches the log event with Crestron-specific properties.
/// </summary> /// </summary>
/// <param name="logEvent"></param>
/// <param name="propertyFactory"></param>
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{ {
var property = propertyFactory.CreateProperty("App", _appName); var property = propertyFactory.CreateProperty("App", _appName);
logEvent.AddOrUpdateProperty(property); logEvent.AddOrUpdateProperty(property);
} }
}
} }

File diff suppressed because it is too large Load diff

View file

@ -9,18 +9,12 @@ using System.IO;
using System.Text; using System.Text;
namespace PepperDash.Core namespace PepperDash.Core;
public class DebugConsoleSink : ILogEventSink
{ {
/// <summary>
/// Represents a DebugConsoleSink
/// </summary>
public class DebugConsoleSink : ILogEventSink
{
private readonly ITextFormatter _textFormatter; private readonly ITextFormatter _textFormatter;
/// <summary>
/// Emit method
/// </summary>
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
if (!Debug.IsRunningOnAppliance) return; if (!Debug.IsRunningOnAppliance) return;
@ -46,19 +40,14 @@ namespace PepperDash.Core
_textFormatter = formatProvider ?? new JsonFormatter(); _textFormatter = formatProvider ?? new JsonFormatter();
} }
} }
public static class DebugConsoleSinkExtensions public static class DebugConsoleSinkExtensions
{ {
/// <summary>
/// DebugConsoleSink method
/// </summary>
public static LoggerConfiguration DebugConsoleSink( public static LoggerConfiguration DebugConsoleSink(
this LoggerSinkConfiguration loggerConfiguration, this LoggerSinkConfiguration loggerConfiguration,
ITextFormatter formatProvider = null) ITextFormatter formatProvider = null)
{ {
return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider)); return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider));
} }
}
} }

View file

@ -1,293 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Represents a debugging context
/// </summary>
public class DebugContext
{
/// <summary>
/// Describes the folder location where a given program stores it's debug level memory. By default, the
/// file written will be named appNdebug where N is 1-10.
/// </summary>
public string Key { get; private set; }
/// <summary>
/// The name of the file containing the current debug settings.
/// </summary>
//string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber);
DebugContextSaveData SaveData;
int SaveTimeoutMs = 30000;
CTimer SaveTimer;
static List<DebugContext> Contexts = new List<DebugContext>();
/// <summary>
/// Creates or gets a debug context
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
/// <summary>
/// GetDebugContext method
/// </summary>
public static DebugContext GetDebugContext(string key)
{
var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (context == null)
{
context = new DebugContext(key);
Contexts.Add(context);
}
return context;
}
/// <summary>
/// Do not use. For S+ access.
/// </summary>
public DebugContext() { }
DebugContext(string key)
{
Key = key;
if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro)
{
// Add command to console
CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug",
"appdebug:P [0-2]: Sets the application's console debug message level",
ConsoleAccessLevelEnum.AccessOperator);
}
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
LoadMemory();
}
/// <summary>
/// Used to save memory when shutting down
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (SaveTimer != null)
{
SaveTimer.Stop();
SaveTimer = null;
}
Console(0, "Saving debug settings");
SaveMemory();
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="levelString"></param>
/// <summary>
/// SetDebugFromConsole method
/// </summary>
public void SetDebugFromConsole(string levelString)
{
try
{
if (string.IsNullOrEmpty(levelString.Trim()))
{
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level);
return;
}
SetDebugLevel(Convert.ToInt32(levelString));
}
catch
{
CrestronConsole.PrintLine("Usage: appdebug:P [0-2]");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0 (no debug), 1 (critical), 2 (all messages)</param>
/// <summary>
/// SetDebugLevel method
/// </summary>
public void SetDebugLevel(int level)
{
if (level <= 2)
{
SaveData.Level = level;
SaveMemoryOnTimeout();
CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}",
InitialParametersClass.ApplicationNumber, SaveData.Level);
}
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
public void Console(uint level, string format, params object[] items)
{
if (SaveData.Level >= level)
CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber,
string.Format(format, items));
}
/// <summary>
/// Console method
/// </summary>
public void Console(uint level, IKeyed dev, string format, params object[] items)
{
if (SaveData.Level >= level)
Console(level, "[{0}] {1}", dev.Key, string.Format(format, items));
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="dev"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
if (SaveData.Level >= level)
{
var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items));
Console(level, str);
LogError(errorLogLevel, str);
}
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
public void Console(uint level, Debug.ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
if (SaveData.Level >= level)
{
var str = string.Format(format, items);
Console(level, str);
LogError(errorLogLevel, str);
}
}
/// <summary>
///
/// </summary>
/// <param name="errorLogLevel"></param>
/// <param name="str"></param>
/// <summary>
/// LogError method
/// </summary>
public void LogError(Debug.ErrorLogLevel errorLogLevel, string str)
{
string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str);
switch (errorLogLevel)
{
case Debug.ErrorLogLevel.Error:
ErrorLog.Error(msg);
break;
case Debug.ErrorLogLevel.Warning:
ErrorLog.Warn(msg);
break;
case Debug.ErrorLogLevel.Notice:
ErrorLog.Notice(msg);
break;
}
}
/// <summary>
/// Writes the memory object after timeout
/// </summary>
void SaveMemoryOnTimeout()
{
if (SaveTimer == null)
SaveTimer = new CTimer(o =>
{
SaveTimer = null;
SaveMemory();
}, SaveTimeoutMs);
else
SaveTimer.Reset(SaveTimeoutMs);
}
/// <summary>
/// Writes the memory - use SaveMemoryOnTimeout
/// </summary>
void SaveMemory()
{
using (StreamWriter sw = new StreamWriter(GetMemoryFileName()))
{
var json = JsonConvert.SerializeObject(SaveData);
sw.Write(json);
sw.Flush();
}
}
/// <summary>
///
/// </summary>
void LoadMemory()
{
var file = GetMemoryFileName();
if (File.Exists(file))
{
using (StreamReader sr = new StreamReader(file))
{
var data = JsonConvert.DeserializeObject<DebugContextSaveData>(sr.ReadToEnd());
if (data != null)
{
SaveData = data;
Debug.Console(1, "Debug memory restored from file");
return;
}
else
SaveData = new DebugContextSaveData();
}
}
}
/// <summary>
/// Helper to get the file path for this app's debug memory
/// </summary>
string GetMemoryFileName()
{
return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key);
}
}
/// <summary>
///
/// </summary>
public class DebugContextSaveData
{
/// <summary>
///
/// </summary>
public int Level { get; set; }
}
}

View file

@ -3,16 +3,10 @@ using Crestron.SimplSharp.CrestronLogger;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
namespace PepperDash.Core.Logging namespace PepperDash.Core.Logging;
public class DebugCrestronLoggerSink : ILogEventSink
{ {
/// <summary>
/// Represents a DebugCrestronLoggerSink
/// </summary>
public class DebugCrestronLoggerSink : ILogEventSink
{
/// <summary>
/// Emit method
/// </summary>
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
if (!Debug.IsRunningOnAppliance) return; if (!Debug.IsRunningOnAppliance) return;
@ -31,5 +25,4 @@ namespace PepperDash.Core.Logging
{ {
CrestronLogger.Initialize(1, LoggerModeEnum.RM); CrestronLogger.Initialize(1, LoggerModeEnum.RM);
} }
}
} }

View file

@ -9,13 +9,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace PepperDash.Core.Logging namespace PepperDash.Core.Logging;
public class DebugErrorLogSink : ILogEventSink
{ {
/// <summary>
/// Represents a DebugErrorLogSink
/// </summary>
public class DebugErrorLogSink : ILogEventSink
{
private ITextFormatter _formatter; private ITextFormatter _formatter;
private Dictionary<LogEventLevel, Action<string>> _errorLogMap = new Dictionary<LogEventLevel, Action<string>> private Dictionary<LogEventLevel, Action<string>> _errorLogMap = new Dictionary<LogEventLevel, Action<string>>
@ -27,9 +24,6 @@ namespace PepperDash.Core.Logging
{LogEventLevel.Error, (msg) => ErrorLog.Error(msg) }, {LogEventLevel.Error, (msg) => ErrorLog.Error(msg) },
{LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) } {LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) }
}; };
/// <summary>
/// Emit method
/// </summary>
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
string message; string message;
@ -67,5 +61,4 @@ namespace PepperDash.Core.Logging
{ {
_formatter = formatter; _formatter = formatter;
} }
}
} }

View file

@ -2,112 +2,72 @@
using Serilog.Events; using Serilog.Events;
using Log = PepperDash.Core.Debug; using Log = PepperDash.Core.Debug;
namespace PepperDash.Core.Logging namespace PepperDash.Core.Logging;
public static class DebugExtensions
{ {
public static class DebugExtensions
{
/// <summary>
/// LogException method
/// </summary>
public static void LogException(this IKeyed device, Exception ex, string message, params object[] args) public static void LogException(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogMessage(ex, message, device: device, args); Log.LogMessage(ex, message, device, args);
} }
/// <summary>
/// LogVerbose method
/// </summary>
public static void LogVerbose(this IKeyed device, Exception ex, string message, params object[] args) public static void LogVerbose(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogVerbose(ex, device, message, args); Log.LogMessage(LogEventLevel.Verbose, ex, message, device, args);
} }
/// <summary>
/// LogVerbose method
/// </summary>
public static void LogVerbose(this IKeyed device, string message, params object[] args) public static void LogVerbose(this IKeyed device, string message, params object[] args)
{ {
Log.LogVerbose(device, message, args); Log.LogMessage(LogEventLevel.Verbose, device, message, args);
} }
/// <summary>
/// LogDebug method
/// </summary>
public static void LogDebug(this IKeyed device, Exception ex, string message, params object[] args) public static void LogDebug(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogDebug(ex, device, message, args); Log.LogMessage(LogEventLevel.Debug, ex, message, device, args);
} }
/// <summary>
/// LogDebug method
/// </summary>
public static void LogDebug(this IKeyed device, string message, params object[] args) public static void LogDebug(this IKeyed device, string message, params object[] args)
{ {
Log.LogDebug(device, message, args); Log.LogMessage(LogEventLevel.Debug, device, message, args);
} }
/// <summary>
/// LogInformation method
/// </summary>
public static void LogInformation(this IKeyed device, Exception ex, string message, params object[] args) public static void LogInformation(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogInformation(ex, device, message, args); Log.LogMessage(LogEventLevel.Information, ex, message, device, args);
} }
/// <summary>
/// LogInformation method
/// </summary>
public static void LogInformation(this IKeyed device, string message, params object[] args) public static void LogInformation(this IKeyed device, string message, params object[] args)
{ {
Log.LogInformation(device, message, args); Log.LogMessage(LogEventLevel.Information, device, message, args);
} }
/// <summary>
/// LogWarning method
/// </summary>
public static void LogWarning(this IKeyed device, Exception ex, string message, params object[] args) public static void LogWarning(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogWarning(ex, device, message, args); Log.LogMessage(LogEventLevel.Warning, ex, message, device, args);
} }
/// <summary>
/// LogWarning method
/// </summary>
public static void LogWarning(this IKeyed device, string message, params object[] args) public static void LogWarning(this IKeyed device, string message, params object[] args)
{ {
Log.LogWarning(device, message, args); Log.LogMessage(LogEventLevel.Warning, device, message, args);
} }
/// <summary>
/// LogError method
/// </summary>
public static void LogError(this IKeyed device, Exception ex, string message, params object[] args) public static void LogError(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogError(ex, device, message, args); Log.LogMessage(LogEventLevel.Error, ex, message, device, args);
} }
/// <summary>
/// LogError method
/// </summary>
public static void LogError(this IKeyed device, string message, params object[] args) public static void LogError(this IKeyed device, string message, params object[] args)
{ {
Log.LogError(device, message, args); Log.LogMessage(LogEventLevel.Error, device, message, args);
} }
/// <summary>
/// LogFatal method
/// </summary>
public static void LogFatal(this IKeyed device, Exception ex, string message, params object[] args) public static void LogFatal(this IKeyed device, Exception ex, string message, params object[] args)
{ {
Log.LogFatal(ex, device, message, args); Log.LogMessage(LogEventLevel.Fatal, ex, message, device, args);
} }
/// <summary>
/// LogFatal method
/// </summary>
public static void LogFatal(this IKeyed device, string message, params object[] args) public static void LogFatal(this IKeyed device, string message, params object[] args)
{ {
Log.LogFatal(device, message, args); Log.LogMessage(LogEventLevel.Fatal, device, message, args);
}
} }
} }

View file

@ -1,26 +1,30 @@
using System.Collections.Generic; extern alias NewtonsoftJson;
using Crestron.SimplSharp;
using Newtonsoft.Json;
namespace PepperDash.Core.Logging using System.Collections.Generic;
using System.Threading;
using Crestron.SimplSharp;
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
namespace PepperDash.Core.Logging;
/// <summary>
/// Class to persist current Debug settings across program restarts
/// </summary>
public class DebugContextCollection
{ {
/// <summary>
/// Represents a DebugContextCollection
/// </summary>
public class DebugContextCollection
{
/// <summary> /// <summary>
/// To prevent threading issues with the DeviceDebugSettings collection /// To prevent threading issues with the DeviceDebugSettings collection
/// </summary> /// </summary>
private readonly CCriticalSection _deviceDebugSettingsLock; private readonly object _deviceDebugSettingsLock = new();
[JsonProperty("items")] private readonly Dictionary<string, DebugContextItem> _items; [JsonProperty("items")]
private readonly Dictionary<string, DebugContextItem> _items = new Dictionary<string, DebugContextItem>();
/// <summary> /// <summary>
/// Collection of the debug settings for each device where the dictionary key is the device key /// Collection of the debug settings for each device where the dictionary key is the device key
/// </summary> /// </summary>
[JsonProperty("deviceDebugSettings")] [JsonProperty("deviceDebugSettings")]
private Dictionary<string, object> DeviceDebugSettings { get; set; } private Dictionary<string, object> DeviceDebugSettings { get; set; } = new Dictionary<string, object>();
/// <summary> /// <summary>
@ -28,9 +32,7 @@ namespace PepperDash.Core.Logging
/// </summary> /// </summary>
public DebugContextCollection() public DebugContextCollection()
{ {
_deviceDebugSettingsLock = new CCriticalSection();
DeviceDebugSettings = new Dictionary<string, object>();
_items = new Dictionary<string, DebugContextItem>();
} }
/// <summary> /// <summary>
@ -71,15 +73,10 @@ namespace PepperDash.Core.Logging
/// <param name="deviceKey"></param> /// <param name="deviceKey"></param>
/// <param name="settings"></param> /// <param name="settings"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// SetDebugSettingsForKey method
/// </summary>
public void SetDebugSettingsForKey(string deviceKey, object settings) public void SetDebugSettingsForKey(string deviceKey, object settings)
{ {
try lock (_deviceDebugSettingsLock)
{ {
_deviceDebugSettingsLock.Enter();
if (DeviceDebugSettings.ContainsKey(deviceKey)) if (DeviceDebugSettings.ContainsKey(deviceKey))
{ {
DeviceDebugSettings[deviceKey] = settings; DeviceDebugSettings[deviceKey] = settings;
@ -87,10 +84,6 @@ namespace PepperDash.Core.Logging
else else
DeviceDebugSettings.Add(deviceKey, settings); DeviceDebugSettings.Add(deviceKey, settings);
} }
finally
{
_deviceDebugSettingsLock.Leave();
}
} }
/// <summary> /// <summary>
@ -98,20 +91,17 @@ namespace PepperDash.Core.Logging
/// </summary> /// </summary>
/// <param name="deviceKey"></param> /// <param name="deviceKey"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// GetDebugSettingsForKey method
/// </summary>
public object GetDebugSettingsForKey(string deviceKey) public object GetDebugSettingsForKey(string deviceKey)
{ {
return DeviceDebugSettings[deviceKey]; return DeviceDebugSettings[deviceKey];
} }
} }
/// <summary> /// <summary>
/// Contains information about /// Contains information about
/// </summary> /// </summary>
public class DebugContextItem public class DebugContextItem
{ {
/// <summary> /// <summary>
/// The level of debug messages to print /// The level of debug messages to print
/// </summary> /// </summary>
@ -123,5 +113,4 @@ namespace PepperDash.Core.Logging
/// </summary> /// </summary>
[JsonProperty("doNotLoadOnNextBoot")] [JsonProperty("doNotLoadOnNextBoot")]
public bool DoNotLoadOnNextBoot { get; set; } public bool DoNotLoadOnNextBoot { get; set; }
}
} }

View file

@ -1,119 +1,200 @@
using System; extern alias NewtonsoftJson;
using System.Collections.Generic;
using System.Linq; using System;
using System.Text; using System.Security.Authentication;
using System.Threading.Tasks; using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Crestron.SimplSharp;
using Serilog; using Serilog;
using Serilog.Configuration;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
using Serilog.Configuration;
using WebSocketSharp.Server;
using Crestron.SimplSharp;
using WebSocketSharp;
using System.Security.Authentication;
using WebSocketSharp.Net;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using System.IO;
using Org.BouncyCastle.Asn1.X509;
using Serilog.Formatting; using Serilog.Formatting;
using Newtonsoft.Json.Linq; using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using Serilog.Formatting.Json; using Serilog.Formatting.Json;
using System.IO;
using WebSocketSharp;
using WebSocketSharp.Server;
using WebSocketSharp.Net;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected
/// WebSocket clients.
/// </summary>
/// <remarks>This class implements the <see cref="ILogEventSink"/> interface and is designed to send
/// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally
/// and uses a self-signed certificate for SSL/TLS encryption.</remarks>
public class DebugWebsocketSink : ILogEventSink, IKeyed
{ {
/// <summary>
/// Represents a DebugWebsocketSink
/// </summary>
public class DebugWebsocketSink : ILogEventSink
{
private HttpServer _httpsServer; private HttpServer _httpsServer;
private string _path = "/debug/join/"; private readonly string _path = "/debug/join/";
private const string _certificateName = "selfCres"; private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345"; private const string _certificatePassword = "cres12345";
private static string CertPath =>
$"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
/// <summary>
/// Gets the port number on which the HTTPS server is currently running.
/// </summary>
public int Port public int Port
{ get {
get
{ {
if(_httpsServer == null) return 0; if (_httpsServer == null) return 0;
return _httpsServer.Port; return _httpsServer.Port;
} }
} }
/// <summary>
/// Gets the WebSocket URL for the current server instance.
/// </summary>
/// <remarks>The URL is dynamically constructed based on the server's current IP address, port,
/// and WebSocket path.</remarks>
public string Url public string Url
{ {
get get
{ {
if (_httpsServer == null) return ""; if (_httpsServer == null || !_httpsServer.IsListening) return "";
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}"; var service = _httpsServer.WebSocketServices[_path];
if (service == null) return "";
// Use CSLAN IP if available, otherwise fallback to primary IP. This ensures we provide a reachable URL in dual-stack environments.
if (!string.IsNullOrEmpty(CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1)))
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1)}:{_httpsServer.Port}{service.Path}";
else
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}";
} }
} }
/// <summary> /// <summary>
/// Gets or sets the IsRunning /// Gets a value indicating whether the HTTPS server is currently listening for incoming connections.
/// </summary> /// </summary>
public bool IsRunning { get => _httpsServer?.IsListening ?? false; } public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
/// <summary>
/// Gets a value indicating whether there are active WebSocket connections.
/// </summary>
public bool HasActiveConnections
{
get
{
if (_httpsServer == null || !_httpsServer.IsListening) return false;
var service = _httpsServer.WebSocketServices[_path];
if (service == null) return false;
return service.Sessions.Count > 0;
}
}
/// <inheritdoc/>
public string Key => "DebugWebsocketSink";
private readonly ITextFormatter _textFormatter; private readonly ITextFormatter _textFormatter;
/// <summary>
/// Initializes a new instance of the <see cref="DebugWebsocketSink"/> class with the specified text formatter.
/// </summary>
/// <remarks>This constructor initializes the WebSocket sink and ensures that a certificate is
/// available for secure communication. If the required certificate does not exist, it will be created
/// automatically. Additionally, the sink is configured to stop the server when the program is
/// stopping.</remarks>
/// <param name="formatProvider">The text formatter used to format log messages. If null, a default JSON formatter is used.</param>
public DebugWebsocketSink(ITextFormatter formatProvider) public DebugWebsocketSink(ITextFormatter formatProvider)
{ {
_textFormatter = formatProvider ?? new JsonFormatter(); _textFormatter = formatProvider ?? new JsonFormatter();
if (!File.Exists($"\\user\\{_certificateName}.pfx")) if (!File.Exists(CertPath))
CreateCert(null); CreateCert();
try
{
CrestronEnvironment.ProgramStatusEventHandler += type => CrestronEnvironment.ProgramStatusEventHandler += type =>
{ {
if (type == eProgramStatusEventType.Stopping) if (type == eProgramStatusEventType.Stopping)
{
StopServer(); StopServer();
}
}; };
} }
catch
private void CreateCert(string[] args)
{ {
// CrestronEnvironment is not available in test / dev environments — safe to skip.
}
}
private static void CreateCert()
{
// NOTE: This method is called from the constructor, which is itself called during Debug's static
// constructor before _logger is assigned. Do NOT call any Debug.Log* methods here — use
// CrestronConsole.PrintLine only, to avoid a NullReferenceException that would poison the Debug type.
try try
{ {
//Debug.Console(0,"CreateCert Creating Utility");
CrestronConsole.PrintLine("CreateCert Creating Utility");
//var utility = new CertificateUtility();
var utility = new BouncyCertificate();
//Debug.Console(0, "CreateCert Calling CreateCert");
CrestronConsole.PrintLine("CreateCert Calling CreateCert");
//utility.CreateCert();
var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0); var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0); var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0); var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
//Debug.Console(0, "DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress); CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), new[] { string.Format("{0}.{1}", hostName, domainName), ipAddress }, new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth }); var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
//Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested var fqdn = string.Format("{0}.{1}", hostName, domainName);
//Debug.Print($"CreateCert Storing Certificate To My.LocalMachine");
//utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine); using var rsa = RSA.Create(2048);
//Debug.Console(0, "CreateCert Saving Cert to \\user\\");
CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\"); var request = new CertificateRequest(
utility.CertificatePassword = _certificatePassword; subjectName,
utility.WriteCertificate(certificate, @"\user\", _certificateName); rsa,
//Debug.Console(0, "CreateCert Ending CreateCert"); HashAlgorithmName.SHA256,
CrestronConsole.PrintLine("CreateCert Ending CreateCert"); RSASignaturePadding.Pkcs1);
// Subject Key Identifier
request.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
// Extended Key Usage: server + client auth
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth
},
false));
// Subject Alternative Names: DNS + IP
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddDnsName(fqdn);
if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
sanBuilder.AddIpAddress(ip);
request.CertificateExtensions.Add(sanBuilder.Build());
var notBefore = DateTimeOffset.UtcNow;
var notAfter = notBefore.AddYears(2);
using var cert = request.CreateSelfSigned(notBefore, notAfter);
var separator = Path.DirectorySeparatorChar;
var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword);
File.WriteAllBytes(outputPath, pfxBytes);
CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
} }
catch (Exception ex) catch (Exception ex)
{ {
//Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace); CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace));
CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace));
} }
} }
/// <summary> /// <summary>
/// Emit method /// Sends a log event to all connected WebSocket clients.
/// </summary> /// </summary>
/// <remarks>The log event is formatted using the configured text formatter and then broadcasted
/// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not
/// listening, the method exits without performing any action.</remarks>
/// <param name="logEvent">The log event to be formatted and broadcasted. Cannot be null.</param>
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
if (_httpsServer == null || !_httpsServer.IsListening) return; if (_httpsServer == null || !_httpsServer.IsListening) return;
@ -121,19 +202,41 @@ namespace PepperDash.Core
var sw = new StringWriter(); var sw = new StringWriter();
_textFormatter.Format(logEvent, sw); _textFormatter.Format(logEvent, sw);
_httpsServer.WebSocketServices.Broadcast(sw.ToString()); _httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
} }
/// <summary> /// <summary>
/// StartServerAndSetPort method /// Starts the WebSocket server on the specified port and configures it with the appropriate certificate.
/// </summary> /// </summary>
/// <remarks>This method initializes the WebSocket server and binds it to the specified port. It
/// also applies the server's certificate for secure communication. Ensure that the port is not already in use
/// and that the certificate file is accessible.</remarks>
/// <param name="port">The port number on which the WebSocket server will listen. Must be a valid, non-negative port number.</param>
public void StartServerAndSetPort(int port) public void StartServerAndSetPort(int port)
{ {
Debug.Console(0, "Starting Websocket Server on port: {0}", port); Debug.LogInformation("Starting Websocket Server on port: {0}", port);
Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword); Start(port, CertPath, _certificatePassword);
}
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
{
try
{
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
// key-container persistence failures, and avoids the private key export restriction.
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
}
catch (Exception ex)
{
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
// Delete it, regenerate with the BCL path, and retry once.
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
try { File.Delete(certPath); } catch { }
CreateCert();
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
}
} }
private void Start(int port, string certPath = "", string certPassword = "") private void Start(int port, string certPath = "", string certPassword = "")
@ -142,101 +245,161 @@ namespace PepperDash.Core
{ {
_httpsServer = new HttpServer(port, true); _httpsServer = new HttpServer(port, true);
if (!string.IsNullOrWhiteSpace(certPath)) if (!string.IsNullOrWhiteSpace(certPath))
{ {
Debug.Console(0, "Assigning SSL Configuration"); Debug.LogInformation("Assigning SSL Configuration");
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
{ _httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(certPath, certPassword);
ClientCertificateRequired = false, _httpsServer.SslConfiguration.ClientCertificateRequired = false;
CheckCertificateRevocation = false, _httpsServer.SslConfiguration.CheckCertificateRevocation = false;
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, _httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
//this is just to test, you might want to actually validate //this is just to test, you might want to actually validate
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => _httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{ {
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered"); Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered");
return true; return true;
}
}; };
} }
Debug.Console(0, "Adding Debug Client Service"); Debug.LogInformation("Adding Debug Client Service");
_httpsServer.AddWebSocketService<DebugClient>(_path); _httpsServer.AddWebSocketService<DebugClient>(_path);
Debug.Console(0, "Assigning Log Info"); _httpsServer.OnGet += HandleHttpGet;
Debug.LogInformation("Assigning Log Info");
_httpsServer.Log.Level = LogLevel.Trace; _httpsServer.Log.Level = LogLevel.Trace;
_httpsServer.Log.Output = (d, s) => _httpsServer.Log.Output = WriteWebSocketInternalLog;
{ Debug.LogInformation("Starting");
uint level;
switch(d.Level)
{
case WebSocketSharp.LogLevel.Fatal:
level = 3;
break;
case WebSocketSharp.LogLevel.Error:
level = 2;
break;
case WebSocketSharp.LogLevel.Warn:
level = 1;
break;
case WebSocketSharp.LogLevel.Info:
level = 0;
break;
case WebSocketSharp.LogLevel.Debug:
level = 4;
break;
case WebSocketSharp.LogLevel.Trace:
level = 5;
break;
default:
level = 4;
break;
}
Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s);
};
Debug.Console(0, "Starting");
_httpsServer.Start(); _httpsServer.Start();
Debug.Console(0, "Ready"); Debug.LogInformation("Ready");
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(0, "WebSocket Failed to start {0}", ex.Message); Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
} Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
} // Null out the server so callers can detect failure via IsRunning / Url null guards.
/// <summary>
/// StopServer method
/// </summary>
public void StopServer()
{
Debug.Console(0, "Stopping Websocket Server");
_httpsServer?.Stop();
_httpsServer = null; _httpsServer = null;
} }
} }
public static class DebugWebsocketSinkExtensions private void HandleHttpGet(object sender, HttpRequestEventArgs e)
{ {
var res = e.Response;
var body = System.Text.Encoding.UTF8.GetBytes(
"<html><body><h2>Certificate accepted.</h2><p>You can close this tab and return to the application.</p></body></html>");
res.ContentType = "text/html";
res.ContentLength64 = body.Length;
res.Close(body, true);
}
/// <summary> /// <summary>
/// DebugWebsocketSink method /// Stops the WebSocket server if it is currently running.
/// </summary> /// </summary>
/// <remarks>This method halts the WebSocket server and releases any associated resources. After
/// calling this method, the server will no longer accept or process incoming connections.</remarks>
public void StopServer()
{
Debug.LogInformation("Stopping Websocket Server");
try
{
if (_httpsServer == null || !_httpsServer.IsListening)
{
return;
}
// Prevent close-sequence internal websocket logs from re-entering the logging pipeline.
_httpsServer.Log.Output = (d, s) => { };
var serviceHost = _httpsServer.WebSocketServices[_path];
if (serviceHost == null)
{
_httpsServer.Stop();
_httpsServer = null;
return;
}
serviceHost.Sessions.Broadcast("Server is stopping");
foreach (var session in serviceHost.Sessions.Sessions)
{
if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive)
{
session.Context.WebSocket.Close(1001, "Server is stopping");
}
}
_httpsServer.Stop();
_httpsServer = null;
}
catch (Exception ex)
{
Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message);
Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace);
}
}
private static void WriteWebSocketInternalLog(LogData data, string supplemental)
{
try
{
if (data == null)
{
return;
}
var message = string.IsNullOrWhiteSpace(data.Message) ? "<none>" : data.Message;
var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental);
// Use direct console output to avoid recursive log sink calls.
CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details));
}
catch
{
// Never throw from websocket log callback.
}
}
}
/// <summary>
/// Configures the logger to write log events to a debug WebSocket sink.
/// </summary>
/// <remarks>This extension method allows you to direct log events to a WebSocket sink for debugging
/// purposes.</remarks>
public static class DebugWebsocketSinkExtensions
{
/// <summary>
/// Configures a logger to write log events to a debug WebSocket sink.
/// </summary>
/// <remarks>This method adds a sink that writes log events to a WebSocket for debugging purposes.
/// It is typically used during development to stream log events in real-time.</remarks>
/// <param name="loggerConfiguration">The logger sink configuration to apply the WebSocket sink to.</param>
/// <param name="formatProvider">An optional text formatter to format the log events. If not provided, a default formatter will be used.</param>
/// <returns>A <see cref="LoggerConfiguration"/> object that can be used to further configure the logger.</returns>
public static LoggerConfiguration DebugWebsocketSink( public static LoggerConfiguration DebugWebsocketSink(
this LoggerSinkConfiguration loggerConfiguration, this LoggerSinkConfiguration loggerConfiguration,
ITextFormatter formatProvider = null) ITextFormatter formatProvider = null)
{ {
return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider)); return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider));
} }
} }
/// <summary> /// <summary>
/// Represents a DebugClient /// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message
/// </summary> /// handling functionality.
public class DebugClient : WebSocketBehavior /// </summary>
{ /// <remarks>The <see cref="DebugClient"/> class extends <see cref="WebSocketBehavior"/> to handle
/// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the
/// duration of the connection and logs relevant events for debugging.</remarks>
public class DebugClient : WebSocketBehavior
{
private DateTime _connectionTime; private DateTime _connectionTime;
/// <summary>
/// Gets the duration of time the WebSocket connection has been active.
/// </summary>
public TimeSpan ConnectedDuration public TimeSpan ConnectedDuration
{ {
get get
@ -252,41 +415,47 @@ namespace PepperDash.Core
} }
} }
/// <summary>
/// Initializes a new instance of the <see cref="DebugClient"/> class.
/// </summary>
public DebugClient() public DebugClient()
{ {
Debug.Console(0, "DebugClient Created"); Debug.LogInformation("DebugClient Created");
} }
/// <inheritdoc/>
protected override void OnOpen() protected override void OnOpen()
{ {
base.OnOpen(); base.OnOpen();
var url = Context.WebSocket.Url; var url = Context.WebSocket.Url;
Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); Debug.LogInformation("New WebSocket Connection from: {0}", url);
_connectionTime = DateTime.Now; _connectionTime = DateTime.Now;
} }
/// <inheritdoc/>
protected override void OnMessage(MessageEventArgs e) protected override void OnMessage(MessageEventArgs e)
{ {
base.OnMessage(e); base.OnMessage(e);
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data); Debug.LogVerbose("WebSocket UiClient Message: {0}", e.Data);
} }
/// <inheritdoc/>
protected override void OnClose(CloseEventArgs e) protected override void OnClose(CloseEventArgs e)
{ {
base.OnClose(e); base.OnClose(e);
Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); Debug.LogDebug("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
} }
/// <inheritdoc/>
protected override void OnError(WebSocketSharp.ErrorEventArgs e) protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{ {
base.OnError(e); base.OnError(e);
Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); Debug.LogError(e.Exception, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
} Debug.LogVerbose("Stack Trace:\r{0}", e.Exception.StackTrace);
} }
} }

View file

@ -4,11 +4,11 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core namespace PepperDash.Core;
{
/// <summary> /// <summary>
/// Not in use /// Not in use
/// </summary> /// </summary>
public static class NetworkComm public static class NetworkComm
{ {
/// <summary> /// <summary>
@ -18,5 +18,3 @@ namespace PepperDash.Core
{ {
} }
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.PasswordManagement namespace PepperDash.Core.PasswordManagement;
{
/// <summary> /// <summary>
/// JSON password configuration /// JSON password configuration
/// </summary> /// </summary>
@ -23,4 +23,3 @@ namespace PepperDash.Core.PasswordManagement
} }
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.PasswordManagement namespace PepperDash.Core.PasswordManagement;
{
/// <summary> /// <summary>
/// Constants /// Constants
/// </summary> /// </summary>
@ -54,4 +54,3 @@ namespace PepperDash.Core.PasswordManagement
/// </summary> /// </summary>
public const ushort StringValueChange = 201; public const ushort StringValueChange = 201;
} }
}

View file

@ -1,10 +1,10 @@
using System; using System;
namespace PepperDash.Core.PasswordManagement namespace PepperDash.Core.PasswordManagement;
{
/// <summary> /// <summary>
/// Represents a PasswordClient /// A class to allow user interaction with the PasswordManager
/// </summary> /// </summary>
public class PasswordClient public class PasswordClient
{ {
/// <summary> /// <summary>
@ -193,4 +193,3 @@ namespace PepperDash.Core.PasswordManagement
} }
} }
} }
}

View file

@ -1,14 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Crestron.SimplSharp; using System.Timers;
namespace PepperDash.Core.PasswordManagement namespace PepperDash.Core.PasswordManagement;
/// <summary>
/// Allows passwords to be stored and managed
/// </summary>
public class PasswordManager
{ {
/// <summary>
/// Represents a PasswordManager
/// </summary>
public class PasswordManager
{
/// <summary> /// <summary>
/// Public dictionary of known passwords /// Public dictionary of known passwords
/// </summary> /// </summary>
@ -21,7 +21,7 @@ namespace PepperDash.Core.PasswordManagement
/// <summary> /// <summary>
/// Timer used to wait until password changes have stopped before updating the dictionary /// Timer used to wait until password changes have stopped before updating the dictionary
/// </summary> /// </summary>
CTimer PasswordTimer; Timer PasswordTimer;
/// <summary> /// <summary>
/// Timer length /// Timer length
/// </summary> /// </summary>
@ -79,60 +79,65 @@ namespace PepperDash.Core.PasswordManagement
// validate the parameters // validate the parameters
if (key > 0 && string.IsNullOrEmpty(password)) if (key > 0 && string.IsNullOrEmpty(password))
{ {
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password)); Debug.LogDebug("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password);
return; return;
} }
try try
{ {
// if key exists, update the value // if key exists, update the value
if(_passwords.ContainsKey(key)) if (_passwords.ContainsKey(key))
_passwords[key] = password; _passwords[key] = password;
// else add the key & value // else add the key & value
else else
_passwords.Add(key, password); _passwords.Add(key, password);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key])); Debug.LogDebug("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]);
if (PasswordTimer == null) if (PasswordTimer == null)
{ {
PasswordTimer = new CTimer((o) => PasswordTimerElapsed(), PasswordTimerElapsedMs); PasswordTimer = new Timer(PasswordTimerElapsedMs) { AutoReset = false };
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started")); PasswordTimer.Elapsed += (s, e) => PasswordTimerElapsed(s, e);
PasswordTimer.Start();
Debug.LogDebug("PasswordManager.UpdatePassword: Timer Started");
OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange); OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
} }
else else
{ {
PasswordTimer.Reset(PasswordTimerElapsedMs); PasswordTimer.Stop();
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset")); PasswordTimer.Interval = PasswordTimerElapsedMs;
PasswordTimer.Start();
Debug.LogDebug("PasswordManager.UpdatePassword: Timer Reset");
} }
} }
catch (Exception e) catch (Exception e)
{ {
var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e); var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e);
Debug.Console(1, msg); Debug.LogError(e, msg);
Debug.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
} }
} }
/// <summary> /// <summary>
/// CTimer callback function /// Timer callback function
/// </summary> /// </summary>
private void PasswordTimerElapsed() private void PasswordTimerElapsed(object sender, ElapsedEventArgs e)
{ {
try try
{ {
PasswordTimer.Stop(); PasswordTimer.Stop();
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: CTimer Stopped")); Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Timer Stopped");
OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange); OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
foreach (var pw in _passwords) foreach (var pw in _passwords)
{ {
// if key exists, continue // if key exists, continue
if (Passwords.ContainsKey(pw.Key)) if (Passwords.ContainsKey(pw.Key))
{ {
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value)); Debug.LogDebug("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value);
if (Passwords[pw.Key] != _passwords[pw.Key]) if (Passwords[pw.Key] != _passwords[pw.Key])
{ {
Passwords[pw.Key] = _passwords[pw.Key]; Passwords[pw.Key] = _passwords[pw.Key];
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key])); Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]);
OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange); OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange);
} }
} }
@ -144,10 +149,11 @@ namespace PepperDash.Core.PasswordManagement
} }
OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange); OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
} }
catch (Exception e) catch (Exception ex)
{ {
var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", e); var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", ex.Message);
Debug.Console(1, msg); Debug.LogError(ex, msg);
Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
} }
} }
@ -168,12 +174,12 @@ namespace PepperDash.Core.PasswordManagement
/// </summary> /// </summary>
public void ListPasswords() public void ListPasswords()
{ {
Debug.Console(0, "PasswordManager.ListPasswords:\r"); Debug.LogInformation("PasswordManager.ListPasswords:\r");
foreach (var pw in Passwords) foreach (var pw in Passwords)
Debug.Console(0, "Passwords[{0}]: {1}\r", pw.Key, pw.Value); Debug.LogInformation("Passwords[{0}]: {1}\r", pw.Key, pw.Value);
Debug.Console(0, "\n"); Debug.LogInformation("\n");
foreach (var pw in _passwords) foreach (var pw in _passwords)
Debug.Console(0, "_passwords[{0}]: {1}\r", pw.Key, pw.Value); Debug.LogInformation("_passwords[{0}]: {1}\r", pw.Key, pw.Value);
} }
/// <summary> /// <summary>
@ -243,5 +249,4 @@ namespace PepperDash.Core.PasswordManagement
PasswordChange(this, args); PasswordChange(this, args);
} }
} }
}
} }

View file

@ -5,7 +5,7 @@
<PropertyGroup> <PropertyGroup>
<RootNamespace>PepperDash.Core</RootNamespace> <RootNamespace>PepperDash.Core</RootNamespace>
<AssemblyName>PepperDashCore</AssemblyName> <AssemblyName>PepperDashCore</AssemblyName>
<TargetFramework>net472</TargetFramework> <TargetFramework>net8</TargetFramework>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
<NeutralLanguage>en</NeutralLanguage> <NeutralLanguage>en</NeutralLanguage>
<OutputPath>bin\$(Configuration)\</OutputPath> <OutputPath>bin\$(Configuration)\</OutputPath>
@ -37,23 +37,21 @@
<None Remove="Properties\**" /> <None Remove="Properties\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Data.DataSetExtensions" /> <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" />
<Reference Include="Microsoft.CSharp" /> <PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.128" />
<Reference Include="System.Net.Http" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4">
<Aliases>global,NewtonsoftJson</Aliases>
</PackageReference>
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SSH.NET" Version="2025.0.0" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" /> <ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6'">
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="Comm\._GenericSshClient.cs" /> <Compile Remove="Comm\._GenericSshClient.cs" />

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.SystemInfo namespace PepperDash.Core.SystemInfo;
{
/// <summary> /// <summary>
/// Constants /// Constants
/// </summary> /// </summary>
@ -261,4 +261,3 @@ namespace PepperDash.Core.SystemInfo
Index = index; Index = index;
} }
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.SystemInfo namespace PepperDash.Core.SystemInfo;
{
/// <summary> /// <summary>
/// Processor info class /// Processor info class
/// </summary> /// </summary>
@ -201,4 +201,3 @@ namespace PepperDash.Core.SystemInfo
} }
} }
}

View file

@ -4,8 +4,8 @@ using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
namespace PepperDash.Core.SystemInfo namespace PepperDash.Core.SystemInfo;
{
/// <summary> /// <summary>
/// System Info class /// System Info class
/// </summary> /// </summary>
@ -468,4 +468,3 @@ namespace PepperDash.Core.SystemInfo
} }
} }
} }
}

View file

@ -20,13 +20,13 @@ using Org.BouncyCastle.Crypto.Operators;
using BigInteger = Org.BouncyCastle.Math.BigInteger; using BigInteger = Org.BouncyCastle.Math.BigInteger;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate; using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace PepperDash.Core namespace PepperDash.Core;
/// <summary>
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
/// </summary>
internal class BouncyCertificate
{ {
/// <summary>
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
/// </summary>
internal class BouncyCertificate
{
public string CertificatePassword { get; set; } = "password"; public string CertificatePassword { get; set; } = "password";
public X509Certificate2 LoadCertificate(string issuerFileName, string password) public X509Certificate2 LoadCertificate(string issuerFileName, string password)
{ {
@ -35,9 +35,6 @@ namespace PepperDash.Core
return issuerCertificate; return issuerCertificate;
} }
/// <summary>
/// IssueCertificate method
/// </summary>
public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages) public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages)
{ {
// It's self-signed, so these are the same. // It's self-signed, so these are the same.
@ -59,9 +56,6 @@ namespace PepperDash.Core
return ConvertCertificate(certificate, subjectKeyPair, random); return ConvertCertificate(certificate, subjectKeyPair, random);
} }
/// <summary>
/// CreateCertificateAuthorityCertificate method
/// </summary>
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
{ {
// It's self-signed, so these are the same. // It's self-signed, so these are the same.
@ -84,9 +78,6 @@ namespace PepperDash.Core
return ConvertCertificate(certificate, subjectKeyPair, random); return ConvertCertificate(certificate, subjectKeyPair, random);
} }
/// <summary>
/// CreateSelfSignedCertificate method
/// </summary>
public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages) public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
{ {
// It's self-signed, so these are the same. // It's self-signed, so these are the same.
@ -314,9 +305,6 @@ namespace PepperDash.Core
return convertedCertificate; return convertedCertificate;
} }
/// <summary>
/// WriteCertificate method
/// </summary>
public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName) public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName)
{ {
// This password is the one attached to the PFX file. Use 'null' for no password. // This password is the one attached to the PFX file. Use 'null' for no password.
@ -344,9 +332,6 @@ namespace PepperDash.Core
} }
} }
} }
/// <summary>
/// AddCertToStore method
/// </summary>
public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl) public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl)
{ {
bool bRet = false; bool bRet = false;
@ -367,5 +352,4 @@ namespace PepperDash.Core
return bRet; return bRet;
} }
}
} }

View file

@ -1,9 +1,9 @@
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
namespace PepperDash.Core.Web.RequestHandlers namespace PepperDash.Core.Web.RequestHandlers;
{
/// <summary> /// <summary>
/// Represents a DefaultRequestHandler /// Web API default request handler
/// </summary> /// </summary>
public class DefaultRequestHandler : WebApiBaseRequestHandler public class DefaultRequestHandler : WebApiBaseRequestHandler
{ {
@ -14,4 +14,3 @@ namespace PepperDash.Core.Web.RequestHandlers
: base(true) : base(true)
{ } { }
} }
}

View file

@ -3,10 +3,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace PepperDash.Core.Web.RequestHandlers namespace PepperDash.Core.Web.RequestHandlers;
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
{ {
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
{
private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers; private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers;
protected readonly bool EnableCors; protected readonly bool EnableCors;
@ -142,9 +142,6 @@ namespace PepperDash.Core.Web.RequestHandlers
/// Process request /// Process request
/// </summary> /// </summary>
/// <param name="context"></param> /// <param name="context"></param>
/// <summary>
/// ProcessRequest method
/// </summary>
public void ProcessRequest(HttpCwsContext context) public void ProcessRequest(HttpCwsContext context)
{ {
if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func<HttpCwsContext, Task> handler)) if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func<HttpCwsContext, Task> handler))
@ -162,5 +159,4 @@ namespace PepperDash.Core.Web.RequestHandlers
handlerTask.GetAwaiter().GetResult(); handlerTask.GetAwaiter().GetResult();
} }
}
} }

View file

@ -2,8 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
namespace PepperDash.Core.Web.RequestHandlers namespace PepperDash.Core.Web.RequestHandlers;
{
/// <summary> /// <summary>
/// CWS Base Handler, implements IHttpCwsHandler /// CWS Base Handler, implements IHttpCwsHandler
/// </summary> /// </summary>
@ -165,4 +165,3 @@ namespace PepperDash.Core.Web.RequestHandlers
handler(context); handler(context);
} }
} }
}

View file

@ -1,28 +1,24 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core.Web.RequestHandlers; using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Core.Logging;
namespace PepperDash.Core.Web namespace PepperDash.Core.Web;
/// <summary>
/// Web API server
/// </summary>
public class WebApiServer : IKeyName
{ {
/// <summary>
/// Web API server
/// </summary>
public class WebApiServer : IKeyName
{
private const string SplusKey = "Uninitialized Web API Server"; private const string SplusKey = "Uninitialized Web API Server";
private const string DefaultName = "Web API Server"; private const string DefaultName = "Web API Server";
private const string DefaultBasePath = "/api"; private const string DefaultBasePath = "/api";
private const uint DebugTrace = 0; private readonly object _serverLock = new();
private const uint DebugInfo = 1;
private const uint DebugVerbose = 2;
private readonly CCriticalSection _serverLock = new CCriticalSection();
private HttpCwsServer _server; private HttpCwsServer _server;
/// <summary> /// <summary>
@ -45,28 +41,6 @@ namespace PepperDash.Core.Web
/// </summary> /// </summary>
public bool IsRegistered { get; private set; } public bool IsRegistered { get; private set; }
/// <summary>
/// Http request handler
/// </summary>
//public IHttpCwsHandler HttpRequestHandler
//{
// get { return _server.HttpRequestHandler; }
// set
// {
// if (_server == null) return;
// _server.HttpRequestHandler = value;
// }
//}
/// <summary>
/// Received request event handler
/// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
//}
/// <summary> /// <summary>
/// Constructor for S+. Make sure to set necessary properties using init method /// Constructor for S+. Make sure to set necessary properties using init method
/// </summary> /// </summary>
@ -97,10 +71,15 @@ namespace PepperDash.Core.Web
Name = string.IsNullOrEmpty(name) ? DefaultName : name; Name = string.IsNullOrEmpty(name) ? DefaultName : name;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
this.LogInformation("Creating Web API Server with Key: {Key}, Name: {Name}, BasePath: {BasePath}", Key, Name, BasePath);
if (_server == null) _server = new HttpCwsServer(BasePath); if (_server == null) _server = new HttpCwsServer(BasePath);
_server.AuthenticateAllRoutes = false;
_server.setProcessName(Key); _server.setProcessName(Key);
_server.HttpRequestHandler = new DefaultRequestHandler(); _server.HttpRequestHandler = new DefaultRequestHandler();
_server.ReceivedRequestEvent += ReceivedRequestEventHandler;
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
@ -114,7 +93,7 @@ namespace PepperDash.Core.Web
{ {
if (programEventType != eProgramStatusEventType.Stopping) return; if (programEventType != eProgramStatusEventType.Stopping) return;
Debug.Console(DebugInfo, this, "Program stopping. stopping server"); this.LogInformation("Program stopping. stopping server");
Stop(); Stop();
} }
@ -125,26 +104,30 @@ namespace PepperDash.Core.Web
/// <param name="ethernetEventArgs"></param> /// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) 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)
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
{ {
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
return; return;
} }
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server"); if (IsRegistered)
{
this.LogInformation("Ethernet link up. Server is already registered.");
return;
}
this.LogInformation("Ethernet link up. Starting server");
Start(); Start();
} }
/// <summary> // /// <summary>
/// Initialize method // /// Initialize method
/// </summary> // /// </summary>
public void Initialize(string key, string basePath) // public void Initialize(string key, string basePath)
{ // {
Key = key; // Key = key;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath; // BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
} // }
/// <summary> /// <summary>
/// Adds a route to CWS /// Adds a route to CWS
@ -153,7 +136,7 @@ namespace PepperDash.Core.Web
{ {
if (route == null) if (route == null)
{ {
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null"); this.LogWarning("Failed to add route, route parameter is null");
return; return;
} }
@ -172,13 +155,29 @@ namespace PepperDash.Core.Web
{ {
if (route == null) if (route == null)
{ {
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null"); this.LogWarning("Failed to remove route, route parameter is null");
return; return;
} }
_server.Routes.Remove(route); _server.Routes.Remove(route);
} }
/// <summary>
/// Sets the fallback request handler that is invoked when no registered route
/// matches an incoming request. Must be called before <see cref="Start"/>.
/// </summary>
/// <param name="handler">The handler to use as the server-level fallback.</param>
public void SetFallbackHandler(IHttpCwsHandler handler)
{
if (handler == null)
{
this.LogWarning("SetFallbackHandler: handler parameter is null, ignoring");
return;
}
_server.HttpRequestHandler = handler;
}
/// <summary> /// <summary>
/// GetRouteCollection method /// GetRouteCollection method
/// </summary> /// </summary>
@ -191,73 +190,60 @@ namespace PepperDash.Core.Web
/// Starts CWS instance /// Starts CWS instance
/// </summary> /// </summary>
public void Start() public void Start()
{
lock (_serverLock)
{ {
try try
{ {
_serverLock.Enter();
if (_server == null) if (_server == null)
{ {
Debug.Console(DebugInfo, this, "Server is null, unable to start"); this.LogDebug("Server is null, unable to start");
return; return;
} }
if (IsRegistered) if (IsRegistered)
{ {
Debug.Console(DebugInfo, this, "Server has already been started"); this.LogDebug("Server has already been started");
return; return;
} }
IsRegistered = _server.Register(); IsRegistered = _server.Register();
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed"); this.LogDebug("Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message); this.LogException(ex, "Start Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace); this.LogVerbose("Start Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
}
finally
{
_serverLock.Leave();
} }
} // end lock
} }
/// <summary> /// <summary>
/// Stop method /// Stop method
/// </summary> /// </summary>
public void Stop() public void Stop()
{
lock (_serverLock)
{ {
try try
{ {
_serverLock.Enter();
if (_server == null) if (_server == null)
{ {
Debug.Console(DebugInfo, this, "Server is null or has already been stopped"); this.LogDebug("Server is null or has already been stopped");
return; return;
} }
IsRegistered = _server.Unregister() == false; var unregistered = _server.Unregister();
IsRegistered = !unregistered;
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful"); this.LogDebug("Stopping server, unregistration {0}", unregistered ? "was successful" : "failed");
_server.Dispose();
_server = null;
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message); this.LogException(ex, "Server Stop Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException);
}
finally
{
_serverLock.Leave();
} }
} // end lock
} }
/// <summary> /// <summary>
@ -272,16 +258,12 @@ namespace PepperDash.Core.Web
{ {
try try
{ {
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented); var req = args.Context?.Request;
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j); this.LogVerbose("ReceivedRequestEventHandler: {Method} {Path}", req?.HttpMethod, req?.Path);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message); this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
}
} }
} }
} }

View file

@ -1,87 +0,0 @@
using System;
namespace PepperDash.Core.WebApi.Presets
{
/// <summary>
/// Represents a Preset
/// </summary>
public class Preset
{
/// <summary>
/// ID of preset
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the UserId
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Gets or sets the RoomTypeId
/// </summary>
public int RoomTypeId { get; set; }
/// <summary>
/// Gets or sets the PresetName
/// </summary>
public string PresetName { get; set; }
/// <summary>
/// Gets or sets the PresetNumber
/// </summary>
public int PresetNumber { get; set; }
/// <summary>
/// Gets or sets the Data
/// </summary>
public string Data { get; set; }
/// <summary>
/// Constructor
/// </summary>
public Preset()
{
PresetName = "";
PresetNumber = 1;
Data = "{}";
}
}
/// <summary>
/// Represents a PresetReceivedEventArgs
/// </summary>
public class PresetReceivedEventArgs : EventArgs
{
/// <summary>
/// True when the preset is found
/// </summary>
public bool LookupSuccess { get; private set; }
/// <summary>
/// Gets or sets the ULookupSuccess
/// </summary>
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
/// <summary>
/// Gets or sets the Preset
/// </summary>
public Preset Preset { get; private set; }
/// <summary>
/// For Simpl+
/// </summary>
public PresetReceivedEventArgs() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="preset"></param>
/// <param name="success"></param>
public PresetReceivedEventArgs(Preset preset, bool success)
{
LookupSuccess = success;
Preset = preset;
}
}
}

View file

@ -1,93 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.WebApi.Presets
{
/// <summary>
///
/// </summary>
public class User
{
/// <summary>
///
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the ExternalId
/// </summary>
public string ExternalId { get; set; }
/// <summary>
/// Gets or sets the FirstName
/// </summary>
public string FirstName { get; set; }
/// <summary>
/// Gets or sets the LastName
/// </summary>
public string LastName { get; set; }
}
/// <summary>
///
/// </summary>
public class UserReceivedEventArgs : EventArgs
{
/// <summary>
/// True when user is found
/// </summary>
public bool LookupSuccess { get; private set; }
/// <summary>
/// Gets or sets the ULookupSuccess
/// </summary>
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
/// <summary>
/// Gets or sets the User
/// </summary>
public User User { get; private set; }
/// <summary>
/// For Simpl+
/// </summary>
public UserReceivedEventArgs() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="user"></param>
/// <param name="success"></param>
public UserReceivedEventArgs(User user, bool success)
{
LookupSuccess = success;
User = user;
}
}
/// <summary>
/// Represents a UserAndRoomMessage
/// </summary>
public class UserAndRoomMessage
{
/// <summary>
///
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Gets or sets the RoomTypeId
/// </summary>
public int RoomTypeId { get; set; }
/// <summary>
/// Gets or sets the PresetNumber
/// </summary>
public int PresetNumber { get; set; }
}
}

View file

@ -1,282 +0,0 @@
using System;
using Crestron.SimplSharp; // For Basic SIMPL# Classes
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.Net.Https;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core.JsonToSimpl;
namespace PepperDash.Core.WebApi.Presets
{
/// <summary>
/// Passcode client for the WebApi
/// </summary>
public class WebApiPasscodeClient : IKeyed
{
/// <summary>
/// Notifies when user received
/// </summary>
public event EventHandler<UserReceivedEventArgs> UserReceived;
/// <summary>
/// Notifies when Preset received
/// </summary>
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
/// <summary>
/// Gets or sets the Key
/// </summary>
public string Key { get; private set; }
//string JsonMasterKey;
/// <summary>
/// An embedded JsonToSimpl master object.
/// </summary>
JsonToSimplGenericMaster J2SMaster;
string UrlBase;
string DefaultPresetJsonFilePath;
User CurrentUser;
Preset CurrentPreset;
/// <summary>
/// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please
/// use an Initialize method
/// </summary>
public WebApiPasscodeClient()
{
}
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="key"></param>
/// <param name="jsonMasterKey"></param>
/// <param name="urlBase"></param>
/// <param name="defaultPresetJsonFilePath"></param>
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
{
Key = key;
//JsonMasterKey = jsonMasterKey;
UrlBase = urlBase;
DefaultPresetJsonFilePath = defaultPresetJsonFilePath;
J2SMaster = new JsonToSimplGenericMaster();
J2SMaster.SaveCallback = this.SaveCallback;
J2SMaster.Initialize(jsonMasterKey);
}
/// <summary>
/// Gets the user for a passcode
/// </summary>
/// <param name="passcode"></param>
/// <summary>
/// GetUserForPasscode method
/// </summary>
public void GetUserForPasscode(string passcode)
{
// Bullshit duplicate code here... These two cases should be the same
// except for https/http and the certificate ignores
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
var jo = new JObject();
jo.Add("pin", passcode);
req.ContentString = jo.ToString();
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
var resp = client.Dispatch(req);
var handler = UserReceived;
if (resp.Code == 200)
{
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
CurrentUser = user;
if (handler != null)
UserReceived(this, new UserReceivedEventArgs(user, true));
}
else
if (handler != null)
UserReceived(this, new UserReceivedEventArgs(null, false));
}
/// <summary>
///
/// </summary>
/// <param name="roomTypeId"></param>
/// <param name="presetNumber"></param>
/// <summary>
/// GetPresetForThisUser method
/// </summary>
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
{
if (CurrentUser == null)
{
CrestronConsole.PrintLine("GetPresetForThisUser no user loaded");
return;
}
var msg = new UserAndRoomMessage
{
UserId = CurrentUser.Id,
RoomTypeId = roomTypeId,
PresetNumber = presetNumber
};
var handler = PresetReceived;
try
{
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
req.ContentString = JsonConvert.SerializeObject(msg);
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
// ask for the preset
var resp = client.Dispatch(req);
if (resp.Code == 200) // got it
{
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
CurrentPreset = preset;
//if there's no preset data, load the template
if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0)
{
//Debug.Console(1, this, "Loaded preset has no data. Loading default template.");
LoadDefaultPresetData();
return;
}
J2SMaster.LoadWithJson(preset.Data);
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(preset, true));
}
else // no existing preset
{
CurrentPreset = new Preset();
LoadDefaultPresetData();
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(null, false));
}
}
catch (HttpException e)
{
var resp = e.Response;
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
LoadDefaultPresetData();
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(null, false));
}
}
void LoadDefaultPresetData()
{
CurrentPreset = null;
if (!File.Exists(DefaultPresetJsonFilePath))
{
Debug.Console(0, this, "Cannot load default preset file. Saving will not work");
return;
}
using (StreamReader sr = new StreamReader(DefaultPresetJsonFilePath))
{
try
{
var data = sr.ReadToEnd();
J2SMaster.SetJsonWithoutEvaluating(data);
CurrentPreset = new Preset() { Data = data, UserId = CurrentUser.Id };
}
catch (Exception e)
{
Debug.Console(0, this, "Error reading default preset JSON: \r{0}", e);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="roomTypeId"></param>
/// <param name="presetNumber"></param>
/// <summary>
/// SavePresetForThisUser method
/// </summary>
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
{
if (CurrentPreset == null)
LoadDefaultPresetData();
//return;
//// A new preset needs to have its numbers set
//if (CurrentPreset.IsNewPreset)
//{
CurrentPreset.UserId = CurrentUser.Id;
CurrentPreset.RoomTypeId = roomTypeId;
CurrentPreset.PresetNumber = presetNumber;
//}
J2SMaster.Save(); // Will trigger callback when ready
}
/// <summary>
/// After save operation on JSON master happens, send it to server
/// </summary>
/// <param name="json"></param>
void SaveCallback(string json)
{
CurrentPreset.Data = json;
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
try
{
var resp = client.Dispatch(req);
// 201=created
// 204=empty content
if (resp.Code == 201)
CrestronConsole.PrintLine("Preset added");
else if (resp.Code == 204)
CrestronConsole.PrintLine("Preset updated");
else if (resp.Code == 209)
CrestronConsole.PrintLine("Preset already exists. Cannot save as new.");
else
CrestronConsole.PrintLine("Preset save failed: {0}\r", resp.Code, resp.ContentString);
}
catch (HttpException e)
{
CrestronConsole.PrintLine("Preset save exception {0}", e.Response.Code);
}
}
}
}

View file

@ -1,13 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using PepperDash.Core.Intersystem.Tokens; using PepperDash.Core.Intersystem.Tokens;
namespace PepperDash.Core.Intersystem.Serialization namespace PepperDash.Core.Intersystem.Serialization;
/// <summary>
/// Interface to determine XSig serialization for an object.
/// </summary>
public interface IXSigSerialization
{ {
/// <summary>
/// Interface to determine XSig serialization for an object.
/// </summary>
public interface IXSigSerialization
{
/// <summary> /// <summary>
/// Serialize the sig data /// Serialize the sig data
/// </summary> /// </summary>
@ -21,5 +21,4 @@ namespace PepperDash.Core.Intersystem.Serialization
/// <param name="tokens"></param> /// <param name="tokens"></param>
/// <returns></returns> /// <returns></returns>
T Deserialize<T>(IEnumerable<XSigToken> tokens) where T : class, IXSigSerialization; T Deserialize<T>(IEnumerable<XSigToken> tokens) where T : class, IXSigSerialization;
}
} }

View file

@ -1,12 +1,12 @@
using System; using System;
namespace PepperDash.Core.Intersystem.Serialization namespace PepperDash.Core.Intersystem.Serialization;
/// <summary>
/// Class to handle this specific exception type
/// </summary>
public class XSigSerializationException : Exception
{ {
/// <summary>
/// Class to handle this specific exception type
/// </summary>
public class XSigSerializationException : Exception
{
/// <summary> /// <summary>
/// default constructor /// default constructor
/// </summary> /// </summary>
@ -24,5 +24,4 @@ namespace PepperDash.Core.Intersystem.Serialization
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="inner"></param> /// <param name="inner"></param>
public XSigSerializationException(string message, Exception inner) : base(message, inner) { } public XSigSerializationException(string message, Exception inner) : base(message, inner) { }
}
} }

View file

@ -1,12 +1,12 @@
using System; using System;
namespace PepperDash.Core.Intersystem.Tokens namespace PepperDash.Core.Intersystem.Tokens;
/// <summary>
/// Represents an XSigAnalogToken
/// </summary>
public sealed class XSigAnalogToken : XSigToken, IFormattable
{ {
/// <summary>
/// Represents an XSigAnalogToken
/// </summary>
public sealed class XSigAnalogToken : XSigToken, IFormattable
{
private readonly ushort _value; private readonly ushort _value;
/// <summary> /// <summary>
@ -59,9 +59,6 @@ namespace PepperDash.Core.Intersystem.Tokens
/// </summary> /// </summary>
/// <param name="offset"></param> /// <param name="offset"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// GetTokenWithOffset method
/// </summary>
public override XSigToken GetTokenWithOffset(int offset) public override XSigToken GetTokenWithOffset(int offset)
{ {
if (offset == 0) return this; if (offset == 0) return this;
@ -72,10 +69,6 @@ namespace PepperDash.Core.Intersystem.Tokens
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// ToString method
/// </summary>
/// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return Index + " = 0x" + Value.ToString("X4"); return Index + " = 0x" + Value.ToString("X4");
@ -87,12 +80,8 @@ namespace PepperDash.Core.Intersystem.Tokens
/// <param name="format"></param> /// <param name="format"></param>
/// <param name="formatProvider"></param> /// <param name="formatProvider"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// ToString method
/// </summary>
public string ToString(string format, IFormatProvider formatProvider) public string ToString(string format, IFormatProvider formatProvider)
{ {
return Value.ToString(format, formatProvider); return Value.ToString(format, formatProvider);
} }
}
} }

View file

@ -1,12 +1,12 @@
using System; using System;
namespace PepperDash.Core.Intersystem.Tokens namespace PepperDash.Core.Intersystem.Tokens;
/// <summary>
/// Represents an XSigDigitalToken
/// </summary>
public sealed class XSigDigitalToken : XSigToken
{ {
/// <summary>
/// Represents an XSigDigitalToken
/// </summary>
public sealed class XSigDigitalToken : XSigToken
{
private readonly bool _value; private readonly bool _value;
/// <summary> /// <summary>
@ -57,9 +57,6 @@ namespace PepperDash.Core.Intersystem.Tokens
/// </summary> /// </summary>
/// <param name="offset"></param> /// <param name="offset"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// GetTokenWithOffset method
/// </summary>
public override XSigToken GetTokenWithOffset(int offset) public override XSigToken GetTokenWithOffset(int offset)
{ {
if (offset == 0) return this; if (offset == 0) return this;
@ -70,10 +67,6 @@ namespace PepperDash.Core.Intersystem.Tokens
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// ToString method
/// </summary>
/// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return Index + " = " + (Value ? "High" : "Low"); return Index + " = " + (Value ? "High" : "Low");
@ -84,12 +77,8 @@ namespace PepperDash.Core.Intersystem.Tokens
/// </summary> /// </summary>
/// <param name="formatProvider"></param> /// <param name="formatProvider"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// ToString method
/// </summary>
public string ToString(IFormatProvider formatProvider) public string ToString(IFormatProvider formatProvider)
{ {
return Value.ToString(formatProvider); return Value.ToString(formatProvider);
} }
}
} }

View file

@ -1,13 +1,13 @@
using System; using System;
using System.Text; using System.Text;
namespace PepperDash.Core.Intersystem.Tokens namespace PepperDash.Core.Intersystem.Tokens;
/// <summary>
/// Represents an XSigSerialToken
/// </summary>
public sealed class XSigSerialToken : XSigToken
{ {
/// <summary>
/// Represents an XSigSerialToken
/// </summary>
public sealed class XSigSerialToken : XSigToken
{
private readonly string _value; private readonly string _value;
/// <summary> /// <summary>
@ -63,9 +63,6 @@ namespace PepperDash.Core.Intersystem.Tokens
/// </summary> /// </summary>
/// <param name="offset"></param> /// <param name="offset"></param>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// GetTokenWithOffset method
/// </summary>
public override XSigToken GetTokenWithOffset(int offset) public override XSigToken GetTokenWithOffset(int offset)
{ {
if (offset == 0) return this; if (offset == 0) return this;
@ -76,13 +73,8 @@ namespace PepperDash.Core.Intersystem.Tokens
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <summary>
/// ToString method
/// </summary>
/// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return Index + " = \"" + Value + "\""; return Index + " = \"" + Value + "\"";
} }
}
} }

View file

@ -1,10 +1,10 @@
namespace PepperDash.Core.Intersystem.Tokens namespace PepperDash.Core.Intersystem.Tokens;
/// <summary>
/// Represents the base class for all XSig datatypes.
/// </summary>
public abstract class XSigToken
{ {
/// <summary>
/// Represents the base class for all XSig datatypes.
/// </summary>
public abstract class XSigToken
{
private readonly int _index; private readonly int _index;
/// <summary> /// <summary>
@ -41,5 +41,4 @@ namespace PepperDash.Core.Intersystem.Tokens
/// <param name="offset">Offset to adjust the index with.</param> /// <param name="offset">Offset to adjust the index with.</param>
/// <returns>XSigToken</returns> /// <returns>XSigToken</returns>
public abstract XSigToken GetTokenWithOffset(int offset); public abstract XSigToken GetTokenWithOffset(int offset);
}
} }

View file

@ -1,10 +1,10 @@
namespace PepperDash.Core.Intersystem.Tokens namespace PepperDash.Core.Intersystem.Tokens;
/// <summary>
/// XSig token types.
/// </summary>
public enum XSigTokenType
{ {
/// <summary>
/// XSig token types.
/// </summary>
public enum XSigTokenType
{
/// <summary> /// <summary>
/// Digital signal datatype. /// Digital signal datatype.
/// </summary> /// </summary>
@ -19,5 +19,4 @@ namespace PepperDash.Core.Intersystem.Tokens
/// Serial signal datatype. /// Serial signal datatype.
/// </summary> /// </summary>
Serial Serial
}
} }

View file

@ -18,17 +18,17 @@ using PepperDash.Core.Intersystem.Tokens;
11111111 <- denotes end of data 11111111 <- denotes end of data
*/ */
namespace PepperDash.Core.Intersystem namespace PepperDash.Core.Intersystem;
/// <summary>
/// Helper methods for creating XSig byte sequences compatible with the Intersystem Communications (ISC) symbol.
/// </summary>
/// <remarks>
/// Indexing is not from the start of each signal type but rather from the beginning of the first defined signal
/// the Intersystem Communications (ISC) symbol.
/// </remarks>
public static class XSigHelpers
{ {
/// <summary>
/// Helper methods for creating XSig byte sequences compatible with the Intersystem Communications (ISC) symbol.
/// </summary>
/// <remarks>
/// Indexing is not from the start of each signal type but rather from the beginning of the first defined signal
/// the Intersystem Communications (ISC) symbol.
/// </remarks>
public static class XSigHelpers
{
/// <summary> /// <summary>
/// Forces all outputs to 0. /// Forces all outputs to 0.
/// </summary> /// </summary>
@ -52,9 +52,6 @@ namespace PepperDash.Core.Intersystem
/// </summary> /// </summary>
/// <param name="xSigSerialization">XSig state resolver.</param> /// <param name="xSigSerialization">XSig state resolver.</param>
/// <returns>Bytes in XSig format for each token within the state representation.</returns> /// <returns>Bytes in XSig format for each token within the state representation.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(IXSigSerialization xSigSerialization) public static byte[] GetBytes(IXSigSerialization xSigSerialization)
{ {
return GetBytes(xSigSerialization, 0); return GetBytes(xSigSerialization, 0);
@ -66,9 +63,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="xSigSerialization">XSig state resolver.</param> /// <param name="xSigSerialization">XSig state resolver.</param>
/// <param name="offset">Offset to which the data will be aligned.</param> /// <param name="offset">Offset to which the data will be aligned.</param>
/// <returns>Bytes in XSig format for each token within the state representation.</returns> /// <returns>Bytes in XSig format for each token within the state representation.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(IXSigSerialization xSigSerialization, int offset) public static byte[] GetBytes(IXSigSerialization xSigSerialization, int offset)
{ {
var tokens = xSigSerialization.Serialize(); var tokens = xSigSerialization.Serialize();
@ -88,9 +82,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="index">1-based digital index</param> /// <param name="index">1-based digital index</param>
/// <param name="value">Digital data to be encoded</param> /// <param name="value">Digital data to be encoded</param>
/// <returns>Bytes in XSig format for digtial information.</returns> /// <returns>Bytes in XSig format for digtial information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int index, bool value) public static byte[] GetBytes(int index, bool value)
{ {
return GetBytes(index, 0, value); return GetBytes(index, 0, value);
@ -103,9 +94,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="offset">Index offset.</param> /// <param name="offset">Index offset.</param>
/// <param name="value">Digital data to be encoded</param> /// <param name="value">Digital data to be encoded</param>
/// <returns>Bytes in XSig format for digtial information.</returns> /// <returns>Bytes in XSig format for digtial information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int index, int offset, bool value) public static byte[] GetBytes(int index, int offset, bool value)
{ {
return new XSigDigitalToken(index + offset, value).GetBytes(); return new XSigDigitalToken(index + offset, value).GetBytes();
@ -117,9 +105,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="startIndex">Starting index of the sequence.</param> /// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="values">Digital signal value array.</param> /// <param name="values">Digital signal value array.</param>
/// <returns>Byte sequence in XSig format for digital signal information.</returns> /// <returns>Byte sequence in XSig format for digital signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int startIndex, bool[] values) public static byte[] GetBytes(int startIndex, bool[] values)
{ {
return GetBytes(startIndex, 0, values); return GetBytes(startIndex, 0, values);
@ -132,9 +117,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="offset">Index offset.</param> /// <param name="offset">Index offset.</param>
/// <param name="values">Digital signal value array.</param> /// <param name="values">Digital signal value array.</param>
/// <returns>Byte sequence in XSig format for digital signal information.</returns> /// <returns>Byte sequence in XSig format for digital signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int startIndex, int offset, bool[] values) public static byte[] GetBytes(int startIndex, int offset, bool[] values)
{ {
// Digital XSig data is 2 bytes per value // Digital XSig data is 2 bytes per value
@ -152,9 +134,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="index">1-based analog index</param> /// <param name="index">1-based analog index</param>
/// <param name="value">Analog data to be encoded</param> /// <param name="value">Analog data to be encoded</param>
/// <returns>Bytes in XSig format for analog signal information.</returns> /// <returns>Bytes in XSig format for analog signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int index, ushort value) public static byte[] GetBytes(int index, ushort value)
{ {
return GetBytes(index, 0, value); return GetBytes(index, 0, value);
@ -167,9 +146,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="offset">Index offset.</param> /// <param name="offset">Index offset.</param>
/// <param name="value">Analog data to be encoded</param> /// <param name="value">Analog data to be encoded</param>
/// <returns>Bytes in XSig format for analog signal information.</returns> /// <returns>Bytes in XSig format for analog signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int index, int offset, ushort value) public static byte[] GetBytes(int index, int offset, ushort value)
{ {
return new XSigAnalogToken(index + offset, value).GetBytes(); return new XSigAnalogToken(index + offset, value).GetBytes();
@ -181,9 +157,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="startIndex">Starting index of the sequence.</param> /// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="values">Analog signal value array.</param> /// <param name="values">Analog signal value array.</param>
/// <returns>Byte sequence in XSig format for analog signal information.</returns> /// <returns>Byte sequence in XSig format for analog signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int startIndex, ushort[] values) public static byte[] GetBytes(int startIndex, ushort[] values)
{ {
return GetBytes(startIndex, 0, values); return GetBytes(startIndex, 0, values);
@ -196,9 +169,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="offset">Index offset.</param> /// <param name="offset">Index offset.</param>
/// <param name="values">Analog signal value array.</param> /// <param name="values">Analog signal value array.</param>
/// <returns>Byte sequence in XSig format for analog signal information.</returns> /// <returns>Byte sequence in XSig format for analog signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int startIndex, int offset, ushort[] values) public static byte[] GetBytes(int startIndex, int offset, ushort[] values)
{ {
// Analog XSig data is 4 bytes per value // Analog XSig data is 4 bytes per value
@ -216,9 +186,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="index">1-based serial index</param> /// <param name="index">1-based serial index</param>
/// <param name="value">Serial data to be encoded</param> /// <param name="value">Serial data to be encoded</param>
/// <returns>Bytes in XSig format for serial signal information.</returns> /// <returns>Bytes in XSig format for serial signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int index, string value) public static byte[] GetBytes(int index, string value)
{ {
return GetBytes(index, 0, value); return GetBytes(index, 0, value);
@ -231,9 +198,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="offset">Index offset.</param> /// <param name="offset">Index offset.</param>
/// <param name="value">Serial data to be encoded</param> /// <param name="value">Serial data to be encoded</param>
/// <returns>Bytes in XSig format for serial signal information.</returns> /// <returns>Bytes in XSig format for serial signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int index, int offset, string value) public static byte[] GetBytes(int index, int offset, string value)
{ {
return new XSigSerialToken(index + offset, value).GetBytes(); return new XSigSerialToken(index + offset, value).GetBytes();
@ -245,9 +209,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="startIndex">Starting index of the sequence.</param> /// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="values">Serial signal value array.</param> /// <param name="values">Serial signal value array.</param>
/// <returns>Byte sequence in XSig format for serial signal information.</returns> /// <returns>Byte sequence in XSig format for serial signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int startIndex, string[] values) public static byte[] GetBytes(int startIndex, string[] values)
{ {
return GetBytes(startIndex, 0, values); return GetBytes(startIndex, 0, values);
@ -260,9 +221,6 @@ namespace PepperDash.Core.Intersystem
/// <param name="offset">Index offset.</param> /// <param name="offset">Index offset.</param>
/// <param name="values">Serial signal value array.</param> /// <param name="values">Serial signal value array.</param>
/// <returns>Byte sequence in XSig format for serial signal information.</returns> /// <returns>Byte sequence in XSig format for serial signal information.</returns>
/// <summary>
/// GetBytes method
/// </summary>
public static byte[] GetBytes(int startIndex, int offset, string[] values) public static byte[] GetBytes(int startIndex, int offset, string[] values)
{ {
// Serial XSig data is not fixed-length like the other formats // Serial XSig data is not fixed-length like the other formats
@ -277,5 +235,4 @@ namespace PepperDash.Core.Intersystem
return bytes; return bytes;
} }
}
} }

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