Compare commits

..

31 Commits

Author SHA1 Message Date
Andrew Welker
8bab3dc966 fix: send touchpanelKey message with all room combiner checks 2025-10-23 11:21:17 -05:00
Andrew Welker
514ac850ca fix: send touchpanel key correctly 2025-10-23 10:01:05 -05:00
Andrew Welker
44432f7a41 fix: send touchpanel key to client when client joins direct server 2025-10-23 09:54:03 -05:00
Andrew Welker
99253b30c2 fix: send touchpanel key to client when client joins 2025-10-23 09:49:45 -05:00
Andrew Welker
bf248fe33e Merge pull request #1343 from PepperDash/device-interface-system
device interface system
2025-10-23 10:40:56 -04:00
cdenig
2f44040e4f Merge pull request #1344 from PepperDash/feature/allow-config-tool-v2-structure
Feature/allow config tool v2 structure
2025-10-23 10:15:03 -04:00
Neil Dorin
10399a1be8 Update src/PepperDash.Core/Config/PortalConfigReader.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 13:54:08 -06:00
Neil Dorin
5409db193c Update src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-22 13:53:23 -06:00
Neil Dorin
f1ce54a524 Merge branch 'main' into feature/allow-config-tool-v2-structure 2025-10-22 13:48:22 -06:00
Andrew Welker
7c72a0d905 fix: send device interface list on client join
In order to avoid updating the MC Edge API to send device interfaces, one of the responses to the clientJoined message will now be a message with the type `/system/deviceInterfaces`. This has a corresponding handler in the core library to put the interfaces in the correct location.
2025-10-21 18:19:09 -05:00
Andrew Welker
5d5e78629e chore: update local build version to 2.18.2-local 2025-10-21 18:17:25 -05:00
Neil Dorin
dab5484d6e Merge pull request #1342 from PepperDash/wsdebug-persistence
Unique Client IDs
2025-10-15 15:16:46 -04:00
Andrew Welker
5c35a3be45 fix: catch exceptions in handlers directly
Previously, any exceptions that were occuring in a hander's action were being swalled due to being off on another thread. Now, those exceptions are caught and printed out.
2025-10-15 14:03:17 -05:00
Andrew Welker
6cb98e12fa fix: use correct collection for program stop 2025-10-15 14:02:06 -05:00
Andrew Welker
608601990b docs: fix Copilot comments 2025-10-15 12:54:14 -05:00
Andrew Welker
3e0f318f7f fix: update log methods to all be consistent 2025-10-15 12:36:42 -05:00
Andrew Welker
98d0cc8fdc docs: add missing XML comments for Mobile Control Project 2025-10-15 12:26:57 -05:00
Andrew Welker
c557c6cdd6 fix: mobileinfo & CWS info call report the correct data 2025-10-15 11:49:06 -05:00
Andrew Welker
8525134ae7 fix: direct server clients now have unique client IDs
Using the generated security token as an ID was presenting problems with duplicate connections using the same ID and trying to figure out where to send the messages. Now, the clients have a unique ID that's an increasing integer that restarts at 1 when the program restarts.
2025-10-15 09:50:12 -05:00
Andrew Welker
1197b15a33 Merge pull request #1340 from PepperDash/display-bridging
Display bridging, Docs, Appdebug
2025-10-10 10:21:35 -04:00
Andrew Welker
ea6a7568fc Merge remote-tracking branch 'origin/feature/add-IKeyed-to-interfaces' into display-bridging 2025-10-09 16:08:17 -05:00
Neil Dorin
c284c4275f feat: Enhance UDPServer initialization in Connect method
Updated the `Connect` method in `GenericUdpServer.cs` to include error handling for hostname parsing. The method now attempts to create a `UDPServer` instance with specified parameters and falls back to default initialization if an error occurs. This improves flexibility and robustness in server setup.
2025-10-09 11:31:38 -06:00
Neil Dorin
0418f8a7cc fix: Fix typos and enhance item selection handling
Corrected parameter name in GenericUdpServer constructor.
Added new action for item selection in ISelectableItemsMessenger
with error handling for missing or invalid keys.
Updated SetItems method to improve clarity and ensure proper
clearing and re-adding of item actions.
2025-10-09 09:40:46 -06:00
Neil Dorin
419177ccd5 fix: Add item management and update handling in messenger
Introduces a new private field `_itemKeys` to store item keys. Adds the `SetItems` method to manage item actions and update events, ensuring proper registration and cleanup. The `SendFullStatus` method is now invoked from a dedicated event handler `LocalItem_ItemUpdated`, improving the handling of item updates.
2025-10-08 12:24:17 -06:00
Neil Dorin
bd01e2bacc fix: Add KeyName class and update camera messaging
This commit introduces a new `KeyName` class implementing the `IKeyName` interface, enhancing the representation of camera data. The `CameraController_CameraSelected` and `SendFullStatus` methods are updated to utilize `KeyName` instances for selected and listed cameras, improving data encapsulation and consistency in the `IHasCamerasWithControlsStateMessage`. Additionally, new using directives for logging and core functionalities are added.
2025-10-07 17:10:11 -06:00
Neil Dorin
2928c5cf94 feat: Enhance camera capabilities and messaging structure
- Introduced `ICameraCapabilities` interface and `CameraCapabilities` class for defining camera features like pan, tilt, zoom, and focus.
- Modified `IHasCameras` interface to include a list of `IHasCameraControls` objects for improved camera management.
- Refactored `CameraBaseMessenger` to be generic, enhancing flexibility and type safety.
- Updated `SendCameraFullMessageObject` to include detailed camera capabilities in status messages.
- Added `CameraStateMessage` class to encapsulate camera state, including control support and capabilities.
- Updated `IHasCamerasWithControlMessenger` to use `IKeyName` for camera list and selected camera properties, improving type consistency.
- Enhanced `MobileControlSystemController` to manage devices implementing `IHasCameraControls`, creating appropriate messengers for different device types.
2025-10-07 15:37:31 -06:00
Neil Dorin
82b5dc96c1 feat: Deprecate IHasCamerasMessenger; introduce new controls
Mark IHasCamerasMessenger as obsolete and replace it with
IHasCamerasWithControlMessenger, which adds functionality
for devices with camera controls. A new state message class,
IHasCamerasWithControlsStateMessage, has been added to
encapsulate camera state. Update MobileControlSystemController
to use the new messenger implementation.
2025-10-07 12:00:01 -06:00
Neil Dorin
37cea8a11c fix: Refactor camera control interfaces and event arguments
Significantly restructure camera control interfaces and event arguments.
Removed obsolete interfaces like `IHasCameras` and `CameraSelectedEventArgs`,
and introduced generic event argument classes for improved type safety.
Added `IHasCamerasWithControls` for better management of camera controls.
Corrected the `IHasCameraMuteWithUnmuteReqeust` interface name.
Reintroduced the `eCameraControlMode` enum to define camera control modes.
These changes enhance the organization, clarity, and functionality of the camera control system.
2025-10-07 11:52:07 -06:00
Neil Dorin
9de94bd65f fix: Update v2 config detection criteria
Changed the logic for identifying v2 configuration files. The check now looks for the presence of a "versions" node instead of the absence of "system" or "template" nodes, reflecting an update in the configuration file structure.
2025-09-22 15:05:06 -06:00
Neil Dorin
ff46fb8f29 feat: Add versioning support to EssentialsConfig
Introduce `Versions` property in `EssentialsConfig` to hold version information.
Add `VersionData` class for Essentials and package versions, and `NugetVersion` class for individual package details.
Retain and document `SystemTemplateConfigs` class.
2025-09-22 14:55:58 -06:00
Neil Dorin
d9243def30 feat: Adds ability to read configs generated from v2 config tool that are pre-merged don't have system or template objects
Refactor config handling and improve documentation

- Updated `PortalConfigReader.cs` to use constants for configuration keys, enhancing maintainability and readability. Improved error logging with `Debug.LogError`.
- Modified `ConfigReader.cs` to handle v2 configuration format, streamlining the loading process and avoiding redundant parsing.
- Added XML documentation comments to properties in `EssentialsConfig.cs`, improving code documentation. Initialized `Rooms` property in the constructor.
- Enhanced `SystemTemplateConfigs` class with XML documentation for better clarity on its properties.
2025-09-22 14:22:57 -06:00
62 changed files with 2215 additions and 911 deletions

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>2.15.1-local</Version>
<Version>2.18.2-local</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<Authors>PepperDash Technology</Authors>
<Company>PepperDash Technology</Company>

View File

@@ -131,14 +131,14 @@ namespace PepperDash.Core
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="buffefSize"></param>
public GenericUdpServer(string key, string address, int port, int buffefSize)
/// <param name="bufferSize"></param>
public GenericUdpServer(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address;
Port = port;
BufferSize = buffefSize;
BufferSize = bufferSize;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
@@ -194,7 +194,21 @@ namespace PepperDash.Core
{
if (Server == null)
{
Server = new UDPServer();
try
{
var address = IPAddress.Parse(Hostname);
Server = new UDPServer(address, Port, BufferSize);
}
catch (Exception ex)
{
this.LogError("Error parsing IP Address '{ipAddress}': message: {message}", Hostname, ex.Message);
this.LogInformation("Creating UDPServer with default buffersize");
Server = new UDPServer();
}
}
if (string.IsNullOrEmpty(Hostname))

View File

@@ -9,40 +9,59 @@ using Serilog.Events;
namespace PepperDash.Core.Config
{
/// <summary>
/// Reads a Portal formatted config file
/// </summary>
public class PortalConfigReader
{
/// <summary>
/// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
/// </summary>
/// <returns>JObject of config file</returns>
public static void ReadAndMergeFileIfNecessary(string filePath, string savePath)
const string template = "template";
const string system = "system";
const string systemUrl = "system_url";
const string templateUrl = "template_url";
const string info = "info";
const string devices = "devices";
const string rooms = "rooms";
const string sourceLists = "sourceLists";
const string destinationLists = "destinationLists";
const string cameraLists = "cameraLists";
const string audioControlPointLists = "audioControlPointLists";
const string tieLines = "tieLines";
const string joinMaps = "joinMaps";
const string global = "global";
/// <summary>
/// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
/// </summary>
/// <returns>JObject of config file</returns>
public static void ReadAndMergeFileIfNecessary(string filePath, string savePath)
{
try
{
if (!File.Exists(filePath))
{
Debug.Console(1, Debug.ErrorLogLevel.Error,
Debug.LogError(
"ERROR: Configuration file not present. Please load file to {0} and reset program", filePath);
}
using (StreamReader fs = new StreamReader(filePath))
{
var jsonObj = JObject.Parse(fs.ReadToEnd());
if(jsonObj["template"] != null && jsonObj["system"] != null)
if(jsonObj[template] != null && jsonObj[system] != null)
{
// it's a double-config, merge it.
var merged = MergeConfigs(jsonObj);
if (jsonObj["system_url"] != null)
if (jsonObj[systemUrl] != null)
{
merged["systemUrl"] = jsonObj["system_url"].Value<string>();
merged[systemUrl] = jsonObj[systemUrl].Value<string>();
}
if (jsonObj["template_url"] != null)
if (jsonObj[templateUrl] != null)
{
merged["templateUrl"] = jsonObj["template_url"].Value<string>();
merged[templateUrl] = jsonObj[templateUrl].Value<string>();
}
jsonObj = merged;
@@ -77,62 +96,62 @@ namespace PepperDash.Core.Config
var merged = new JObject();
// Put together top-level objects
if (system["info"] != null)
merged.Add("info", Merge(template["info"], system["info"], "infO"));
if (system[info] != null)
merged.Add(info, Merge(template[info], system[info], info));
else
merged.Add("info", template["info"]);
merged.Add(info, template[info]);
merged.Add("devices", MergeArraysOnTopLevelProperty(template["devices"] as JArray,
system["devices"] as JArray, "key", "devices"));
merged.Add(devices, MergeArraysOnTopLevelProperty(template[devices] as JArray,
system[devices] as JArray, "key", devices));
if (system["rooms"] == null)
merged.Add("rooms", template["rooms"]);
if (system[rooms] == null)
merged.Add(rooms, template[rooms]);
else
merged.Add("rooms", MergeArraysOnTopLevelProperty(template["rooms"] as JArray,
system["rooms"] as JArray, "key", "rooms"));
merged.Add(rooms, MergeArraysOnTopLevelProperty(template[rooms] as JArray,
system[rooms] as JArray, "key", rooms));
if (system["sourceLists"] == null)
merged.Add("sourceLists", template["sourceLists"]);
if (system[sourceLists] == null)
merged.Add(sourceLists, template[sourceLists]);
else
merged.Add("sourceLists", Merge(template["sourceLists"], system["sourceLists"], "sourceLists"));
merged.Add(sourceLists, Merge(template[sourceLists], system[sourceLists], sourceLists));
if (system["destinationLists"] == null)
merged.Add("destinationLists", template["destinationLists"]);
if (system[destinationLists] == null)
merged.Add(destinationLists, template[destinationLists]);
else
merged.Add("destinationLists",
Merge(template["destinationLists"], system["destinationLists"], "destinationLists"));
merged.Add(destinationLists,
Merge(template[destinationLists], system[destinationLists], destinationLists));
if (system["cameraLists"] == null)
merged.Add("cameraLists", template["cameraLists"]);
if (system[cameraLists] == null)
merged.Add(cameraLists, template[cameraLists]);
else
merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists"));
merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists));
if (system["audioControlPointLists"] == null)
merged.Add("audioControlPointLists", template["audioControlPointLists"]);
if (system[audioControlPointLists] == null)
merged.Add(audioControlPointLists, template[audioControlPointLists]);
else
merged.Add("audioControlPointLists",
Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists"));
merged.Add(audioControlPointLists,
Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists));
// Template tie lines take precedence. Config tool doesn't do them at system
// level anyway...
if (template["tieLines"] != null)
merged.Add("tieLines", template["tieLines"]);
else if (system["tieLines"] != null)
merged.Add("tieLines", system["tieLines"]);
if (template[tieLines] != null)
merged.Add(tieLines, template[tieLines]);
else if (system[tieLines] != null)
merged.Add(tieLines, system[tieLines]);
else
merged.Add("tieLines", new JArray());
merged.Add(tieLines, new JArray());
if (template["joinMaps"] != null)
merged.Add("joinMaps", template["joinMaps"]);
if (template[joinMaps] != null)
merged.Add(joinMaps, template[joinMaps]);
else
merged.Add("joinMaps", new JObject());
merged.Add(joinMaps, new JObject());
if (system["global"] != null)
merged.Add("global", Merge(template["global"], system["global"], "global"));
if (system[global] != null)
merged.Add(global, Merge(template[global], system[global], global));
else
merged.Add("global", template["global"]);
merged.Add(global, template[global]);
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
return merged;
@@ -228,7 +247,7 @@ namespace PepperDash.Core.Config
}
catch (Exception e)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Cannot merge items at path {0}: \r{1}", propPath, e);
Debug.LogError($"Cannot merge items at path {propPath}: \r{e}");
}
}
}

View File

@@ -124,22 +124,35 @@ namespace PepperDash.Essentials.Core.Config
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config");
return true;
}
}
else
{
var doubleObj = JObject.Parse(fs.ReadToEnd());
ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject<EssentialsConfig>();
var parsedConfig = JObject.Parse(fs.ReadToEnd());
// Extract SystemUrl and TemplateUrl into final config output
if (doubleObj["system_url"] != null)
// Check if it's a v2 config (check for "version" node)
// this means it's already merged by the Portal API
// from the v2 config tool
var isV2Config = parsedConfig["versions"] != null;
if (isV2Config)
{
ConfigObject.SystemUrl = doubleObj["system_url"].Value<string>();
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config");
return true;
}
if (doubleObj["template_url"] != null)
// Extract SystemUrl and TemplateUrl into final config output
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
if (parsedConfig["system_url"] != null)
{
ConfigObject.TemplateUrl = doubleObj["template_url"].Value<string>();
ConfigObject.SystemUrl = parsedConfig["system_url"].Value<string>();
}
if (parsedConfig["template_url"] != null)
{
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
}
}

View File

@@ -16,13 +16,21 @@ namespace PepperDash.Essentials.Core.Config
/// </summary>
public class EssentialsConfig : BasicConfig
{
[JsonProperty("system_url")]
/// <summary>
/// Gets or sets the SystemUrl
/// </summary>
[JsonProperty("system_url")]
public string SystemUrl { get; set; }
[JsonProperty("template_url")]
/// <summary>
/// Gets or sets the TemplateUrl
/// </summary>
[JsonProperty("template_url")]
public string TemplateUrl { get; set; }
/// <summary>
/// Gets the SystemUuid extracted from the SystemUrl
/// </summary>
[JsonProperty("systemUuid")]
public string SystemUuid
{
@@ -45,6 +53,9 @@ namespace PepperDash.Essentials.Core.Config
}
}
/// <summary>
/// Gets the TemplateUuid extracted from the TemplateUrl
/// </summary>
[JsonProperty("templateUuid")]
public string TemplateUuid
{
@@ -67,30 +78,84 @@ namespace PepperDash.Essentials.Core.Config
}
}
[JsonProperty("rooms")]
/// <summary>
/// Gets or sets the Rooms
/// </summary>
[JsonProperty("rooms")]
public List<DeviceConfig> Rooms { get; set; }
/// <summary>
/// Gets or sets the Versions
/// </summary>
public VersionData Versions { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="EssentialsConfig"/> class.
/// </summary>
public EssentialsConfig()
: base()
{
Rooms = new List<DeviceConfig>();
}
}
/// <summary>
/// Represents a SystemTemplateConfigs
/// </summary>
public class SystemTemplateConfigs
/// <summary>
/// Represents version data for Essentials and its packages
/// </summary>
public class VersionData
{
/// <summary>
/// Gets or sets the Essentials version
/// </summary>
[JsonProperty("essentials")]
public NugetVersion Essentials { get; set; }
/// <summary>
/// Gets or sets the list of Packages
/// </summary>
[JsonProperty("packages")]
public List<NugetVersion> Packages { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="VersionData"/> class.
/// </summary>
public VersionData()
{
Packages = new List<NugetVersion>();
}
}
/// <summary>
/// Represents a NugetVersion
/// </summary>
public class NugetVersion
{
/// <summary>
/// Gets or sets the Version
/// </summary>
[JsonProperty("version")]
public string Version { get; set; }
/// <summary>
/// Gets or sets the PackageId
/// </summary>
[JsonProperty("packageId")]
public string PackageId { get; set; }
}
/// <summary>
/// Represents a SystemTemplateConfigs
/// </summary>
public class SystemTemplateConfigs
{
/// <summary>
/// Gets or sets the System
/// </summary>
/// <summary>
/// Gets or sets the System
/// </summary>
public EssentialsConfig System { get; set; }
/// <summary>
/// Gets or sets the Template
/// </summary>
public EssentialsConfig Template { get; set; }
}
}

View File

@@ -1,323 +0,0 @@
using System;
using System.Collections.Generic;
using PepperDash.Core;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Enum for camera control modes
/// </summary>
public enum eCameraControlMode
{
/// <summary>
/// Manual control mode, where the camera is controlled directly by the user or system
/// </summary>
Manual = 0,
/// <summary>
/// Off control mode, where the camera is turned off or disabled
/// </summary>
Off,
/// <summary>
/// Auto control mode, where the camera automatically adjusts settings based on the environment or conditions
/// </summary>
Auto
}
/// <summary>
/// Interface for devices that have cameras
/// </summary>
public interface IHasCameras : IKeyName
{
/// <summary>
/// Event that is raised when a camera is selected
/// </summary>
event EventHandler<CameraSelectedEventArgs> CameraSelected;
/// <summary>
/// List of cameras on the device. This should be a list of CameraBase objects
/// </summary>
List<CameraBase> Cameras { get; }
/// <summary>
/// The currently selected camera. This should be a CameraBase object
/// </summary>
CameraBase SelectedCamera { get; }
/// <summary>
/// Feedback that indicates the currently selected camera
/// </summary>
StringFeedback SelectedCameraFeedback { get; }
/// <summary>
/// Selects a camera from the list of available cameras based on the provided key.
/// </summary>
/// <param name="key">The unique identifier or name of the camera to select.</param>
void SelectCamera(string key);
}
/// <summary>
/// Defines the contract for IHasCodecCameras
/// </summary>
public interface IHasCodecCameras : IHasCameras, IHasFarEndCameraControl
{
}
/// <summary>
/// To be implmented on codecs that can disable their camera(s) to blank the near end video
/// </summary>
public interface IHasCameraOff
{
/// <summary>
/// Feedback that indicates whether the camera is off
/// </summary>
BoolFeedback CameraIsOffFeedback { get; }
/// <summary>
/// Turns the camera off, blanking the near end video
/// </summary>
void CameraOff();
}
/// <summary>
/// Describes the ability to mute and unmute camera video
/// </summary>
public interface IHasCameraMute
{
/// <summary>
/// Feedback that indicates whether the camera is muted
/// </summary>
BoolFeedback CameraIsMutedFeedback { get; }
/// <summary>
/// Mutes the camera video, preventing it from being sent to the far end
/// </summary>
void CameraMuteOn();
/// <summary>
/// Unmutes the camera video, allowing it to be sent to the far end
/// </summary>
void CameraMuteOff();
/// <summary>
/// Toggles the camera mute state. If the camera is muted, it will be unmuted, and vice versa.
/// </summary>
void CameraMuteToggle();
}
/// <summary>
/// Interface for devices that can mute and unmute their camera video, with an event for unmute requests
/// </summary>
public interface IHasCameraMuteWithUnmuteReqeust : IHasCameraMute
{
/// <summary>
/// Event that is raised when a video unmute is requested, typically by the far end
/// </summary>
event EventHandler VideoUnmuteRequested;
}
/// <summary>
/// Event arguments for the CameraSelected event
/// </summary>
public class CameraSelectedEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the SelectedCamera
/// </summary>
public CameraBase SelectedCamera { get; private set; }
/// <summary>
/// Constructor for CameraSelectedEventArgs
/// </summary>
/// <param name="camera"></param>
public CameraSelectedEventArgs(CameraBase camera)
{
SelectedCamera = camera;
}
}
/// <summary>
/// Interface for devices that have a far end camera control
/// </summary>
public interface IHasFarEndCameraControl
{
/// <summary>
/// Gets the far end camera, which is typically a CameraBase object that represents the camera at the far end of a call
/// </summary>
CameraBase FarEndCamera { get; }
/// <summary>
/// Feedback that indicates whether the far end camera is being controlled
/// </summary>
BoolFeedback ControllingFarEndCameraFeedback { get; }
}
/// <summary>
/// Defines the contract for IAmFarEndCamera
/// </summary>
public interface IAmFarEndCamera
{
}
/// <summary>
/// Interface for devices that have camera controls
/// </summary>
public interface IHasCameraControls
{
}
/// <summary>
/// Defines the contract for IHasCameraPtzControl
/// </summary>
public interface IHasCameraPtzControl : IHasCameraPanControl, IHasCameraTiltControl, IHasCameraZoomControl
{
/// <summary>
/// Resets the camera position
/// </summary>
void PositionHome();
}
/// <summary>
/// Interface for camera pan control
/// </summary>
public interface IHasCameraPanControl : IHasCameraControls
{
/// <summary>
/// Pans the camera left
/// </summary>
void PanLeft();
/// <summary>
/// Pans the camera right
/// </summary>
void PanRight();
/// <summary>
/// Stops the camera pan movement
/// </summary>
void PanStop();
}
/// <summary>
/// Defines the contract for IHasCameraTiltControl
/// </summary>
public interface IHasCameraTiltControl : IHasCameraControls
{
/// <summary>
/// Tilts the camera down
/// </summary>
void TiltDown();
/// <summary>
/// Tilts the camera up
/// </summary>
void TiltUp();
/// <summary>
/// Stops the camera tilt movement
/// </summary>
void TiltStop();
}
/// <summary>
/// Defines the contract for IHasCameraZoomControl
/// </summary>
public interface IHasCameraZoomControl : IHasCameraControls
{
/// <summary>
/// Zooms the camera in
/// </summary>
void ZoomIn();
/// <summary>
/// Zooms the camera out
/// </summary>
void ZoomOut();
/// <summary>
/// Stops the camera zoom movement
/// </summary>
void ZoomStop();
}
/// <summary>
/// Defines the contract for IHasCameraFocusControl
/// </summary>
public interface IHasCameraFocusControl : IHasCameraControls
{
/// <summary>
/// Focuses the camera near
/// </summary>
void FocusNear();
/// <summary>
/// Focuses the camera far
/// </summary>
void FocusFar();
/// <summary>
/// Stops the camera focus movement
/// </summary>
void FocusStop();
/// <summary>
/// Triggers the camera's auto focus functionality, if available.
/// </summary>
void TriggerAutoFocus();
}
/// <summary>
/// Interface for devices that have auto focus mode control
/// </summary>
public interface IHasAutoFocusMode
{
/// <summary>
/// Sets the focus mode to auto or manual, or toggles between them.
/// </summary>
void SetFocusModeAuto();
/// <summary>
/// Sets the focus mode to manual, allowing for manual focus adjustments.
/// </summary>
void SetFocusModeManual();
/// <summary>
/// Toggles the focus mode between auto and manual.
/// </summary>
void ToggleFocusMode();
}
/// <summary>
/// Interface for devices that have camera auto mode control
/// </summary>
public interface IHasCameraAutoMode : IHasCameraControls
{
/// <summary>
/// Enables or disables the camera's auto mode, which may include automatic adjustments for focus, exposure, and other settings.
/// </summary>
void CameraAutoModeOn();
/// <summary>
/// Disables the camera's auto mode, allowing for manual control of camera settings.
/// </summary>
void CameraAutoModeOff();
/// <summary>
/// Toggles the camera's auto mode state. If the camera is in auto mode, it will switch to manual mode, and vice versa.
/// </summary>
void CameraAutoModeToggle();
/// <summary>
/// Feedback that indicates whether the camera's auto mode is currently enabled.
/// </summary>
BoolFeedback CameraAutoModeIsOnFeedback { get; }
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Event arguments for the CameraSelected event
/// </summary>
[Obsolete("Use CameraSelectedEventArgs<T> instead. This class will be removed in a future version")]
public class CameraSelectedEventArgs : EventArgs
{
/// Gets or sets the SelectedCamera
/// </summary>
public CameraBase SelectedCamera { get; private set; }
/// <summary>
/// Constructor for CameraSelectedEventArgs
/// </summary>
/// <param name="camera"></param>
public CameraSelectedEventArgs(CameraBase camera)
{
SelectedCamera = camera;
}
}
/// <summary>
/// Event arguments for the CameraSelected event
/// </summary>
/// <typeparam name="T"></typeparam>
public class CameraSelectedEventArgs<T> : EventArgs
{
/// <summary>
/// Gets or sets the SelectedCamera
/// </summary>
public T SelectedCamera { get; private set; }
/// <summary>
/// Constructor for CameraSelectedEventArgs
/// </summary>
/// <param name="camera"></param>
public CameraSelectedEventArgs(T camera)
{
SelectedCamera = camera;
}
}
}

View File

@@ -0,0 +1,12 @@
using PepperDash.Core;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Defines the contract for IAmFarEndCamera
/// </summary>
public interface IAmFarEndCamera : IKeyName
{
}
}

View File

@@ -0,0 +1,86 @@
using Newtonsoft.Json;
using PepperDash.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for camera capabilities
/// </summary>
public interface ICameraCapabilities: IKeyName
{
/// <summary>
/// Indicates whether the camera can pan
/// </summary>
[JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)]
bool CanPan { get; }
/// <summary>
/// Indicates whether the camera can tilt
/// </summary>
[JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)]
bool CanTilt { get; }
/// <summary>
/// Indicates whether the camera can zoom
/// </summary>
[JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)]
bool CanZoom { get; }
/// <summary>
/// Indicates whether the camera can focus
/// </summary>
[JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)]
bool CanFocus { get; }
}
/// <summary>
/// Indicates the capabilities of a camera
/// </summary>
public class CameraCapabilities : ICameraCapabilities
{
/// <summary>
/// Unique Key
/// </summary>
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
public string Key { get; set; }
/// <summary>
/// Isn't it obvious :)
/// </summary>
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; }
/// <summary>
/// Indicates whether the camera can pan
/// </summary>
[JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)]
public bool CanPan { get; set; }
/// <summary>
/// Indicates whether the camera can tilt
/// </summary>
[JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)]
public bool CanTilt { get; set; }
/// <summary>
/// Indicates whether the camera can zoom
/// </summary>
[JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)]
public bool CanZoom { get; set; }
/// <summary>
/// Indicates whether the camera can focus
/// </summary>
[JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)]
public bool CanFocus { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that have auto focus mode control
/// </summary>
public interface IHasAutoFocusMode : IHasCameraControls
{
/// <summary>
/// Sets the focus mode to auto or manual, or toggles between them.
/// </summary>
void SetFocusModeAuto();
/// <summary>
/// Sets the focus mode to manual, allowing for manual focus adjustments.
/// </summary>
void SetFocusModeManual();
/// <summary>
/// Toggles the focus mode between auto and manual.
/// </summary>
void ToggleFocusMode();
}
}

View File

@@ -0,0 +1,31 @@
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that have camera auto mode control
/// </summary>
public interface IHasCameraAutoMode : IHasCameraControls
{
/// <summary>
/// Enables or disables the camera's auto mode, which may include automatic adjustments for focus, exposure, and other settings.
/// </summary>
void CameraAutoModeOn();
/// <summary>
/// Disables the camera's auto mode, allowing for manual control of camera settings.
/// </summary>
void CameraAutoModeOff();
/// <summary>
/// Toggles the camera's auto mode state. If the camera is in auto mode, it will switch to manual mode, and vice versa.
/// </summary>
void CameraAutoModeToggle();
/// <summary>
/// Feedback that indicates whether the camera's auto mode is currently enabled.
/// </summary>
BoolFeedback CameraAutoModeIsOnFeedback { get; }
}
}

View File

@@ -0,0 +1,13 @@
using PepperDash.Core;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that have camera controls
/// </summary>
public interface IHasCameraControls : IKeyName
{
}
}

View File

@@ -0,0 +1,29 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Defines the contract for IHasCameraFocusControl
/// </summary>
public interface IHasCameraFocusControl : IHasCameraControls
{
/// <summary>
/// Focuses the camera near
/// </summary>
void FocusNear();
/// <summary>
/// Focuses the camera far
/// </summary>
void FocusFar();
/// <summary>
/// Stops the camera focus movement
/// </summary>
void FocusStop();
/// <summary>
/// Triggers the camera's auto focus functionality, if available.
/// </summary>
void TriggerAutoFocus();
}
}

View File

@@ -0,0 +1,31 @@
using PepperDash.Core;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Describes the ability to mute and unmute camera video
/// </summary>
public interface IHasCameraMute : IKeyName
{
/// <summary>
/// Feedback that indicates whether the camera is muted
/// </summary>
BoolFeedback CameraIsMutedFeedback { get; }
/// <summary>
/// Mutes the camera video, preventing it from being sent to the far end
/// </summary>
void CameraMuteOn();
/// <summary>
/// Unmutes the camera video, allowing it to be sent to the far end
/// </summary>
void CameraMuteOff();
/// <summary>
/// Toggles the camera mute state. If the camera is muted, it will be unmuted, and vice versa.
/// </summary>
void CameraMuteToggle();
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that can mute and unmute their camera video, with an event for unmute requests
/// </summary>
public interface IHasCameraMuteWithUnmuteReqeust : IHasCameraMute
{
/// <summary>
/// Event that is raised when a video unmute is requested, typically by the far end
/// </summary>
event EventHandler VideoUnmuteRequested;
}
}

View File

@@ -0,0 +1,27 @@
using PepperDash.Essentials.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// To be implmented on codecs that can disable their camera(s) to blank the near end video
/// </summary>
public interface IHasCameraOff : IHasCameraControls
{
/// <summary>
/// Feedback that indicates whether the camera is off
/// </summary>
BoolFeedback CameraIsOffFeedback { get; }
/// <summary>
/// Turns the camera off, blanking the near end video
/// </summary>
void CameraOff();
}
}

View File

@@ -0,0 +1,24 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for camera pan control
/// </summary>
public interface IHasCameraPanControl : IHasCameraControls
{
/// <summary>
/// Pans the camera left
/// </summary>
void PanLeft();
/// <summary>
/// Pans the camera right
/// </summary>
void PanRight();
/// <summary>
/// Stops the camera pan movement
/// </summary>
void PanStop();
}
}

View File

@@ -0,0 +1,14 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Defines the contract for IHasCameraPtzControl
/// </summary>
public interface IHasCameraPtzControl : IHasCameraPanControl, IHasCameraTiltControl, IHasCameraZoomControl
{
/// <summary>
/// Resets the camera position
/// </summary>
void PositionHome();
}
}

View File

@@ -0,0 +1,24 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Defines the contract for IHasCameraTiltControl
/// </summary>
public interface IHasCameraTiltControl : IHasCameraControls
{
/// <summary>
/// Tilts the camera down
/// </summary>
void TiltDown();
/// <summary>
/// Tilts the camera up
/// </summary>
void TiltUp();
/// <summary>
/// Stops the camera tilt movement
/// </summary>
void TiltStop();
}
}

View File

@@ -0,0 +1,24 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Defines the contract for IHasCameraZoomControl
/// </summary>
public interface IHasCameraZoomControl : IHasCameraControls
{
/// <summary>
/// Zooms the camera in
/// </summary>
void ZoomIn();
/// <summary>
/// Zooms the camera out
/// </summary>
void ZoomOut();
/// <summary>
/// Stops the camera zoom movement
/// </summary>
void ZoomStop();
}
}

View File

@@ -0,0 +1,41 @@
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using System;
using System.Collections.Generic;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that have cameras
/// </summary>
[Obsolete("Use IHasCamerasWithControls instead. This interface will be removed in a future version")]
public interface IHasCameras : IKeyName
{
/// <summary>
/// Event that is raised when a camera is selected
/// </summary>
event EventHandler<CameraSelectedEventArgs> CameraSelected;
/// <summary>
/// List of cameras on the device. This should be a list of CameraBase objects
/// </summary>
List<CameraBase> Cameras { get; }
/// <summary>
/// The currently selected camera. This should be a CameraBase object
/// </summary>
CameraBase SelectedCamera { get; }
/// <summary>
/// Feedback that indicates the currently selected camera
/// </summary>
StringFeedback SelectedCameraFeedback { get; }
/// <summary>
/// Selects a camera from the list of available cameras based on the provided key.
/// </summary>
/// <param name="key">The unique identifier or name of the camera to select.</param>
void SelectCamera(string key);
}
}

View File

@@ -0,0 +1,40 @@
using PepperDash.Core;
using PepperDash.Essentials.Core;
using System;
using System.Collections.Generic;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that have cameras with controls
/// </summary>
public interface IHasCamerasWithControls : IKeyName, IKeyed
{
/// <summary>
/// List of cameras on the device. This should be a list of IHasCameraControls objects
/// </summary>
List<IHasCameraControls> Cameras { get; }
/// <summary>
/// The currently selected camera. This should be an IHasCameraControls object
/// </summary>
IHasCameraControls SelectedCamera { get; }
/// <summary>
/// Feedback that indicates the currently selected camera
/// </summary>
StringFeedback SelectedCameraFeedback { get; }
/// <summary>
/// Event that is raised when a camera is selected
/// </summary>
event EventHandler<CameraSelectedEventArgs<IHasCameraControls>> CameraSelected;
/// <summary>
/// Selects a camera from the list of available cameras based on the provided key.
/// </summary>
/// <param name="key"></param>
void SelectCamera(string key);
}
}

View File

@@ -0,0 +1,13 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Defines the contract for IHasCodecCameras
/// </summary>
public interface IHasCodecCameras : IHasCameras, IHasFarEndCameraControl
{
}
}

View File

@@ -0,0 +1,23 @@
using PepperDash.Core;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Interface for devices that have a far end camera control
/// </summary>
public interface IHasFarEndCameraControl : IKeyName
{
/// <summary>
/// Gets the far end camera, which is typically a CameraBase object that represents the camera at the far end of a call
/// </summary>
CameraBase FarEndCamera { get; }
/// <summary>
/// Feedback that indicates whether the far end camera is being controlled
/// </summary>
BoolFeedback ControllingFarEndCameraFeedback { get; }
}
}

View File

@@ -0,0 +1,24 @@
namespace PepperDash.Essentials.Devices.Common.Cameras
{
/// <summary>
/// Enum for camera control modes
/// </summary>
public enum eCameraControlMode
{
/// <summary>
/// Manual control mode, where the camera is controlled directly by the user or system
/// </summary>
Manual = 0,
/// <summary>
/// Off control mode, where the camera is turned off or disabled
/// </summary>
Off,
/// <summary>
/// Auto control mode, where the camera automatically adjusts settings based on the environment or conditions
/// </summary>
Auto
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Devices.Common.Cameras;
@@ -9,12 +11,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary>
/// Messenger for a CameraBase device
/// </summary>
public class CameraBaseMessenger : MessengerBase
public class CameraBaseMessenger<T> : MessengerBase where T : IKeyed
{
/// <summary>
/// Gets or sets the Camera
/// </summary>
public CameraBase Camera { get; set; }
public T Camera { get; set; }
/// <summary>
/// Constructor
@@ -22,10 +24,13 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <param name="key"></param>
/// <param name="camera"></param>
/// <param name="messagePath"></param>
public CameraBaseMessenger(string key, CameraBase camera, string messagePath)
: base(key, messagePath, camera)
public CameraBaseMessenger(string key, T camera, string messagePath)
: base(key, messagePath, camera as IKeyName)
{
Camera = camera ?? throw new ArgumentNullException("camera");
if (camera == null)
throw new ArgumentNullException(nameof(camera));
Camera = camera;
if (Camera is IHasCameraPresets presetsCamera)
@@ -178,19 +183,44 @@ namespace PepperDash.Essentials.AppServer.Messengers
private void SendCameraFullMessageObject(string id = null)
{
var presetList = new List<CameraPreset>();
CameraCapabilities capabilities = null;
if (Camera is IHasCameraPresets presetsCamera)
presetList = presetsCamera.Presets;
PostStatusMessage(JToken.FromObject(new
if (Camera is ICameraCapabilities cameraCapabilities)
capabilities = new CameraCapabilities
{
CanPan = cameraCapabilities.CanPan,
CanTilt = cameraCapabilities.CanTilt,
CanZoom = cameraCapabilities.CanZoom,
CanFocus = cameraCapabilities.CanFocus
};
if (Camera is CameraBase cameraBase)
capabilities = new CameraCapabilities
{
CanPan = cameraBase.CanPan,
CanTilt = cameraBase.CanTilt,
CanZoom = cameraBase.CanZoom,
CanFocus = cameraBase.CanFocus
};
var message = new CameraStateMessage
{
cameraManualSupported = Camera is IHasCameraControls,
cameraAutoSupported = Camera is IHasCameraAutoMode,
cameraOffSupported = Camera is IHasCameraOff,
cameraMode = GetCameraMode(),
hasPresets = Camera is IHasCameraPresets,
presets = presetList
}), id
CameraManualSupported = Camera is IHasCameraControls,
CameraAutoSupported = Camera is IHasCameraAutoMode,
CameraOffSupported = Camera is IHasCameraOff,
CameraMode = (eCameraControlMode)Enum.Parse(typeof(eCameraControlMode), GetCameraMode(), true),
HasPresets = Camera is IHasCameraPresets,
Presets = presetList,
Capabilities = capabilities,
IsFarEnd = Camera is IAmFarEndCamera
};
PostStatusMessage(message, id
);
}
@@ -210,4 +240,59 @@ namespace PepperDash.Essentials.AppServer.Messengers
return m;
}
}
/// <summary>
/// State message for a camera device
/// </summary>
public class CameraStateMessage : DeviceStateMessageBase
{
/// <summary>
/// Indicates whether the camera supports manual control
/// </summary>
[JsonProperty("cameraManualSupported", NullValueHandling = NullValueHandling.Ignore)]
public bool CameraManualSupported { get; set; }
/// <summary>
/// Indicates whether the camera supports auto control
/// </summary>
[JsonProperty("cameraAutoSupported", NullValueHandling = NullValueHandling.Ignore)]
public bool CameraAutoSupported { get; set; }
/// <summary>
/// Indicates whether the camera supports off control
/// </summary>
[JsonProperty("cameraOffSupported", NullValueHandling = NullValueHandling.Ignore)]
public bool CameraOffSupported { get; set; }
/// <summary>
/// Indicates the current camera control mode
/// </summary>
[JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public eCameraControlMode CameraMode { get; set; }
/// <summary>
/// Indicates whether the camera has presets
/// </summary>
[JsonProperty("hasPresets", NullValueHandling = NullValueHandling.Ignore)]
public bool HasPresets { get; set; }
/// <summary>
/// List of presets if the camera supports them
/// </summary>
[JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)]
public List<CameraPreset> Presets { get; set; }
/// <summary>
/// Indicates the capabilities of the camera
/// </summary>
[JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)]
public CameraCapabilities Capabilities { get; set; }
/// <summary>
/// Indicates whether the camera is a far end camera
/// </summary>
[JsonProperty("isFarEnd", NullValueHandling = NullValueHandling.Ignore)]
public bool IsFarEnd { get; set; }
}
}

View File

@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using PepperDash.Essentials.Devices.Common.Cameras;
using System;
using System.Collections.Generic;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Messenger for devices that implement the IHasCameras interface.
/// </summary>
[Obsolete("Use IHasCamerasWithControlsMessenger instead. This class will be removed in a future version")]
public class IHasCamerasMessenger : MessengerBase
{
/// <summary>

View File

@@ -0,0 +1,137 @@
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Devices.Common.Cameras;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Messenger for devices that implement the IHasCameras interface.
/// </summary>
public class IHasCamerasWithControlMessenger : MessengerBase
{
/// <summary>
/// Device being bridged that implements IHasCameras interface.
/// </summary>
public IHasCamerasWithControls CameraController { get; private set; }
/// <summary>
/// Messenger for devices that implement IHasCameras interface.
/// </summary>
/// <param name="key"></param>
/// <param name="cameraController"></param>
/// <param name="messagePath"></param>
/// <exception cref="ArgumentNullException"></exception>
public IHasCamerasWithControlMessenger(string key, string messagePath, IHasCamerasWithControls cameraController)
: base(key, messagePath, cameraController)
{
CameraController = cameraController ?? throw new ArgumentNullException("cameraController");
CameraController.CameraSelected += CameraController_CameraSelected;
}
private void CameraController_CameraSelected(object sender, CameraSelectedEventArgs<IHasCameraControls> e)
{
var selectedCamera = new KeyName
{
Key = e.SelectedCamera.Key,
Name = e.SelectedCamera.Name
};
PostStatusMessage(new IHasCamerasWithControlsStateMessage
{
SelectedCamera = selectedCamera
});
}
/// <summary>
/// Registers the actions for this messenger.
/// </summary>
/// <exception cref="ArgumentException"></exception>
protected override void RegisterActions()
{
base.RegisterActions();
AddAction("/fullStatus", (id, context) => SendFullStatus(id));
AddAction("/cameraListStatus", (id, content) => SendFullStatus(id));
AddAction("/selectCamera", (id, content) =>
{
var cameraKey = content?.ToObject<string>();
if (!string.IsNullOrEmpty(cameraKey))
{
CameraController.SelectCamera(cameraKey);
}
else
{
throw new ArgumentException("Content must be a string representing the camera key");
}
});
}
private void SendFullStatus(string clientId)
{
var cameraList = new List<IKeyName>();
KeyName selectedCamera = null;
foreach (var cam in CameraController.Cameras)
{
cameraList.Add(new KeyName{
Key = cam.Key,
Name = cam.Name
});
}
if (CameraController.SelectedCamera != null)
{
selectedCamera = new KeyName
{
Key = CameraController.SelectedCamera.Key,
Name = CameraController.SelectedCamera.Name
};
}
var state = new IHasCamerasWithControlsStateMessage
{
CameraList = cameraList,
SelectedCamera = selectedCamera
};
PostStatusMessage(state, clientId);
}
}
/// <summary>
/// State message for devices that implement the IHasCameras interface.
/// </summary>
public class IHasCamerasWithControlsStateMessage : DeviceStateMessageBase
{
/// <summary>
/// List of cameras available in the device.
/// </summary>
[JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)]
public List<IKeyName> CameraList { get; set; }
/// <summary>
/// The currently selected camera on the device.
/// </summary>
[JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)]
public IKeyName SelectedCamera { get; set; }
}
class KeyName : IKeyName
{
public string Key { get; set; }
public string Name { get; set; }
public KeyName()
{
Key = "";
Name = "";
}
}
}

View File

@@ -16,6 +16,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
private readonly string _propName;
private List<string> _itemKeys = new List<string>();
/// <summary>
/// Constructs a messenger for a device that implements ISelectableItems<typeparamref name="TKey"/>
/// </summary>
@@ -39,9 +41,35 @@ namespace PepperDash.Essentials.AppServer.Messengers
AddAction("/itemsStatus", (id, content) => SendFullStatus(id));
AddAction("/selectItem", (id, content) =>
{
try
{
var key = content.ToObject<TKey>();
if (key == null)
{
this.LogError("No key specified to select");
return;
}
if (itemDevice.Items.ContainsKey((TKey)Convert.ChangeType(key, typeof(TKey))))
{
itemDevice.Items[(TKey)Convert.ChangeType(key, typeof(TKey))].Select();
}
else
{
this.LogError("Key {0} not found in items", key);
}
}
catch (Exception e)
{
this.LogError("Error selecting item: {0}", e.Message);
}
});
itemDevice.ItemsUpdated += (sender, args) =>
{
SendFullStatus();
SetItems();
};
itemDevice.CurrentItemChanged += (sender, args) =>
@@ -49,23 +77,47 @@ namespace PepperDash.Essentials.AppServer.Messengers
SendFullStatus();
};
foreach (var input in itemDevice.Items)
SetItems();
}
/// <summary>
/// Sets the items and registers their update events
/// </summary>
private void SetItems()
{
if (_itemKeys != null && _itemKeys.Count > 0)
{
var key = input.Key;
var localItem = input.Value;
/// Clear out any existing item actions
foreach (var item in _itemKeys)
{
RemoveAction($"/{item}");
}
_itemKeys.Clear();
}
foreach (var item in itemDevice.Items)
{
var key = item.Key;
var localItem = item.Value;
AddAction($"/{key}", (id, content) =>
{
localItem.Select();
});
localItem.ItemUpdated += (sender, args) =>
{
SendFullStatus();
};
_itemKeys.Add(key.ToString());
localItem.ItemUpdated -= LocalItem_ItemUpdated;
localItem.ItemUpdated += LocalItem_ItemUpdated;
}
}
private void LocalItem_ItemUpdated(object sender, EventArgs e)
{
SendFullStatus();
}
private void SendFullStatus(string id = null)
{
try

View File

@@ -3,10 +3,15 @@ using System;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a ClientSpecificUpdateRequest
/// Send an update request for a specific client
/// </summary>
[Obsolete]
public class ClientSpecificUpdateRequest
{
/// <summary>
/// Initialize an instance of the <see cref="ClientSpecificUpdateRequest"/> class.
/// </summary>
/// <param name="action"></param>
public ClientSpecificUpdateRequest(Action<string> action)
{
ResponseMethod = action;

View File

@@ -7,8 +7,9 @@ namespace PepperDash.Essentials
/// </summary>
public interface IDelayedConfiguration
{
/// <summary>
/// Event triggered when the configuration is ready. Used when Mobile Control is interacting with a SIMPL program.
/// </summary>
event EventHandler<EventArgs> ConfigurationIsReady;
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.WebSocketServer;
using Serilog.Events;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a MessageToClients
/// </summary>
public class MessageToClients : IQueueMessage
{
private readonly MobileControlWebsocketServer _server;
private readonly object msgToSend;
/// <summary>
/// Message to send to Direct Server Clients
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="server">WebSocket server instance</param>
public MessageToClients(object msg, MobileControlWebsocketServer server)
{
_server = server;
msgToSend = msg;
}
/// <summary>
/// Message to send to Direct Server Clients
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="server">WebSocket server instance</param>
public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server)
{
_server = server;
msgToSend = msg;
}
#region Implementation of IQueueMessage
/// <summary>
/// Dispatch method
/// </summary>
public void Dispatch()
{
try
{
if (_server == null)
{
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null");
return;
}
var message = JsonConvert.SerializeObject(msgToSend, Formatting.None,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } });
var clientSpecificMessage = msgToSend as MobileControlMessage;
if (clientSpecificMessage.ClientId != null)
{
var clientId = clientSpecificMessage.ClientId;
_server.LogVerbose("Message TX To client {clientId}: {message}", clientId, message);
_server.SendMessageToClient(clientId, message);
return;
}
_server.SendMessageToAllClients(message);
_server.LogVerbose("Message TX To all clients: {message}", message);
}
catch (ThreadAbortException)
{
//Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Caught an exception in the Transmit Processor");
}
}
#endregion
}
}

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json.Linq;
using System;
using Newtonsoft.Json.Linq;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using System;
namespace PepperDash.Essentials
{
@@ -10,12 +10,20 @@ namespace PepperDash.Essentials
public class MobileControlAction : IMobileControlAction
{
/// <summary>
/// Gets or sets the Messenger
/// Gets the Messenger
/// </summary>
public IMobileControlMessenger Messenger { get; private set; }
/// <summary>
/// Action to execute when this path is matched
/// </summary>
public Action<string, string, JToken> Action { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="MobileControlAction"/> class
/// </summary>
/// <param name="messenger">Messenger associated with this action</param>
/// <param name="handler">Action to take when this path is matched</param>
public MobileControlAction(IMobileControlMessenger messenger, Action<string, string, JToken> handler)
{
Messenger = messenger;

View File

@@ -1,28 +1,25 @@
using PepperDash.Core;
using PepperDash.Core.Logging;
using System;
using System.Collections.Generic;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a MobileControlDeviceFactory
/// Factory to create a Mobile Control System Controller
/// </summary>
public class MobileControlDeviceFactory : EssentialsDeviceFactory<MobileControlSystemController>
{
/// <summary>
/// Create the factory for a Mobile Control System Controller
/// </summary>
public MobileControlDeviceFactory()
{
TypeNames = new List<string> { "appserver", "mobilecontrol", "webserver" };
}
/// <summary>
/// BuildDevice method
/// </summary>
/// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{

View File

@@ -6,29 +6,35 @@ using PepperDash.Essentials.Core.Config;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a MobileControlEssentialsConfig
/// Configuration class for sending data to Mobile Control Edge or a client using the Direct Server
/// </summary>
public class MobileControlEssentialsConfig : EssentialsConfig
{
/// <summary>
/// Current versions for the system
/// </summary>
[JsonProperty("runtimeInfo")]
public MobileControlRuntimeInfo RuntimeInfo { get; set; }
/// <summary>
/// Create Configuration for Mobile Control. Used as part of the data sent to a client
/// </summary>
/// <param name="config">The base configuration</param>
public MobileControlEssentialsConfig(EssentialsConfig config)
: base()
{
// TODO: Consider using Reflection to iterate properties
this.Devices = config.Devices;
this.Info = config.Info;
this.JoinMaps = config.JoinMaps;
this.Rooms = config.Rooms;
this.SourceLists = config.SourceLists;
this.DestinationLists = config.DestinationLists;
this.SystemUrl = config.SystemUrl;
this.TemplateUrl = config.TemplateUrl;
this.TieLines = config.TieLines;
Devices = config.Devices;
Info = config.Info;
JoinMaps = config.JoinMaps;
Rooms = config.Rooms;
SourceLists = config.SourceLists;
DestinationLists = config.DestinationLists;
SystemUrl = config.SystemUrl;
TemplateUrl = config.TemplateUrl;
TieLines = config.TieLines;
if (this.Info == null)
this.Info = new InfoConfig();
if (Info == null)
Info = new InfoConfig();
RuntimeInfo = new MobileControlRuntimeInfo();
}
@@ -46,15 +52,21 @@ namespace PepperDash.Essentials
[JsonProperty("pluginVersion")]
public string PluginVersion { get; set; }
/// <summary>
/// Essentials Version
/// </summary>
[JsonProperty("essentialsVersion")]
public string EssentialsVersion { get; set; }
/// <summary>
/// PepperDash Core Version
/// </summary>
[JsonProperty("pepperDashCoreVersion")]
public string PepperDashCoreVersion { get; set; }
/// <summary>
/// Gets or sets the EssentialsPlugins
/// List of Plugins loaded on this system
/// </summary>
[JsonProperty("essentialsPlugins")]
public List<LoadedAssembly> EssentialsPlugins { get; set; }

View File

@@ -7,10 +7,13 @@ using PepperDash.Essentials.Core;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a MobileControlFactory
/// Factory class for the Mobile Control App Controller
/// </summary>
public class MobileControlFactory
{
/// <summary>
/// Create an instance of the <see cref="MobileControlFactory"/> class.
/// </summary>
public MobileControlFactory()
{
var assembly = Assembly.GetExecutingAssembly();

View File

@@ -91,10 +91,16 @@ namespace PepperDash.Essentials
/// </summary>
public MobileControlApiService ApiService { get; private set; }
/// <summary>
/// Get Room Bridges associated with this controller
/// </summary>
public List<MobileControlBridgeBase> RoomBridges => _roomBridges;
private readonly MobileControlWebsocketServer _directServer;
/// <summary>
/// Get the Direct Server instance associated with this controller
/// </summary>
public MobileControlWebsocketServer DirectServer => _directServer;
private readonly CCriticalSection _wsCriticalSection = new CCriticalSection();
@@ -104,10 +110,16 @@ namespace PepperDash.Essentials
/// </summary>
public string SystemUrl; //set only from SIMPL Bridge!
/// <summary>
/// True if the Mobile Control Edge Server Websocket is connected
/// </summary>
public bool Connected => _wsClient2 != null && _wsClient2.IsAlive;
private IEssentialsRoomCombiner _roomCombiner;
/// <summary>
/// Gets the SystemUuid from configuration or SIMPL Bridge
/// </summary>
public string SystemUuid
{
get
@@ -169,6 +181,9 @@ namespace PepperDash.Essentials
private DateTime _lastAckMessage;
/// <summary>
/// Gets the LastAckMessage timestamp
/// </summary>
public DateTime LastAckMessage => _lastAckMessage;
private CTimer _pingTimer;
@@ -177,11 +192,11 @@ namespace PepperDash.Essentials
private LogLevel _wsLogLevel = LogLevel.Error;
/// <summary>
///
/// Initializes a new instance of the <see cref="MobileControlSystemController"/> class.
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="config"></param>
/// <param name="key">The unique key for this controller.</param>
/// <param name="name">The name of the controller.</param>
/// <param name="config">The configuration settings for the controller.</param>
public MobileControlSystemController(string key, string name, MobileControlConfig config)
: base(key, name)
{
@@ -405,14 +420,15 @@ namespace PepperDash.Essentials
messengerAdded = true;
}
if (device is CameraBase cameraDevice)
// Default to IHasCameraControls if CameraBase and IHasCameraControls
if (device is CameraBase cameraDevice && !(device is IHasCameraControls))
{
this.LogVerbose(
"Adding CameraBaseMessenger for {deviceKey}",
device.Key
);
var cameraMessenger = new CameraBaseMessenger(
var cameraMessenger = new CameraBaseMessenger<CameraBase>(
$"{device.Key}-cameraBase-{Key}",
cameraDevice,
$"/device/{device.Key}"
@@ -423,6 +439,21 @@ namespace PepperDash.Essentials
messengerAdded = true;
}
if (device is IHasCameraControls cameraControlDev)
{
this.LogVerbose(
"Adding IHasCamerasWithControlMessenger for {deviceKey}",
device.Key
);
var cameraControlMessenger = new CameraBaseMessenger<IHasCameraControls>(
$"{device.Key}-hasCamerasWithControls-{Key}",
cameraControlDev,
$"/device/{device.Key}"
);
AddDefaultDeviceMessenger(cameraControlMessenger);
messengerAdded = true;
}
if (device is BlueJeansPc)
{
this.LogVerbose(
@@ -975,6 +1006,19 @@ namespace PepperDash.Essentials
messengerAdded = true;
}
if (device is IHasCamerasWithControls cameras2)
{
this.LogVerbose("Adding IHasCamerasWithControlsMessenger for {deviceKey}", device.Key
);
var messenger = new IHasCamerasWithControlMessenger(
$"{device.Key}-cameras-{Key}",
$"/device/{device.Key}",
cameras2
);
AddDefaultDeviceMessenger(messenger);
messengerAdded = true;
}
this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key);
if (device is EssentialsDevice)
@@ -1163,6 +1207,9 @@ namespace PepperDash.Essentials
/// </summary>
public string Host { get; private set; }
/// <summary>
/// Gets the configured Client App URL
/// </summary>
public string ClientAppUrl => Config.ClientAppUrl;
private void OnRoomCombinationScenarioChanged(
@@ -1174,7 +1221,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// CheckForDeviceMessenger method
/// Checks if a device messenger exists for the given key.
/// </summary>
public bool CheckForDeviceMessenger(string key)
{
@@ -1182,13 +1229,13 @@ namespace PepperDash.Essentials
}
/// <summary>
/// AddDeviceMessenger method
/// Add the provided messenger to the messengers collection
/// </summary>
public void AddDeviceMessenger(IMobileControlMessenger messenger)
{
if (_messengers.ContainsKey(messenger.Key))
{
this.LogWarning("Messenger with key {messengerKey) already added", messenger.Key);
this.LogWarning("Messenger with key {messengerKey} already added", messenger.Key);
return;
}
@@ -1262,9 +1309,6 @@ namespace PepperDash.Essentials
messenger.RegisterWithAppServer(this);
}
/// <summary>
/// Initialize method
/// </summary>
/// <inheritdoc />
public override void Initialize()
{
@@ -1309,7 +1353,7 @@ namespace PepperDash.Essentials
#region IMobileControl Members
/// <summary>
/// GetAppServer method
/// Gets the App Server instance
/// </summary>
public static IMobileControl GetAppServer()
{
@@ -1327,16 +1371,10 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Generates the url and creates the websocket client
/// </summary>
private bool CreateWebsocket()
{
if (_wsClient2 != null)
{
_wsClient2.Close();
_wsClient2 = null;
}
_wsClient2?.Close();
_wsClient2 = null;
if (string.IsNullOrEmpty(SystemUuid))
{
@@ -1353,33 +1391,13 @@ namespace PepperDash.Essentials
{
Log =
{
Output = (data, message) =>
{
switch (data.Level)
{
case LogLevel.Trace:
this.LogVerbose(data.Message);
break;
case LogLevel.Debug:
this.LogDebug(data.Message);
break;
case LogLevel.Info:
this.LogInformation(data.Message);
break;
case LogLevel.Warn:
this.LogWarning(data.Message);
break;
case LogLevel.Error:
this.LogError(data.Message);
break;
case LogLevel.Fatal:
this.LogFatal(data.Message);
break;
}
}
Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this)
}
};
// setting to trace to let level be controlled by appdebug
_wsClient2.Log.Level = LogLevel.Trace;
_wsClient2.SslConfiguration.EnabledSslProtocols =
System.Security.Authentication.SslProtocols.Tls11
| System.Security.Authentication.SslProtocols.Tls12;
@@ -1393,7 +1411,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// LinkSystemMonitorToAppServer method
/// Link the System Monitor to this App server
/// </summary>
public void LinkSystemMonitorToAppServer()
{
@@ -1420,14 +1438,6 @@ namespace PepperDash.Essentials
private void SetWebsocketDebugLevel(string cmdparameters)
{
// if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4)
// {
// this.LogInformation(
// "Setting websocket log level not currently allowed on 4 series."
// );
// return; // Web socket log level not currently allowed in series4
// }
if (string.IsNullOrEmpty(cmdparameters))
{
this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel);
@@ -1465,10 +1475,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Sends message to server to indicate the system is shutting down
/// </summary>
/// <param name="programEventType"></param>
private void CrestronEnvironment_ProgramStatusEventHandler(
eProgramStatusEventType programEventType
)
@@ -1501,6 +1507,9 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Get action paths for the current actions
/// </summary>
public List<(string, string)> GetActionDictionaryPaths()
{
var paths = new List<(string, string)>();
@@ -1573,24 +1582,24 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Get the room bridge with the provided key
/// </summary>
/// <param name="key">The key of the room bridge</param>
public MobileControlBridgeBase GetRoomBridge(string key)
{
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
}
/// <summary>
/// GetRoomMessenger method
/// Get the room messenger with the provided key
/// </summary>
/// <param name="key">The Key of the rooom messenger</param>
public IMobileControlRoomMessenger GetRoomMessenger(string key)
{
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bridge_ConfigurationIsReady(object sender, EventArgs e)
{
this.LogDebug("Bridge ready. Registering");
@@ -1611,10 +1620,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
/// <param name="o"></param>
private void ReconnectToServerTimerCallback(object o)
{
this.LogDebug("Attempting to reconnect to server...");
@@ -1622,9 +1627,6 @@ namespace PepperDash.Essentials
ConnectWebsocketClient();
}
/// <summary>
/// Verifies system connection with servers
/// </summary>
private void AuthorizeSystem(string code)
{
if (
@@ -1669,9 +1671,6 @@ namespace PepperDash.Essentials
});
}
/// <summary>
/// Dumps info in response to console command.
/// </summary>
private void ShowInfo()
{
var url = Config != null ? Host : "No config";
@@ -1737,38 +1736,37 @@ namespace PepperDash.Essentials
"\r\n UI Client Info:\r\n" +
" Tokens Defined: {0}\r\n" +
" Clients Connected: {1}\r\n",
_directServer.UiClients.Count,
_directServer.UiClientContexts.Count,
_directServer.ConnectedUiClientsCount
);
var clientNo = 1;
foreach (var clientContext in _directServer.UiClients)
foreach (var clientContext in _directServer.UiClientContexts)
{
var isAlive = false;
var duration = "Not Connected";
if (clientContext.Value.Client != null)
{
isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive;
duration = clientContext.Value.Client.ConnectedDuration.ToString();
}
var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token);
CrestronConsole.ConsoleCommandResponse(
"\r\nClient {0}:\r\n" +
"Room Key: {1}\r\n" +
"Touchpanel Key: {6}\r\n" +
"Token: {2}\r\n" +
"Client URL: {3}\r\n" +
"Connected: {4}\r\n" +
"Duration: {5}\r\n",
clientNo,
clientContext.Value.Token.RoomKey,
clientContext.Key,
string.Format("{0}{1}", _directServer.UserAppUrlPrefix, clientContext.Key),
isAlive,
duration,
clientContext.Value.Token.TouchpanelKey
$"\r\nClient {clientNo}:\r\n" +
$" Room Key: {clientContext.Value.Token.RoomKey}\r\n" +
$" Touchpanel Key: {clientContext.Value.Token.TouchpanelKey}\r\n" +
$" Token: {clientContext.Key}\r\n" +
$" Client URL: {_directServer.UserAppUrlPrefix}{clientContext.Key}\r\n" +
$" Clients:\r\n"
);
if (!clients.Any())
{
CrestronConsole.ConsoleCommandResponse(" No clients connected");
}
foreach (var client in clients)
{
CrestronConsole.ConsoleCommandResponse(
$" ID: {client.Id}\r\n" +
$" Connected: {client.Context.WebSocket.IsAlive}\r\n" +
$" Duration: {(client.Context.WebSocket.IsAlive ? client.ConnectedDuration.TotalSeconds.ToString() : "Not Connected")}\r\n"
);
}
clientNo++;
}
}
@@ -1782,7 +1780,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// RegisterSystemToServer method
/// Register this system to the Mobile Control Edge Server
/// </summary>
public void RegisterSystemToServer()
{
@@ -1806,9 +1804,6 @@ namespace PepperDash.Essentials
ConnectWebsocketClient();
}
/// <summary>
/// Connects the Websocket Client
/// </summary>
private void ConnectWebsocketClient()
{
try
@@ -1849,9 +1844,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Attempts to connect the websocket
/// </summary>
private void TryConnect()
{
try
@@ -1881,9 +1873,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer
/// </summary>
private void HandleConnectFailure()
{
_wsClient2 = null;
@@ -1915,11 +1904,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer();
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleOpen(object sender, EventArgs e)
{
StopServerReconnectTimer();
@@ -1928,11 +1912,6 @@ namespace PepperDash.Essentials
SendMessageObject(new MobileControlMessage { Type = "hello" });
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleMessage(object sender, MessageEventArgs e)
{
if (e.IsPing)
@@ -1949,11 +1928,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleError(object sender, ErrorEventArgs e)
{
this.LogError("Websocket error {0}", e.Message);
@@ -1962,11 +1936,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer();
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleClose(object sender, CloseEventArgs e)
{
this.LogDebug(
@@ -1987,9 +1956,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer();
}
/// <summary>
/// After a "hello" from the server, sends config and stuff
/// </summary>
private void SendInitialMessage()
{
this.LogInformation("Sending initial join message");
@@ -2016,7 +1982,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// GetConfigWithPluginVersion method
/// Get the Essentials configuration with version data
/// </summary>
public MobileControlEssentialsConfig GetConfigWithPluginVersion()
{
@@ -2051,8 +2017,13 @@ namespace PepperDash.Essentials
}
/// <summary>
/// SetClientUrl method
/// Set the Client URL for a given room
/// </summary>
/// <param name="path">new App URL</param>
/// <param name="roomKey">room key. Default is null</param>
/// <remarks>
/// If roomKey is null, the URL will be set for the entire system.
/// </remarks>
public void SetClientUrl(string path, string roomKey = null)
{
var message = new MobileControlMessage
@@ -2068,9 +2039,6 @@ namespace PepperDash.Essentials
/// Sends any object type to server
/// </summary>
/// <param name="o"></param>
/// <summary>
/// SendMessageObject method
/// </summary>
public void SendMessageObject(IMobileControlMessage o)
{
@@ -2094,8 +2062,9 @@ namespace PepperDash.Essentials
/// <summary>
/// SendMessageObjectToDirectClient method
/// Send a message to a client using the Direct Server
/// </summary>
/// <param name="o">object to send</param>
public void SendMessageObjectToDirectClient(object o)
{
if (
@@ -2108,10 +2077,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Disconnects the Websocket Client and stops the heartbeat timer
/// </summary>
private void CleanUpWebsocketClient()
{
if (_wsClient2 == null)
@@ -2169,9 +2134,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
private void StartServerReconnectTimer()
{
StopServerReconnectTimer();
@@ -2182,9 +2144,6 @@ namespace PepperDash.Essentials
this.LogDebug("Reconnect Timer Started.");
}
/// <summary>
/// Does what it says
/// </summary>
private void StopServerReconnectTimer()
{
if (_serverReconnectTimer == null)
@@ -2195,10 +2154,6 @@ namespace PepperDash.Essentials
_serverReconnectTimer = null;
}
/// <summary>
/// Resets reconnect timer and updates usercode
/// </summary>
/// <param name="content"></param>
private void HandleHeartBeat(JToken content)
{
SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" });
@@ -2219,6 +2174,7 @@ namespace PepperDash.Essentials
{
var clientId = content["clientId"].Value<string>();
var roomKey = content["roomKey"].Value<string>();
var touchpanelKey = content.SelectToken("touchpanelKey");
if (_roomCombiner == null)
{
@@ -2230,6 +2186,10 @@ namespace PepperDash.Essentials
};
SendMessageObject(message);
SendDeviceInterfaces(clientId);
SendTouchpanelKey(clientId, touchpanelKey);
return;
}
@@ -2241,7 +2201,12 @@ namespace PepperDash.Essentials
ClientId = clientId,
Content = roomKey
};
SendMessageObject(message);
SendDeviceInterfaces(clientId);
SendTouchpanelKey(clientId, touchpanelKey);
return;
}
@@ -2259,6 +2224,10 @@ namespace PepperDash.Essentials
};
SendMessageObject(message);
SendDeviceInterfaces(clientId);
SendTouchpanelKey(clientId, touchpanelKey);
return;
}
@@ -2272,6 +2241,54 @@ namespace PepperDash.Essentials
};
SendMessageObject(newMessage);
SendDeviceInterfaces(clientId);
SendTouchpanelKey(clientId, touchpanelKey);
}
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
{
if (touchpanelKeyToken == null)
{
this.LogWarning("Touchpanel key not found for client {clientId}", clientId);
return;
}
SendMessageObject(new MobileControlMessage
{
Type = "/system/touchpanelKey",
ClientId = clientId,
Content = touchpanelKeyToken.Value<string>()
});
}
private void SendDeviceInterfaces(string clientId)
{
this.LogDebug("Sending Device interfaces");
var devices = DeviceManager.GetDevices();
Dictionary<string, DeviceInterfaceInfo> deviceInterfaces = new Dictionary<string, DeviceInterfaceInfo>();
foreach (var device in devices)
{
var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List<string>();
deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo
{
Key = device.Key,
Name = (device as IKeyName)?.Name ?? "",
Interfaces = interfaces
});
}
var message = new MobileControlMessage
{
Type = "/system/deviceInterfaces",
ClientId = clientId,
Content = JToken.FromObject(new { deviceInterfaces })
};
SendMessageObject(message);
}
private void HandleUserCode(JToken content, Action<string, string> action = null)
@@ -2308,16 +2325,13 @@ namespace PepperDash.Essentials
}
/// <summary>
/// HandleClientMessage method
/// Enqueue an incoming message for processing
/// </summary>
public void HandleClientMessage(string message)
{
_receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx));
}
/// <summary>
///
/// </summary>
private void ParseStreamRx(string messageText)
{
if (string.IsNullOrEmpty(messageText))
@@ -2385,10 +2399,33 @@ namespace PepperDash.Essentials
foreach (var handler in handlers)
{
Task.Run(
() =>
handler.Action(message.Type, message.ClientId, message.Content)
);
Task.Run(async () =>
{
try
{
handler.Action(message.Type, message.ClientId, message.Content);
}
catch (Exception ex)
{
this.LogError(
"Exception in handler for message type {type}, ClientId {clientId}",
message.Type,
message.ClientId
);
this.LogDebug(ex, "Stack Trace: ");
}
}).ContinueWith(task =>
{
if (task.IsFaulted && task.Exception != null)
{
this.LogError(
"Unhandled exception in Task for message type {type}, ClientId {clientId}",
message.Type,
message.ClientId
);
this.LogDebug(task.Exception.GetBaseException(), "Stack Trace: ");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
break;
@@ -2404,10 +2441,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
/// <param name="s"></param>
private void TestHttpRequest(string s)
{
{

View File

@@ -1,23 +1,35 @@
using PepperDash.Core;
using System;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using System;
namespace PepperDash.Essentials.RoomBridges
{
/// <summary>
///
/// Base class for a Mobile Control Bridge that's used to control a room
/// </summary>
public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger
{
/// <summary>
/// Triggered when the user Code changes
/// </summary>
public event EventHandler<EventArgs> UserCodeChanged;
/// <summary>
/// Triggered when a user should be prompted for the new code
/// </summary>
public event EventHandler<EventArgs> UserPromptedForCode;
/// <summary>
/// Triggered when a client joins to control this room
/// </summary>
public event EventHandler<EventArgs> ClientJoined;
/// <summary>
/// Triggered when the App URL for this room changes
/// </summary>
public event EventHandler<EventArgs> AppUrlChanged;
/// <summary>
@@ -49,15 +61,32 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary>
public string McServerUrl { get; private set; }
/// <summary>
/// Room Name
/// </summary>
public abstract string RoomName { get; }
/// <summary>
/// Room key
/// </summary>
public abstract string RoomKey { get; }
/// <summary>
/// Create an instance of the <see cref="MobileControlBridgeBase"/> class
/// </summary>
/// <param name="key">The unique key for this bridge</param>
/// <param name="messagePath">The message path for this bridge</param>
protected MobileControlBridgeBase(string key, string messagePath)
: base(key, messagePath)
{
}
/// <summary>
/// Create an instance of the <see cref="MobileControlBridgeBase"/> class
/// </summary>
/// <param name="key">The unique key for this bridge</param>
/// <param name="messagePath">The message path for this bridge</param>
/// <param name="device">The device associated with this bridge</param>
protected MobileControlBridgeBase(string key, string messagePath, IKeyName device)
: base(key, messagePath, device)
{
@@ -110,6 +139,10 @@ namespace PepperDash.Essentials.RoomBridges
SetUserCode(code);
}
/// <summary>
/// Update the App Url with the provided URL
/// </summary>
/// <param name="url">The new App URL</param>
public virtual void UpdateAppUrl(string url)
{
AppUrl = url;
@@ -137,16 +170,25 @@ namespace PepperDash.Essentials.RoomBridges
OnUserCodeChanged();
}
/// <summary>
/// Trigger the UserCodeChanged event
/// </summary>
protected void OnUserCodeChanged()
{
UserCodeChanged?.Invoke(this, new EventArgs());
}
/// <summary>
/// Trigger the UserPromptedForCode event
/// </summary>
protected void OnUserPromptedForCode()
{
UserPromptedForCode?.Invoke(this, new EventArgs());
}
/// <summary>
/// Trigger the ClientJoined event
/// </summary>
protected void OnClientJoined()
{
ClientJoined?.Invoke(this, new EventArgs());

View File

@@ -41,24 +41,37 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary>
public string DefaultRoomKey { get; private set; }
/// <summary>
///
/// Gets the name of the room
/// </summary>
public override string RoomName
{
get { return Room.Name; }
}
/// <summary>
/// Gets the key of the room
/// </summary>
public override string RoomKey
{
get { return Room.Key; }
}
/// <summary>
/// Initializes a new instance of the <see cref="MobileControlEssentialsRoomBridge"/> class with the specified room
/// </summary>
/// <param name="room">The essentials room to bridge</param>
public MobileControlEssentialsRoomBridge(IEssentialsRoom room) :
this($"mobileControlBridge-{room.Key}", room.Key, room)
{
Room = room;
}
/// <summary>
/// Initializes a new instance of the <see cref="MobileControlEssentialsRoomBridge"/> class with the specified parameters
/// </summary>
/// <param name="key">The unique key for this bridge</param>
/// <param name="roomKey">The key of the room to bridge</param>
/// <param name="room">The essentials room to bridge</param>
public MobileControlEssentialsRoomBridge(string key, string roomKey, IEssentialsRoom room) : base(key, $"/room/{room.Key}", room as Device)
{
DefaultRoomKey = roomKey;
@@ -66,7 +79,9 @@ namespace PepperDash.Essentials.RoomBridges
AddPreActivationAction(GetRoom);
}
/// <summary>
/// Registers all message handling actions with the AppServer for this room bridge
/// </summary>
protected override void RegisterActions()
{
// we add actions to the messaging system with a path, and a related action. Custom action
@@ -284,6 +299,9 @@ namespace PepperDash.Essentials.RoomBridges
Room = tempRoom;
}
/// <summary>
/// Handles user code changes and generates QR code URL
/// </summary>
protected override void UserCodeChange()
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Server user code changed: {userCode}", this, UserCode);
@@ -807,18 +825,33 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("configuration", NullValueHandling = NullValueHandling.Ignore)]
public RoomConfiguration Configuration { get; set; }
/// <summary>
/// Gets or sets the activity mode of the room
/// </summary>
[JsonProperty("activityMode", NullValueHandling = NullValueHandling.Ignore)]
public int? ActivityMode { get; set; }
/// <summary>
/// Gets or sets whether advanced sharing is active
/// </summary>
[JsonProperty("advancedSharingActive", NullValueHandling = NullValueHandling.Ignore)]
public bool? AdvancedSharingActive { get; set; }
/// <summary>
/// Gets or sets whether the room is powered on
/// </summary>
[JsonProperty("isOn", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsOn { get; set; }
/// <summary>
/// Gets or sets whether the room is warming up
/// </summary>
[JsonProperty("isWarmingUp", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsWarmingUp { get; set; }
/// <summary>
/// Gets or sets whether the room is cooling down
/// </summary>
[JsonProperty("isCoolingDown", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsCoolingDown { get; set; }
@@ -834,9 +867,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("share", NullValueHandling = NullValueHandling.Ignore)]
public ShareState Share { get; set; }
/// <summary>
/// Gets or sets the volume controls collection
/// </summary>
[JsonProperty("volumes", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, Volume> Volumes { get; set; }
/// <summary>
/// Gets or sets whether the room is in a call
/// </summary>
[JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsInCall { get; set; }
}
@@ -853,9 +892,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("currentShareText", NullValueHandling = NullValueHandling.Ignore)]
public string CurrentShareText { get; set; }
/// <summary>
/// Gets or sets whether sharing is enabled
/// </summary>
[JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)]
public bool? Enabled { get; set; }
/// <summary>
/// Gets or sets whether content is currently being shared
/// </summary>
[JsonProperty("isSharing", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsSharing { get; set; }
}
@@ -865,24 +910,45 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary>
public class RoomConfiguration
{
/// <summary>
/// Gets or sets whether the room has video conferencing capabilities
/// </summary>
[JsonProperty("hasVideoConferencing", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasVideoConferencing { get; set; }
/// <summary>
/// Gets or sets whether the video codec is a Zoom Room
/// </summary>
[JsonProperty("videoCodecIsZoomRoom", NullValueHandling = NullValueHandling.Ignore)]
public bool? VideoCodecIsZoomRoom { get; set; }
/// <summary>
/// Gets or sets whether the room has audio conferencing capabilities
/// </summary>
[JsonProperty("hasAudioConferencing", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasAudioConferencing { get; set; }
/// <summary>
/// Gets or sets whether the room has environmental controls (lighting, shades, etc.)
/// </summary>
[JsonProperty("hasEnvironmentalControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasEnvironmentalControls { get; set; }
/// <summary>
/// Gets or sets whether the room has camera controls
/// </summary>
[JsonProperty("hasCameraControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasCameraControls { get; set; }
/// <summary>
/// Gets or sets whether the room has set-top box controls
/// </summary>
[JsonProperty("hasSetTopBoxControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasSetTopBoxControls { get; set; }
/// <summary>
/// Gets or sets whether the room has routing controls
/// </summary>
[JsonProperty("hasRoutingControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasRoutingControls { get; set; }
@@ -949,6 +1015,9 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("defaultDisplayKey", NullValueHandling = NullValueHandling.Ignore)]
public string DefaultDisplayKey { get; set; }
/// <summary>
/// Gets or sets the destinations dictionary keyed by destination type
/// </summary>
[JsonProperty("destinations", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<eSourceListItemDestinationTypes, string> Destinations { get; set; }
@@ -959,9 +1028,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("environmentalDevices", NullValueHandling = NullValueHandling.Ignore)]
public List<EnvironmentalDeviceConfiguration> EnvironmentalDevices { get; set; }
/// <summary>
/// Gets or sets the source list for the room
/// </summary>
[JsonProperty("sourceList", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, SourceListItem> SourceList { get; set; }
/// <summary>
/// Gets or sets the destination list for the room
/// </summary>
[JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, DestinationListItem> DestinationList { get; set; }
@@ -972,6 +1047,9 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)]
public AudioControlPointListItem AudioControlPointList { get; set; }
/// <summary>
/// Gets or sets the camera list for the room
/// </summary>
[JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, CameraListItem> CameraList { get; set; }
@@ -1004,9 +1082,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("uiBehavior", NullValueHandling = NullValueHandling.Ignore)]
public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; }
/// <summary>
/// Gets or sets whether the room supports advanced sharing features
/// </summary>
[JsonProperty("supportsAdvancedSharing", NullValueHandling = NullValueHandling.Ignore)]
public bool? SupportsAdvancedSharing { get; set; }
/// <summary>
/// Gets or sets whether the user can change the share mode
/// </summary>
[JsonProperty("userCanChangeShareMode", NullValueHandling = NullValueHandling.Ignore)]
public bool? UserCanChangeShareMode { get; set; }
@@ -1017,6 +1101,9 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("roomCombinerKey", NullValueHandling = NullValueHandling.Ignore)]
public string RoomCombinerKey { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="RoomConfiguration"/> class
/// </summary>
public RoomConfiguration()
{
Destinations = new Dictionary<eSourceListItemDestinationTypes, string>();
@@ -1046,6 +1133,11 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("deviceType", NullValueHandling = NullValueHandling.Ignore)]
public eEnvironmentalDeviceTypes DeviceType { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="EnvironmentalDeviceConfiguration"/> class
/// </summary>
/// <param name="key">The device key</param>
/// <param name="type">The environmental device type</param>
public EnvironmentalDeviceConfiguration(string key, eEnvironmentalDeviceTypes type)
{
DeviceKey = key;
@@ -1054,14 +1146,29 @@ namespace PepperDash.Essentials.RoomBridges
}
/// <summary>
/// Enumeration of eEnvironmentalDeviceTypes values
/// Enumeration of environmental device types
/// </summary>
public enum eEnvironmentalDeviceTypes
{
/// <summary>
/// No environmental device type specified
/// </summary>
None,
/// <summary>
/// Lighting device type
/// </summary>
Lighting,
/// <summary>
/// Shade device type
/// </summary>
Shade,
/// <summary>
/// Shade controller device type
/// </summary>
ShadeController,
/// <summary>
/// Relay device type
/// </summary>
Relay,
}

View File

@@ -1,18 +1,22 @@
using PepperDash.Core;
using System;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using PepperDash.Core;
namespace PepperDash.Essentials.Services
{
/// <summary>
/// Represents a MobileControlApiService
/// Service for interacting with a Mobile Control Edge server instance
/// </summary>
public class MobileControlApiService
{
private readonly HttpClient _client;
/// <summary>
/// Create an instance of the <see cref="MobileControlApiService"/> class.
/// </summary>
/// <param name="apiUrl">Mobile Control Edge API URL</param>
public MobileControlApiService(string apiUrl)
{
var handler = new HttpClientHandler
@@ -24,6 +28,13 @@ namespace PepperDash.Essentials.Services
_client = new HttpClient(handler);
}
/// <summary>
/// Send authorization request to Mobile Control Edge Server
/// </summary>
/// <param name="apiUrl">Mobile Control Edge API URL</param>
/// <param name="grantCode">Grant code for authorization</param>
/// <param name="systemUuid">System UUID for authorization</param>
/// <returns>Authorization response</returns>
public async Task<AuthorizationResponse> SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid)
{
try

View File

@@ -7,8 +7,15 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary>
public interface ITheme : IKeyed
{
/// <summary>
/// Current theme
/// </summary>
string Theme { get; }
/// <summary>
/// Set the theme with the given value
/// </summary>
/// <param name="theme">The theme to set</param>
void UpdateTheme(string theme);
}
}

View File

@@ -8,12 +8,24 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary>
public interface ITswAppControl : IKeyed
{
/// <summary>
/// Updates when the Zoom Room Control Application opens or closes
/// </summary>
BoolFeedback AppOpenFeedback { get; }
/// <summary>
/// Hide the Zoom App and show the User Control Application
/// </summary>
void HideOpenApp();
/// <summary>
/// Close the Zoom App and show the User Control Application
/// </summary>
void CloseOpenApp();
/// <summary>
/// Open the Zoom App
/// </summary>
void OpenApp();
}
@@ -22,10 +34,19 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary>
public interface ITswZoomControl : IKeyed
{
/// <summary>
/// Updates when Zoom has an incoming call
/// </summary>
BoolFeedback ZoomIncomingCallFeedback { get; }
/// <summary>
/// Updates when Zoom is in a call
/// </summary>
BoolFeedback ZoomInCallFeedback { get; }
/// <summary>
/// End a Zoom Call
/// </summary>
void EndZoomCall();
}
}

View File

@@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel
{
/// <summary>
/// Represents a ITswAppControlMessenger
/// Messenger for controlling the Zoom App on a TSW Panel that supports the Zoom Room Control Application
/// </summary>
public class ITswAppControlMessenger : MessengerBase
{
private readonly ITswAppControl _appControl;
/// <summary>
/// Create an instance of the <see cref="ITswAppControlMessenger"/> class.
/// </summary>
/// <param name="key">The key for this messenger</param>
/// <param name="messagePath">The message path for this messenger</param>
/// <param name="device">The device for this messenger</param>
public ITswAppControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device)
{
_appControl = device as ITswAppControl;
}
/// <inheritdoc />
protected override void RegisterActions()
{
if (_appControl == null)
@@ -43,14 +50,14 @@ namespace PepperDash.Essentials.Touchpanel
};
}
private void SendFullStatus()
private void SendFullStatus(string id = null)
{
var message = new TswAppStateMessage
{
AppOpen = _appControl.AppOpenFeedback.BoolValue,
};
PostStatusMessage(message);
PostStatusMessage(message, id);
}
}
@@ -59,6 +66,9 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary>
public class TswAppStateMessage : DeviceStateMessageBase
{
/// <summary>
/// True if the Zoom app is open on a TSW panel
/// </summary>
[JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)]
public bool? AppOpen { get; set; }
}

View File

@@ -8,17 +8,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel
{
/// <summary>
/// Represents a ITswZoomControlMessenger
/// Messenger to handle Zoom status and control for a TSW panel that supports the Zoom Application
/// </summary>
public class ITswZoomControlMessenger : MessengerBase
{
private readonly ITswZoomControl _zoomControl;
/// <summary>
/// Create an instance of the <see cref="ITswZoomControlMessenger"/> class for the given device
/// </summary>
/// <param name="key">The key for this messenger</param>
/// <param name="messagePath">The message path for this messenger</param>
/// <param name="device">The device for this messenger</param>
public ITswZoomControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device)
{
_zoomControl = device as ITswZoomControl;
}
/// <inheritdoc />
protected override void RegisterActions()
{
if (_zoomControl == null)
@@ -27,7 +34,9 @@ namespace PepperDash.Essentials.Touchpanel
return;
}
AddAction($"/fullStatus", (id, context) => SendFullStatus());
AddAction($"/fullStatus", (id, context) => SendFullStatus(id));
AddAction($"/zoomStatus", (id, content) => SendFullStatus(id));
AddAction($"/endCall", (id, context) => _zoomControl.EndZoomCall());
@@ -53,7 +62,7 @@ namespace PepperDash.Essentials.Touchpanel
};
}
private void SendFullStatus()
private void SendFullStatus(string id = null)
{
var message = new TswZoomStateMessage
{
@@ -61,7 +70,7 @@ namespace PepperDash.Essentials.Touchpanel
IncomingCall = _zoomControl?.ZoomIncomingCallFeedback.BoolValue
};
PostStatusMessage(message);
PostStatusMessage(message, id);
}
}
@@ -70,9 +79,16 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary>
public class TswZoomStateMessage : DeviceStateMessageBase
{
/// <summary>
/// True if the panel is in a Zoom call
/// </summary>
[JsonProperty("inCall", NullValueHandling = NullValueHandling.Ignore)]
public bool? InCall { get; set; }
/// <summary>
/// True if there is an incoming Zoom call
/// </summary>
[JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)]
public bool? IncomingCall { get; set; }
}

View File

@@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel
{
/// <summary>
/// Represents a ThemeMessenger
/// Messenger to save the current theme (light/dark) and send to a device
/// </summary>
public class ThemeMessenger : MessengerBase
{
private readonly ITheme _tpDevice;
/// <summary>
/// Create an instance of the <see cref="ThemeMessenger"/> class
/// </summary>
/// <param name="key">The key for this messenger</param>
/// <param name="path">The path for this messenger</param>
/// <param name="device">The device for this messenger</param>
public ThemeMessenger(string key, string path, ITheme device) : base(key, path, device as Device)
{
_tpDevice = device;
}
/// <inheritdoc />
protected override void RegisterActions()
{
AddAction("/fullStatus", (id, content) =>

View File

@@ -1,13 +1,9 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.WebSocketServer;
using Serilog.Events;
using System;
using System.Threading;
using WebSocketSharp;
namespace PepperDash.Essentials
@@ -20,12 +16,22 @@ namespace PepperDash.Essentials
private readonly WebSocket _ws;
private readonly object msgToSend;
/// <summary>
/// Initialize a message to send
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="ws">WebSocket instance</param>
public TransmitMessage(object msg, WebSocket ws)
{
_ws = ws;
msgToSend = msg;
}
/// <summary>
/// Initialize a message to send
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="ws">WebSocket instance</param>
public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws)
{
_ws = ws;
@@ -43,13 +49,13 @@ namespace PepperDash.Essentials
{
if (_ws == null)
{
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null");
Debug.LogWarning("Cannot send message. Websocket client is null");
return;
}
if (!_ws.IsAlive)
{
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is not connected");
Debug.LogWarning("Cannot send message. Websocket client is not connected");
return;
}
@@ -57,83 +63,14 @@ namespace PepperDash.Essentials
var message = JsonConvert.SerializeObject(msgToSend, Formatting.None,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } });
Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message);
Debug.LogVerbose("Message TX: {0}", message);
_ws.Send(message);
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Caught an exception in the Transmit Processor");
}
}
#endregion
}
/// <summary>
/// Represents a MessageToClients
/// </summary>
public class MessageToClients : IQueueMessage
{
private readonly MobileControlWebsocketServer _server;
private readonly object msgToSend;
public MessageToClients(object msg, MobileControlWebsocketServer server)
{
_server = server;
msgToSend = msg;
}
public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server)
{
_server = server;
msgToSend = msg;
}
#region Implementation of IQueueMessage
/// <summary>
/// Dispatch method
/// </summary>
public void Dispatch()
{
try
{
if (_server == null)
{
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null");
return;
}
var message = JsonConvert.SerializeObject(msgToSend, Formatting.None,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } });
var clientSpecificMessage = msgToSend as MobileControlMessage;
if (clientSpecificMessage.ClientId != null)
{
var clientId = clientSpecificMessage.ClientId;
_server.LogVerbose("Message TX To client {clientId} Message: {message}", clientId, message);
_server.SendMessageToClient(clientId, message);
return;
}
_server.SendMessageToAllClients(message);
_server.LogVerbose("Message TX To all clients: {message}", message);
}
catch (ThreadAbortException)
{
//Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Caught an exception in the Transmit Processor");
Debug.LogError("Caught an exception in the Transmit Processor: {message}", ex.Message);
Debug.LogDebug(ex, "Stack Trace: ");
}
}
#endregion

View File

@@ -3,12 +3,19 @@ using System;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a UserCodeChanged
/// Defines the action to take when the User code changes
/// </summary>
public class UserCodeChanged
{
/// <summary>
/// Action to take when the User Code changes
/// </summary>
public Action<string, string> UpdateUserCode { get; private set; }
/// <summary>
/// create an instance of the <see cref="UserCodeChanged"/> class
/// </summary>
/// <param name="updateMethod">action to take when the User Code changes</param>
public UserCodeChanged(Action<string, string> updateMethod)
{
UpdateUserCode = updateMethod;

View File

@@ -0,0 +1,97 @@
using PepperDash.Core;
using PepperDash.Core.Logging;
using WebSocketSharp;
namespace PepperDash.Essentials
{
/// <summary>
/// Utility functions for logging and other common tasks.
/// </summary>
public static class Utilities
{
private static int nextClientId = 0;
/// <summary>
/// Get the next unique client ID
/// </summary>
/// <returns>Client ID</returns>
public static int GetNextClientId()
{
nextClientId++;
return nextClientId;
}
/// <summary>
/// Converts a WebSocketServer LogData object to Essentials logging calls.
/// </summary>
/// <param name="data">The LogData object to convert.</param>
/// <param name="message">The log message.</param>
/// <param name="device">The device associated with the log message.</param>
public static void ConvertWebsocketLog(LogData data, string message, IKeyed device = null)
{
switch (data.Level)
{
case LogLevel.Trace:
if (device == null)
{
Debug.LogVerbose(message);
}
else
{
device.LogVerbose(message);
}
break;
case LogLevel.Debug:
if (device == null)
{
Debug.LogDebug(message);
}
else
{
device.LogDebug(message);
}
break;
case LogLevel.Info:
if (device == null)
{
Debug.LogInformation(message);
}
else
{
device.LogInformation(message);
}
break;
case LogLevel.Warn:
if (device == null)
{
Debug.LogWarning(message);
}
else
{
device.LogWarning(message);
}
break;
case LogLevel.Error:
if (device == null)
{
Debug.LogError(message);
}
else
{
device.LogError(message);
}
break;
case LogLevel.Fatal:
if (device == null)
{
Debug.LogFatal(message);
}
else
{
device.LogFatal(message);
}
break;
}
}
}
}

View File

@@ -15,15 +15,17 @@ namespace PepperDash.Essentials
[JsonProperty("master", NullValueHandling = NullValueHandling.Ignore)]
public Volume Master { get; set; }
/// <summary>
/// Aux Faders as configured in the room
/// </summary>
[JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, Volume> AuxFaders { get; set; }
/// <summary>
/// Count of aux faders for this system
/// </summary>
[JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)]
public int? NumberOfAuxFaders { get; set; }
public Volumes()
{
}
}
/// <summary>
@@ -31,16 +33,21 @@ namespace PepperDash.Essentials
/// </summary>
public class Volume
{
/// <summary>
/// Gets or sets the Key
/// </summary>
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
public string Key { get; set; }
/// <summary>
/// Level for this volume object
/// </summary>
[JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)]
public int? Level { get; set; }
/// <summary>
/// True if this volume control is muted
/// </summary>
[JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)]
public bool? Muted { get; set; }
@@ -51,12 +58,21 @@ namespace PepperDash.Essentials
[JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
public string Label { get; set; }
/// <summary>
/// True if this volume object has mute control
/// </summary>
[JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasMute { get; set; }
/// <summary>
/// True if this volume object has Privacy mute control
/// </summary>
[JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasPrivacyMute { get; set; }
/// <summary>
/// True if the privacy mute is muted
/// </summary>
[JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)]
public bool? PrivacyMuted { get; set; }
@@ -68,6 +84,15 @@ namespace PepperDash.Essentials
[JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)]
public string MuteIcon { get; set; }
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
/// <param name="level">The level for this volume object</param>
/// <param name="muted">True if this volume control is muted</param>
/// <param name="label">The label for this volume object</param>
/// <param name="hasMute">True if this volume object has mute control</param>
/// <param name="muteIcon">The mute icon for this volume object</param>
public Volume(string key, int level, bool muted, string label, bool hasMute, string muteIcon)
: this(key)
{
@@ -78,18 +103,32 @@ namespace PepperDash.Essentials
MuteIcon = muteIcon;
}
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
/// <param name="level">The level for this volume object</param>
public Volume(string key, int level)
: this(key)
{
Level = level;
}
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
/// <param name="muted">True if this volume control is muted</param>
public Volume(string key, bool muted)
: this(key)
{
Muted = muted;
}
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
public Volume(string key)
{
Key = key;

View File

@@ -12,11 +12,20 @@ namespace PepperDash.Essentials.WebApiHandlers
public class ActionPathsHandler : WebApiBaseRequestHandler
{
private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="ActionPathsHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public ActionPathsHandler(MobileControlSystemController controller) : base(true)
{
mcController = controller;
}
/// <summary>
/// Handle a request to get the action paths
/// </summary>
/// <param name="context">Request Context</param>
protected override void HandleGet(HttpCwsContext context)
{
var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController));
@@ -37,9 +46,16 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlSystemController mcController;
/// <summary>
/// Registered action paths for this system
/// </summary>
[JsonProperty("actionPaths")]
public List<ActionPath> ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2 }).ToList();
/// <summary>
/// Create an instance of the <see cref="ActionPathsResponse"/> class.
/// </summary>
/// <param name="mcController"></param>
public ActionPathsResponse(MobileControlSystemController mcController)
{
this.mcController = mcController;

View File

@@ -1,10 +1,10 @@
using Crestron.SimplSharp.WebScripting;
using System;
using System.Threading.Tasks;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web;
using System;
using System.Threading.Tasks;
namespace PepperDash.Essentials.WebApiHandlers
{
@@ -15,11 +15,20 @@ namespace PepperDash.Essentials.WebApiHandlers
{
private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="MobileAuthRequestHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true)
{
mcController = controller;
}
/// <summary>
/// Handle authorization request for this processor
/// </summary>
/// <param name="context">request context</param>
/// <returns>Task</returns>
protected override async Task HandlePost(HttpCwsContext context)
{
try

View File

@@ -1,26 +1,35 @@
using Crestron.SimplSharp.WebScripting;
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.WebSocketServer;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PepperDash.Essentials.WebApiHandlers
{
/// <summary>
/// Represents a MobileInfoHandler
/// Represents a MobileInfoHandler. Used with the Essentials CWS API
/// </summary>
public class MobileInfoHandler : WebApiBaseRequestHandler
{
private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="MobileInfoHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public MobileInfoHandler(MobileControlSystemController controller) : base(true)
{
mcController = controller;
}
/// <summary>
/// Get Mobile Control Information
/// </summary>
/// <param name="context"></param>
protected override void HandleGet(HttpCwsContext context)
{
try
@@ -50,14 +59,22 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlSystemController mcController;
/// <summary>
/// Edge Server. Null if edge server is disabled
/// </summary>
[JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)]
public MobileControlEdgeServer EdgeServer => mcController.Config.EnableApiServer ? new MobileControlEdgeServer(mcController) : null;
/// <summary>
/// Direct server. Null if the direct server is disabled
/// </summary>
[JsonProperty("directServer", NullValueHandling = NullValueHandling.Ignore)]
public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null;
/// <summary>
/// Create an instance of the <see cref="InformationResponse"/> class.
/// </summary>
/// <param name="controller"></param>
public InformationResponse(MobileControlSystemController controller)
{
mcController = controller;
@@ -72,24 +89,46 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlSystemController mcController;
/// <summary>
/// Mobile Control Edge Server address for this system
/// </summary>
[JsonProperty("serverAddress")]
public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host;
/// <summary>
/// System Name for this system
/// </summary>
[JsonProperty("systemName")]
public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config";
/// <summary>
/// System URL for this system
/// </summary>
[JsonProperty("systemUrl")]
public string SystemUrl => ConfigReader.ConfigObject.SystemUrl;
/// <summary>
/// User code to use in MC UI for this system
/// </summary>
[JsonProperty("userCode")]
public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available";
/// <summary>
/// True if connected to edge server
/// </summary>
[JsonProperty("connected")]
public bool Connected => mcController.Connected;
/// <summary>
/// Seconds since last comms with edge server
/// </summary>
[JsonProperty("secondsSinceLastAck")]
public int SecondsSinceLastAck => (DateTime.Now - mcController.LastAckMessage).Seconds;
/// <summary>
/// Create an instance of the <see cref="MobileControlEdgeServer"/> class.
/// </summary>
/// <param name="controller">controller to use for this</param>
public MobileControlEdgeServer(MobileControlSystemController controller)
{
mcController = controller;
@@ -104,21 +143,43 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlWebsocketServer directServer;
/// <summary>
/// URL to use to interact with this server
/// </summary>
[JsonProperty("userAppUrl")]
public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]";
/// <summary>
/// TCP/IP Port this server is configured to use
/// </summary>
[JsonProperty("serverPort")]
public int ServerPort => directServer.Port;
/// <summary>
/// Count of defined tokens for this server
/// </summary>
[JsonProperty("tokensDefined")]
public int TokensDefined => directServer.UiClients.Count;
public int TokensDefined => directServer.UiClientContexts.Count;
/// <summary>
/// Count of connected clients
/// </summary>
[JsonProperty("clientsConnected")]
public int ClientsConnected => directServer.ConnectedUiClientsCount;
/// <summary>
/// List of tokens and connected clients for this server
/// </summary>
[JsonProperty("clients")]
public List<MobileControlDirectClient> Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList();
public List<MobileControlDirectClient> Clients => directServer.UiClientContexts
.Select(context => (context, clients: directServer.UiClients.Where(client => client.Value.Token == context.Value.Token.Token).Select(c => c.Value).ToList()))
.Select((clientTuple, i) => new MobileControlDirectClient(clientTuple.clients, clientTuple.context, i, directServer.UserAppUrlPrefix))
.ToList();
/// <summary>
/// Create an instance of the <see cref="MobileControlDirectServer"/> class.
/// </summary>
/// <param name="server"></param>
public MobileControlDirectServer(MobileControlWebsocketServer server)
{
directServer = server;
@@ -142,33 +203,85 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly string urlPrefix;
/// <summary>
/// Client number for this client
/// </summary>
[JsonProperty("clientNumber")]
public string ClientNumber => $"{clientNumber}";
/// <summary>
/// Room Key for this client
/// </summary>
[JsonProperty("roomKey")]
public string RoomKey => context.Token.RoomKey;
/// <summary>
/// Touchpanel Key, if defined, for this client
/// </summary>
[JsonProperty("touchpanelKey")]
public string TouchpanelKey => context.Token.TouchpanelKey;
/// <summary>
/// URL for this client
/// </summary>
[JsonProperty("url")]
public string Url => $"{urlPrefix}{Key}";
/// <summary>
/// Token for this client
/// </summary>
[JsonProperty("token")]
public string Token => Key;
[JsonProperty("connected")]
public bool Connected => context.Client != null && context.Client.Context.WebSocket.IsAlive;
private readonly List<UiClient> clients;
[JsonProperty("duration")]
public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds;
/// <summary>
/// List of status for all connected UI Clients
/// </summary>
[JsonProperty("clientStatus")]
public List<ClientStatus> ClientStatus => clients.Select(c => new ClientStatus(c)).ToList();
public MobileControlDirectClient(KeyValuePair<string, UiClientContext> clientContext, int index, string urlPrefix)
/// <summary>
/// Create an instance of the <see cref="MobileControlDirectClient"/> class.
/// </summary>
/// <param name="clients">List of Websocket Clients</param>
/// <param name="context">Context for the client</param>
/// <param name="index">Index of the client</param>
/// <param name="urlPrefix">URL prefix for the client</param>
public MobileControlDirectClient(List<UiClient> clients, KeyValuePair<string, UiClientContext> context, int index, string urlPrefix)
{
context = clientContext.Value;
Key = clientContext.Key;
this.context = context.Value;
Key = context.Key;
clientNumber = index;
this.urlPrefix = urlPrefix;
this.clients = clients;
}
}
/// <summary>
/// Report the status of a UiClient
/// </summary>
public class ClientStatus
{
private readonly UiClient client;
/// <summary>
/// True if client is connected
/// </summary>
public bool Connected => client != null && client.Context.WebSocket.IsAlive;
/// <summary>
/// Get the time this client has been connected
/// </summary>
public double Duration => client == null ? 0 : client.ConnectedDuration.TotalSeconds;
/// <summary>
/// Create an instance of the <see cref="ClientStatus"/> class for the specified client
/// </summary>
/// <param name="client">client to report on</param>
public ClientStatus(UiClient client)
{
this.client = client;
}
}
}

View File

@@ -14,11 +14,20 @@ namespace PepperDash.Essentials.WebApiHandlers
public class UiClientHandler : WebApiBaseRequestHandler
{
private readonly MobileControlWebsocketServer server;
/// <summary>
/// Essentials CWS API handler for the MC Direct Server
/// </summary>
/// <param name="directServer">Direct Server instance</param>
public UiClientHandler(MobileControlWebsocketServer directServer) : base(true)
{
server = directServer;
}
/// <summary>
/// Create a client for the Direct Server
/// </summary>
/// <param name="context">HTTP Context for this request</param>
protected override void HandlePost(HttpCwsContext context)
{
var req = context.Request;
@@ -65,6 +74,10 @@ namespace PepperDash.Essentials.WebApiHandlers
res.End();
}
/// <summary>
/// Handle DELETE request for a Client
/// </summary>
/// <param name="context"></param>
protected override void HandleDelete(HttpCwsContext context)
{
var req = context.Request;
@@ -93,7 +106,7 @@ namespace PepperDash.Essentials.WebApiHandlers
if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext))
if (!server.UiClientContexts.TryGetValue(request.Token, out UiClientContext clientContext))
{
var response = new ClientResponse
{
@@ -134,7 +147,7 @@ namespace PepperDash.Essentials.WebApiHandlers
return;
}
server.UiClients.Remove(request.Token);
server.UiClientContexts.Remove(request.Token);
server.UpdateSecret();

View File

@@ -0,0 +1,24 @@
using System;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Event Args for <see cref="UiClient"/> ConnectionClosed event
/// </summary>
public class ConnectionClosedEventArgs : EventArgs
{
/// <summary>
/// Client ID that is being closed
/// </summary>
public string ClientId { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="ConnectionClosedEventArgs"/> class.
/// </summary>
/// <param name="clientId">client that's closing</param>
public ConnectionClosedEventArgs(string clientId)
{
ClientId = clientId;
}
}
}

View File

@@ -17,9 +17,15 @@ namespace PepperDash.Essentials.WebSocketServer
[JsonProperty("clientId")]
public string ClientId { get; set; }
/// <summary>
/// Room Key for this client
/// </summary>
[JsonProperty("roomKey")]
public string RoomKey { get; set; }
/// <summary>
/// System UUID for this system
/// </summary>
[JsonProperty("systemUUid")]
public string SystemUuid { get; set; }

View File

@@ -5,15 +5,28 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public class JoinToken
{
/// <summary>
/// Unique client ID for a client that is joining
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the Code
/// </summary>
public string Code { get; set; }
/// <summary>
/// Room Key this token is associated with
/// </summary>
public string RoomKey { get; set; }
/// <summary>
/// Unique ID for this token
/// </summary>
public string Uuid { get; set; }
/// <summary>
/// Touchpanel Key this token is associated with, if this is a touch panel token
/// </summary>
public string TouchpanelKey { get; set; } = "";
/// <summary>

View File

@@ -10,6 +10,7 @@ using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using Org.BouncyCastle.Crypto.Prng;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
@@ -56,7 +57,14 @@ namespace PepperDash.Essentials.WebSocketServer
/// <summary>
/// Gets the collection of UI client contexts
/// </summary>
public Dictionary<string, UiClientContext> UiClients { get; private set; }
public Dictionary<string, UiClientContext> UiClientContexts { get; private set; }
private readonly Dictionary<string, UiClient> uiClients = new Dictionary<string, UiClient>();
/// <summary>
/// Gets the collection of UI clients
/// </summary>
public ReadOnlyDictionary<string, UiClient> UiClients => new ReadOnlyDictionary<string, UiClient>(uiClients);
private readonly MobileControlSystemController _parent;
@@ -127,17 +135,7 @@ namespace PepperDash.Essentials.WebSocketServer
{
get
{
var count = 0;
foreach (var client in UiClients)
{
if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
{
count++;
}
}
return count;
return uiClients.Values.Where(c => c.Context.WebSocket.IsAlive).Count();
}
}
@@ -202,7 +200,7 @@ namespace PepperDash.Essentials.WebSocketServer
}
UiClients = new Dictionary<string, UiClientContext>();
UiClientContexts = new Dictionary<string, UiClientContext>();
//_joinTokens = new Dictionary<string, JoinToken>();
@@ -277,30 +275,10 @@ namespace PepperDash.Essentials.WebSocketServer
};
}
_server.Log.Output = (data, message) =>
{
switch (data.Level)
{
case LogLevel.Trace:
this.LogVerbose(data.Message);
break;
case LogLevel.Debug:
this.LogDebug(data.Message);
break;
case LogLevel.Info:
this.LogInformation(data.Message);
break;
case LogLevel.Warn:
this.LogWarning(data.Message);
break;
case LogLevel.Error:
this.LogError(data.Message);
break;
case LogLevel.Fatal:
this.LogFatal(data.Message);
break;
}
};
_server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
// setting to trace to allow logging level to be controlled by appdebug
_server.Log.Level = LogLevel.Trace;
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
@@ -326,6 +304,9 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
/// <summary>
/// Set the internal logging level for the Websocket Server
/// </summary>
public void SetWebsocketLogLevel(LogLevel level)
{
CrestronConsole.ConsoleCommandResponse($"Setting direct server debug level to {level}", level.ToString());
@@ -554,20 +535,20 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey);
if (UiClients == null)
if (UiClientContexts == null)
{
UiClients = new Dictionary<string, UiClientContext>();
UiClientContexts = new Dictionary<string, UiClientContext>();
}
UiClients.Add(token.Key, new UiClientContext(token.Value));
UiClientContexts.Add(token.Key, new UiClientContext(token.Value));
}
}
if (UiClients.Count > 0)
if (UiClientContexts.Count > 0)
{
this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClients.Count);
this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClientContexts.Count);
foreach (var client in UiClients)
foreach (var client in UiClientContexts)
{
var key = client.Key;
var path = _wsPath + key;
@@ -575,13 +556,8 @@ namespace PepperDash.Essentials.WebSocketServer
_server.AddWebSocketService(path, () =>
{
var c = new UiClient($"uiclient-{key}-{roomKey}");
this.LogDebug("Constructing UiClient with id: {key}", key);
c.Controller = _parent;
c.RoomKey = roomKey;
UiClients[key].SetClient(c);
return c;
this.LogInformation("Building a UiClient with ID {id}", client.Value.Token.Id);
return BuildUiClient(roomKey, client.Value.Token, key);
});
}
}
@@ -591,7 +567,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogWarning("No secret found");
}
this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClients.Count);
this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClientContexts.Count);
}
catch (Exception ex)
{
@@ -616,7 +592,7 @@ namespace PepperDash.Essentials.WebSocketServer
_secret.Tokens.Clear();
foreach (var uiClientContext in UiClients)
foreach (var uiClientContext in UiClientContexts)
{
_secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token);
}
@@ -725,21 +701,17 @@ namespace PepperDash.Essentials.WebSocketServer
var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey };
UiClients.Add(key, new UiClientContext(token));
UiClientContexts.Add(key, new UiClientContext(token));
var path = _wsPath + key;
_server.AddWebSocketService(path, () =>
{
var c = new UiClient($"uiclient-{key}-{bridge.RoomKey}");
this.LogVerbose("Constructing UiClient with id: {key}", key);
c.Controller = _parent;
c.RoomKey = bridge.RoomKey;
UiClients[key].SetClient(c);
return c;
this.LogInformation("Building a UiClient with ID {id}", token.Id);
return BuildUiClient(bridge.RoomKey, token, key);
});
this.LogInformation("Added new WebSocket UiClient service at path: {path}", path);
this.LogInformation("Added new WebSocket UiClient for path: {path}", path);
this.LogInformation("Token: {@token}", token);
this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count);
@@ -749,6 +721,44 @@ namespace PepperDash.Essentials.WebSocketServer
return (key, path);
}
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{
var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id);
c.Controller = _parent;
c.RoomKey = roomKey;
if (uiClients.ContainsKey(token.Id))
{
this.LogWarning("removing client with duplicate id {id}", token.Id);
uiClients.Remove(token.Id);
}
uiClients.Add(token.Id, c);
// UiClients[key].SetClient(c);
c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId);
token.Id = null;
return c;
}
/// <summary>
/// Prints out the session data for each path
/// </summary>
public void PrintSessionData()
{
foreach (var path in _server.WebSocketServices.Paths)
{
this.LogInformation("Path: {path}", path);
this.LogInformation(" Session Count: {sessionCount}", _server.WebSocketServices[path].Sessions.Count);
this.LogInformation(" Active Session Count: {activeSessionCount}", _server.WebSocketServices[path].Sessions.ActiveIDs.Count());
this.LogInformation(" Inactive Session Count: {inactiveSessionCount}", _server.WebSocketServices[path].Sessions.InactiveIDs.Count());
this.LogInformation(" Active Clients:");
foreach (var session in _server.WebSocketServices[path].Sessions.IDs)
{
this.LogInformation(" Client ID: {id}", (_server.WebSocketServices[path].Sessions[session] as UiClient)?.Id);
}
}
}
/// <summary>
/// Removes all clients from the server
/// </summary>
@@ -766,7 +776,7 @@ namespace PepperDash.Essentials.WebSocketServer
return;
}
foreach (var client in UiClients)
foreach (var client in UiClientContexts)
{
if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
{
@@ -784,7 +794,7 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
UiClients.Clear();
UiClientContexts.Clear();
UpdateSecret();
}
@@ -803,9 +813,9 @@ namespace PepperDash.Essentials.WebSocketServer
var key = s;
if (UiClients.ContainsKey(key))
if (UiClientContexts.ContainsKey(key))
{
var uiClientContext = UiClients[key];
var uiClientContext = UiClientContexts[key];
if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive)
{
@@ -815,7 +825,7 @@ namespace PepperDash.Essentials.WebSocketServer
var path = _wsPath + key;
if (_server.RemoveWebSocketService(path))
{
UiClients.Remove(key);
UiClientContexts.Remove(key);
UpdateSecret();
@@ -839,9 +849,9 @@ namespace PepperDash.Essentials.WebSocketServer
{
CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r");
CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count));
CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClientContexts.Count));
foreach (var client in UiClients)
foreach (var client in UiClientContexts)
{
CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key));
}
@@ -853,9 +863,9 @@ namespace PepperDash.Essentials.WebSocketServer
{
foreach (var client in UiClients.Values)
{
if (client.Client != null && client.Client.Context.WebSocket.IsAlive)
if (client != null && client.Context.WebSocket.IsAlive)
{
client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
}
}
@@ -990,77 +1000,81 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogVerbose("Join Room Request with token: {token}", token);
byte[] body;
if (UiClients.TryGetValue(token, out UiClientContext clientContext))
{
var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey);
if (bridge != null)
{
res.StatusCode = 200;
res.ContentType = "application/json";
var devices = DeviceManager.GetDevices();
Dictionary<string, DeviceInterfaceInfo> deviceInterfaces = new Dictionary<string, DeviceInterfaceInfo>();
foreach (var device in devices)
{
var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List<string>();
deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo
{
Key = device.Key,
Name = device is IKeyName ? (device as IKeyName).Name : "",
Interfaces = interfaces
});
}
// Construct the response object
JoinResponse jRes = new JoinResponse
{
ClientId = token,
RoomKey = bridge.RoomKey,
SystemUuid = _parent.SystemUuid,
RoomUuid = _parent.SystemUuid,
Config = _parent.GetConfigWithPluginVersion(),
CodeExpires = new DateTime().AddYears(1),
UserCode = bridge.UserCode,
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port),
EnableDebug = false,
DeviceInterfaceSupport = deviceInterfaces
};
// Serialize to JSON and convert to Byte[]
var json = JsonConvert.SerializeObject(jRes);
var body = Encoding.UTF8.GetBytes(json);
res.ContentLength64 = body.LongLength;
// Send the response
res.Close(body, true);
}
else
{
var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey);
res.StatusCode = 404;
res.ContentType = "application/json";
this.LogVerbose("{message}", message);
var body = Encoding.UTF8.GetBytes(message);
res.ContentLength64 = body.LongLength;
res.Close(body, true);
}
}
else
if (!UiClientContexts.TryGetValue(token, out UiClientContext clientContext))
{
var message = "Token invalid or has expired";
res.StatusCode = 401;
res.ContentType = "application/json";
this.LogVerbose("{message}", message);
var body = Encoding.UTF8.GetBytes(message);
body = Encoding.UTF8.GetBytes(message);
res.ContentLength64 = body.LongLength;
res.Close(body, true);
return;
}
var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey);
if (bridge == null)
{
var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey);
res.StatusCode = 404;
res.ContentType = "application/json";
this.LogVerbose("{message}", message);
body = Encoding.UTF8.GetBytes(message);
res.ContentLength64 = body.LongLength;
res.Close(body, true);
return;
}
res.StatusCode = 200;
res.ContentType = "application/json";
var devices = DeviceManager.GetDevices();
Dictionary<string, DeviceInterfaceInfo> deviceInterfaces = new Dictionary<string, DeviceInterfaceInfo>();
foreach (var device in devices)
{
var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List<string>();
deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo
{
Key = device.Key,
Name = (device as IKeyName)?.Name ?? "",
Interfaces = interfaces
});
}
var clientId = $"{Utilities.GetNextClientId()}";
clientContext.Token.Id = clientId;
this.LogVerbose("Assigning ClientId: {clientId}", clientId);
// Construct the response object
JoinResponse jRes = new JoinResponse
{
ClientId = clientId,
RoomKey = bridge.RoomKey,
SystemUuid = _parent.SystemUuid,
RoomUuid = _parent.SystemUuid,
Config = _parent.GetConfigWithPluginVersion(),
CodeExpires = new DateTime().AddYears(1),
UserCode = bridge.UserCode,
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port),
EnableDebug = false,
DeviceInterfaceSupport = deviceInterfaces
};
// Serialize to JSON and convert to Byte[]
var json = JsonConvert.SerializeObject(jRes);
body = Encoding.UTF8.GetBytes(json);
res.ContentLength64 = body.LongLength;
// Send the response
res.Close(body, true);
}
/// <summary>
@@ -1242,12 +1256,14 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public void SendMessageToAllClients(string message)
{
foreach (var clientContext in UiClients.Values)
foreach (var client in uiClients.Values)
{
if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive)
if (!client.Context.WebSocket.IsAlive)
{
clientContext.Client.Context.WebSocket.Send(message);
continue;
}
client.Context.WebSocket.Send(message);
}
}
@@ -1266,17 +1282,16 @@ namespace PepperDash.Essentials.WebSocketServer
return;
}
if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext))
if (uiClients.TryGetValue((string)clientId, out var client))
{
if (clientContext.Client != null)
{
var socket = clientContext.Client.Context.WebSocket;
var socket = client.Context.WebSocket;
if (socket.IsAlive)
{
socket.Send(message);
}
if (!socket.IsAlive)
{
this.LogError("Unable to send message to client {id}. Client is disconnected: {message}", clientId, message);
return;
}
socket.Send(message);
}
else
{

View File

@@ -13,8 +13,15 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public string GrantCode { get; set; }
/// <summary>
/// Gets or sets the Tokens for this server
/// </summary>
public Dictionary<string, JoinToken> Tokens { get; set; }
/// <summary>
/// Initialize a new instance of the <see cref="ServerTokenSecrets"/> class with the provided grant code
/// </summary>
/// <param name="grantCode">The grant code for this server</param>
public ServerTokenSecrets(string grantCode)
{
GrantCode = grantCode;

View File

@@ -1,5 +1,4 @@
using System;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
@@ -22,6 +21,21 @@ namespace PepperDash.Essentials.WebSocketServer
/// <inheritdoc />
public string Key { get; private set; }
/// <summary>
/// Client ID used by client for this connection
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Token associated with this client
/// </summary>
public string Token { get; private set; }
/// <summary>
/// Touchpanel Key associated with this client
/// </summary>
public string TouchpanelKey { get; private set; }
/// <summary>
/// Gets or sets the mobile control system controller that handles this client's messages
/// </summary>
@@ -32,11 +46,6 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public string RoomKey { get; set; }
/// <summary>
/// The unique identifier for this client instance
/// </summary>
private string _clientId;
/// <summary>
/// The timestamp when this client connection was established
/// </summary>
@@ -60,13 +69,24 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
/// <summary>
/// Triggered when this client closes it's connection
/// </summary>
public event EventHandler<ConnectionClosedEventArgs> ConnectionClosed;
/// <summary>
/// Initializes a new instance of the UiClient class with the specified key
/// </summary>
/// <param name="key">The unique key to identify this client</param>
public UiClient(string key)
/// <param name="id">The client ID used by the client for this connection</param>
/// <param name="token">The token associated with this client</param>
/// <param name="touchpanelKey">The touchpanel key associated with this client</param>
public UiClient(string key, string id, string token, string touchpanelKey = "")
{
Key = key;
Id = id;
Token = token;
TouchpanelKey = touchpanelKey;
}
/// <inheritdoc />
@@ -74,19 +94,10 @@ namespace PepperDash.Essentials.WebSocketServer
{
base.OnOpen();
var url = Context.WebSocket.Url;
this.LogInformation("New WebSocket Connection from: {url}", url);
_connectionTime = DateTime.Now;
var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)");
if (!match.Success)
{
_connectionTime = DateTime.Now;
return;
}
var clientId = match.Groups[1].Value;
_clientId = clientId;
Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
Log.Level = LogLevel.Trace;
if (Controller == null)
{
@@ -99,8 +110,9 @@ namespace PepperDash.Essentials.WebSocketServer
Type = "/system/clientJoined",
Content = JToken.FromObject(new
{
clientId,
clientId = Id,
roomKey = RoomKey,
touchpanelKey = string.IsNullOrEmpty(TouchpanelKey) ? string.Empty : TouchpanelKey,
})
};
@@ -110,7 +122,7 @@ namespace PepperDash.Essentials.WebSocketServer
if (bridge == null) return;
SendUserCodeToClient(bridge, clientId);
SendUserCodeToClient(bridge, Id);
bridge.UserCodeChanged -= Bridge_UserCodeChanged;
bridge.UserCodeChanged += Bridge_UserCodeChanged;
@@ -125,7 +137,7 @@ namespace PepperDash.Essentials.WebSocketServer
/// <param name="e">Event arguments</param>
private void Bridge_UserCodeChanged(object sender, EventArgs e)
{
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, Id);
}
/// <summary>
@@ -172,13 +184,15 @@ namespace PepperDash.Essentials.WebSocketServer
foreach (var messenger in Controller.Messengers)
{
messenger.Value.UnsubscribeClient(_clientId);
messenger.Value.UnsubscribeClient(Id);
}
foreach (var messenger in Controller.DefaultMessengers)
{
messenger.Value.UnsubscribeClient(_clientId);
messenger.Value.UnsubscribeClient(Id);
}
ConnectionClosed?.Invoke(this, new ConnectionClosedEventArgs(Id));
}
/// <inheritdoc />

View File

@@ -14,6 +14,10 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public JoinToken Token { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="UiClientContext"/> class with the provided token
/// </summary>
/// <param name="token">token for this client</param>
public UiClientContext(JoinToken token)
{
Token = token;

View File

@@ -8,12 +8,25 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public class Version
{
/// <summary>
/// Server version this Websocket is connected to
/// </summary>
[JsonProperty("serverVersion")]
public string ServerVersion { get; set; }
/// <summary>
/// True if the server is on a processor
/// </summary>
[JsonProperty("serverIsRunningOnProcessorHardware")]
public bool ServerIsRunningOnProcessorHardware { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="Version"/> class
/// </summary>
/// <remarks>
/// The <see cref="ServerIsRunningOnProcessorHardware"/> property is set to true by default.
/// </remarks>
public Version()
{
ServerIsRunningOnProcessorHardware = true;

View File

@@ -13,25 +13,28 @@ namespace PepperDash.Essentials.WebSocketServer
}
/// <summary>
/// Represents a WebSocketServerSecret
/// Stores a secret value using the provided secret store provider
/// </summary>
public class WebSocketServerSecret : ISecret
{
/// <summary>
/// Gets or sets the Provider
/// Gets the Secret Provider associated with this secret
/// </summary>
public ISecretProvider Provider { get; private set; }
/// <summary>
/// Gets or sets the Key
/// Gets the Key associated with this secret
/// </summary>
public string Key { get; private set; }
/// <summary>
/// Gets or sets the Value
/// Gets the Value associated with this secret
/// </summary>
public object Value { get; private set; }
/// <summary>
/// Initialize and instance of the <see cref="WebSocketServerSecret"/> class
/// </summary>
public WebSocketServerSecret(string key, object value, ISecretProvider provider)
{
Key = key;