Compare commits

...

31 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
1c55003a20 feat: add HttpPut and HttpDelete attributes for comprehensive HTTP method support
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-25 13:09:46 +00:00
copilot-swe-agent[bot]
9c56b9b057 feat: add OpenAPI 3 documentation endpoint for Essentials CWS API 2025-07-24 14:13:02 +00:00
copilot-swe-agent[bot]
95fc8ddfff Add OpenAPI attributes to all remaining request handlers
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-24 14:07:18 +00:00
copilot-swe-agent[bot]
79d07ab7f4 Add OpenAPI attributes to additional request handlers for comprehensive coverage
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-24 13:53:48 +00:00
copilot-swe-agent[bot]
213973a323 Replace hardcoded SwaggerHandler logic with attribute-based metadata system
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-24 13:51:16 +00:00
copilot-swe-agent[bot]
0a85f1706e Initial plan for Swashbuckle-style attributes implementation
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-24 13:44:34 +00:00
copilot-swe-agent[bot]
a7267f1df3 Fix $ref properties in OpenAPI schema generation
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-24 01:18:00 +00:00
copilot-swe-agent[bot]
df61fdfea1 Add SwaggerHandler for OpenAPI 3 documentation endpoint
Co-authored-by: andrew-welker <1765622+andrew-welker@users.noreply.github.com>
2025-07-24 01:12:20 +00:00
copilot-swe-agent[bot]
db3ceaf0b4 Initial plan 2025-07-24 01:04:48 +00:00
Andrew Welker
1dbac7d1c8 Merge pull request #1292 from PepperDash/portkey-add
feat: add destination and source port key properties for advanced routing
2025-07-22 15:26:44 -05:00
Neil Dorin
799d4c127c Update src/PepperDash.Essentials.Core/Devices/DestinationListItem.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-22 14:02:01 -06:00
Andrew Welker
a6cd9a0571 feat: add destination and source port key properties for advanced routing 2025-07-22 14:56:28 -05:00
Andrew Welker
da30424657 Merge pull request #1289 from PepperDash/meter-feedback-interface
meter feedback interface
2025-07-21 15:20:29 -05:00
Andrew Welker
311452beac fix: use correct namespaces
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:30:11 -05:00
Andrew Welker
789113008e docs: update comments
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:29:11 -05:00
Andrew Welker
660836bd5a docs: remove spaces
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:28:59 -05:00
Andrew Welker
97b2ffed9c docs: fix comment
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:28:37 -05:00
Andrew Welker
2bbefa062d docs: fix comments
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-21 13:28:10 -05:00
Andrew Welker
3421b2f28c Merge branch 'main' into meter-feedback-interface 2025-07-17 12:34:16 -05:00
Andrew Welker
82889e9794 Merge pull request #1288 from PepperDash/mc-touchpanel-cs
Backwards Compatibility issues
2025-07-17 12:33:01 -05:00
Andrew Welker
1dcd4e328c fix: Destination support for USB 2025-07-17 12:32:26 -05:00
Andrew Welker
e76369726d docs: XML comments for DestinationListItem 2025-07-17 12:25:52 -05:00
Andrew Welker
2bf0f2092b fix: use new interface in direct server 2025-07-17 12:16:32 -05:00
Andrew Welker
c1eccfd790 fix: refactor interfaces for backwards compatibility 2025-07-17 12:13:08 -05:00
aknous
f1a89161bc Merge pull request #1287 from PepperDash/mc-touchpanel-cs
fix: use Control Subnet IP if MC TP devices are on the CS Lan
2025-07-17 11:17:03 -04:00
Andrew Welker
e59c50d0aa refactor: use tryParse for IP Address parsing
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-17 10:15:19 -05:00
Andrew Welker
9d313d8c7c fix: use Control Subnet IP if MC TP devices are on the CS Lan 2025-07-17 09:54:08 -05:00
Andrew Welker
9813673b66 feat: ICurrentSources interface to allow for tracking breakaway routing 2025-07-17 09:15:25 -05:00
Andrew Welker
ddbcc13c50 fix: add property for sync device association 2025-07-16 10:41:46 -05:00
Andrew Welker
2a70fc678e fix: add IStateFeedback interface 2025-07-11 13:13:52 -05:00
Andrew Welker
056614cba1 fix: add IMeterFeedback interface 2025-07-09 14:32:01 -05:00
38 changed files with 1485 additions and 169 deletions

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

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

View File

@@ -2,7 +2,14 @@
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
public interface IDisplay: IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName
/// <summary>
/// Interface for display devices that can be controlled and monitored.
/// This interface combines functionality for feedback, routing, power control,
/// warming/cooling, usage tracking, and key name management.
/// It is designed to be implemented by devices that require these capabilities,
/// such as projectors, displays, and other visual output devices.
/// </summary>
public interface IDisplay : IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName
{
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>
/// Interface for devices that provide audio meter feedback.
/// This interface is used to standardize access to meter feedback across different devices.
/// </summary>
public interface IMeterFeedback
{
/// <summary>
/// Gets the meter feedback for the device.
/// This property provides an IntFeedback that represents the current audio level or meter value.
/// </summary>
IntFeedback MeterFeedback { get; }
}
}

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.ObjectModel;
using Crestron.SimplSharpPro;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
@@ -33,11 +35,11 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
string SystemUuid { get; }
BoolFeedback ApiOnlineAndAuthorized { get;}
BoolFeedback ApiOnlineAndAuthorized { get; }
void SendMessageObject(IMobileControlMessage o);
void AddAction<T>(T messenger, Action<string, string, JToken> action) where T:IMobileControlMessenger;
void AddAction<T>(T messenger, Action<string, string, JToken> action) where T : IMobileControlMessenger;
void RemoveAction(string key);
@@ -45,14 +47,14 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
bool CheckForDeviceMessenger(string key);
IMobileControlRoomMessenger GetRoomMessenger(string key);
IMobileControlRoomMessenger GetRoomMessenger(string key);
}
}
/// <summary>
/// Describes a mobile control messenger
/// </summary>
public interface IMobileControlMessenger: IKeyed
public interface IMobileControlMessenger : IKeyed
{
IMobileControl AppServerController { get; }
string MessagePath { get; }
@@ -104,16 +106,47 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
public interface IMobileControlAction
{
IMobileControlMessenger Messenger { get; }
IMobileControlMessenger Messenger { get; }
Action<string,string, JToken> Action { get; }
Action<string, string, JToken> Action { get; }
}
/// <summary>
/// Describes a MobileControl Touchpanel Controller
/// </summary>
public interface IMobileControlTouchpanelController : IKeyed
{
/// <summary>
/// The default room key for the controller
/// </summary>
string DefaultRoomKey { get; }
/// <summary>
/// Sets the application URL for the controller
/// </summary>
/// <param name="url">The application URL</param>
void SetAppUrl(string url);
/// <summary>
/// Indicates whether the controller uses a direct server connection
/// </summary>
bool UseDirectServer { get; }
/// <summary>
/// Indicates whether the controller is a Zoom Room controller
/// </summary>
bool ZoomRoomController { get; }
}
/// <summary>
/// Describes a MobileControl Crestron Touchpanel Controller
/// This interface extends the IMobileControlTouchpanelController to include connected IP information
/// </summary>
public interface IMobileControlCrestronTouchpanelController : IMobileControlTouchpanelController
{
/// <summary>
/// Gets a collection of connected IP information for the touchpanel controller
/// </summary>
ReadOnlyCollection<ConnectedIpInformation> ConnectedIps { get; }
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>
/// Interface for devices that provide state feedback.
/// This interface is used to standardize access to state feedback across different devices.
/// </summary>
public interface IStateFeedback
{
/// <summary>
/// Gets the state feedback for the device.
/// This property provides a BoolFeedback that represents the current state (on/off) of the device.
/// </summary>
BoolFeedback StateFeedback { get; }
}
}

View File

@@ -5,19 +5,34 @@ using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Represents a destination item in a routing system that can receive audio/video signals.
/// Contains information about the destination device, its properties, and location settings.
/// </summary>
public class DestinationListItem
{
/// <summary>
/// Gets or sets the key identifier for the sink device that this destination represents.
/// </summary>
[JsonProperty("sinkKey")]
public string SinkKey { get; set; }
private EssentialsDevice _sinkDevice;
/// <summary>
/// Gets the actual device instance for this destination.
/// Lazily loads the device from the DeviceManager using the SinkKey.
/// </summary>
[JsonIgnore]
public EssentialsDevice SinkDevice
{
get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as EssentialsDevice); }
}
/// <summary>
/// Gets the preferred display name for this destination.
/// Returns the custom Name if set, otherwise returns the SinkDevice name, or "---" if no device is found.
/// </summary>
[JsonProperty("preferredName")]
public string PreferredName
{
@@ -32,31 +47,78 @@ namespace PepperDash.Essentials.Core
}
}
/// <summary>
/// Gets or sets the custom name for this destination.
/// If set, this name will be used as the PreferredName instead of the device name.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this destination should be included in destination lists.
/// </summary>
[JsonProperty("includeInDestinationList")]
public bool IncludeInDestinationList { get; set; }
/// <summary>
/// Gets or sets the display order for this destination in lists.
/// Lower values appear first in sorted lists.
/// </summary>
[JsonProperty("order")]
public int Order { get; set; }
/// <summary>
/// Gets or sets the surface location identifier for this destination.
/// Used to specify which surface or screen this destination is located on.
/// </summary>
[JsonProperty("surfaceLocation")]
public int SurfaceLocation { get; set; }
/// <summary>
/// Gets or sets the vertical location position for this destination.
/// Used for spatial positioning in multi-display configurations.
/// </summary>
[JsonProperty("verticalLocation")]
public int VerticalLocation { get; set; }
/// <summary>
/// Gets or sets the horizontal location position for this destination.
/// Used for spatial positioning in multi-display configurations.
/// </summary>
[JsonProperty("horizontalLocation")]
public int HorizontalLocation { get; set; }
/// <summary>
/// Gets or sets the signal type that this destination can receive (Audio, Video, AudioVideo, etc.).
/// </summary>
[JsonProperty("sinkType")]
public eRoutingSignalType SinkType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this destination is used for codec content sharing.
/// </summary>
[JsonProperty("isCodecContentDestination")]
public bool isCodecContentDestination { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this destination is used for program audio output.
/// </summary>
[JsonProperty("isProgramAudioDestination")]
public bool isProgramAudioDestination { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this destination supports USB connections.
/// Indicates if the destination can handle USB functionality, such as USB signal routing or device connections.
/// This property is used to determine compatibility with USB-based devices or systems.
/// </summary>
[JsonProperty("supportsUsb")]
public bool SupportsUsb { get; set; }
/// <summary>
/// The key of the destination port associated with this destination item
/// This is used to identify the specific port on the destination device that this item refers to for advanced routing
/// </summary>
[JsonProperty("destinationPortKey")]
public string DestinationPortKey { get; set; }
}
}

View File

@@ -1,12 +1,14 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PepperDash.Core;
using System.Collections.Generic;
namespace PepperDash.Essentials.Core
{
/// <summary>
///
/// Defines the type of source list item, which can be a route, off, or other.
/// This is used to categorize the source list items in a room.
/// The type is serialized to JSON and can be used to determine how the item should be displayed or handled in the UI.
/// </summary>
public enum eSourceListItemType
{
@@ -166,6 +168,26 @@ namespace PepperDash.Essentials.Core
[JsonProperty("disableSimpleRouting")]
public bool DisableSimpleRouting { get; set; }
/// <summary>
/// The key of the device that provides video sync for this source item
/// </summary>
[JsonProperty("syncProviderDeviceKey")]
public string SyncProviderDeviceKey { get; set; }
/// <summary>
/// Indicates if the source supports USB connections
/// </summary>
[JsonProperty("supportsUsb")]
public bool SupportsUsb { get; set; }
/// <summary>
/// The key of the source port associated with this source item
/// This is used to identify the specific port on the source device that this item refers to for advanced routing
/// </summary>
[JsonProperty("sourcePortKey")]
public string SourcePortKey { get; set; }
/// <summary>
/// Default constructor for SourceListItem, initializes the Icon to "Blank"
/// </summary>
@@ -177,7 +199,7 @@ namespace PepperDash.Essentials.Core
/// <summary>
/// Returns a string representation of the SourceListItem, including the SourceKey and Name
/// </summary>
/// <returns></returns>
/// <returns> A string representation of the SourceListItem</returns>
public override string ToString()
{
return $"{SourceKey}:{Name}";

View File

@@ -25,6 +25,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />

View File

@@ -0,0 +1,29 @@
using System.Collections.Generic;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Core.Routing
{
/// <summary>
/// The current sources for the room, keyed by eRoutingSignalType.
/// This allows for multiple sources to be tracked, such as audio and video.
/// </summary>
/// <remarks>
/// This interface is used to provide access to the current sources in a room,
/// allowing for more complex routing scenarios where multiple signal types are involved.
/// </remarks>
public interface ICurrentSources
{
/// <summary>
/// Gets the current sources for the room, keyed by eRoutingSignalType.
/// This dictionary contains the current source for each signal type, such as audio, video, and control signals.
/// </summary>
Dictionary<eRoutingSignalType, SourceListItem> CurrentSources { get; }
/// <summary>
/// Gets the current source keys for the room, keyed by eRoutingSignalType.
/// This dictionary contains the keys for the current source for each signal type, such as audio, video, and control signals.
/// </summary>
Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; }
}
}

View File

@@ -9,6 +9,8 @@ using PepperDash.Essentials.Core.Routing;
using PepperDash.Essentials.Core.Routing;
using PepperDash.Essentials.Core.Routing.Interfaces
*/
using System;
namespace PepperDash.Essentials.Core
{
/// <summary>
@@ -21,10 +23,24 @@ namespace PepperDash.Essentials.Core
/// <summary>
/// For rooms with a single presentation source, change event
/// </summary>
[Obsolete("Use ICurrentSources instead")]
public interface IHasCurrentSourceInfoChange
{
/// <summary>
/// The key for the current source info, used to look up the source in the SourceList
/// </summary>
string CurrentSourceInfoKey { get; set; }
/// <summary>
/// The current source info for the room, used to look up the source in the SourceList
/// </summary>
SourceListItem CurrentSourceInfo { get; set; }
/// <summary>
/// Event that is raised when the current source info changes.
/// This is used to notify the system of changes to the current source info.
/// The event handler receives the new source info and the type of change that occurred.
/// </summary>
event SourceInfoChangeHandler CurrentSourceChange;
}
}

View File

@@ -1,29 +1,29 @@
namespace PepperDash.Essentials.Core
using PepperDash.Essentials.Core.Routing;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// For fixed-source endpoint devices
/// </summary>
public interface IRoutingSink : IRoutingInputs, IHasCurrentSourceInfoChange
{
{
}
/// <summary>
/// For fixed-source endpoint devices with an input port
/// </summary>
public interface IRoutingSinkWithInputPort :IRoutingSink
public interface IRoutingSinkWithInputPort : IRoutingSink
{
/// <summary>
/// Gets the current input port for this routing sink.
/// </summary>
RoutingInputPort CurrentInputPort { get; }
}
/*/// <summary>
/// For fixed-source endpoint devices
/// </summary>
public interface IRoutingSink<TSelector> : IRoutingInputs<TSelector>, IHasCurrentSourceInfoChange
{
void UpdateRouteRequest<TOutputSelector>(RouteRequest<TSelector, TOutputSelector> request);
RouteRequest<TSelector, TOutputSelector> GetRouteRequest<TOutputSelector>();
}*/
/// <summary>
/// Interface for routing sinks that have access to the current source information.
/// </summary>
public interface IRoutingSinkWithCurrentSources : IRoutingSink, ICurrentSources
{
}
}

View File

@@ -0,0 +1,193 @@
using System;
using System.ComponentModel;
namespace PepperDash.Essentials.Core.Web.Attributes
{
/// <summary>
/// Base class for HTTP method attributes
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public abstract class HttpMethodAttribute : Attribute
{
public string Method { get; }
protected HttpMethodAttribute(string method)
{
Method = method;
}
}
/// <summary>
/// Indicates that a request handler supports HTTP GET operations
/// </summary>
public class HttpGetAttribute : HttpMethodAttribute
{
public HttpGetAttribute() : base("GET") { }
}
/// <summary>
/// Indicates that a request handler supports HTTP POST operations
/// </summary>
public class HttpPostAttribute : HttpMethodAttribute
{
public HttpPostAttribute() : base("POST") { }
}
/// <summary>
/// Indicates that a request handler supports HTTP PUT operations
/// </summary>
public class HttpPutAttribute : HttpMethodAttribute
{
public HttpPutAttribute() : base("PUT") { }
}
/// <summary>
/// Indicates that a request handler supports HTTP DELETE operations
/// </summary>
public class HttpDeleteAttribute : HttpMethodAttribute
{
public HttpDeleteAttribute() : base("DELETE") { }
}
/// <summary>
/// Provides OpenAPI operation metadata for a request handler
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class OpenApiOperationAttribute : Attribute
{
/// <summary>
/// A brief summary of what the operation does
/// </summary>
public string Summary { get; set; }
/// <summary>
/// A verbose explanation of the operation behavior
/// </summary>
public string Description { get; set; }
/// <summary>
/// Unique string used to identify the operation
/// </summary>
public string OperationId { get; set; }
/// <summary>
/// A list of tags for API documentation control
/// </summary>
public string[] Tags { get; set; }
public OpenApiOperationAttribute()
{
}
}
/// <summary>
/// Describes a response from an API operation
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class OpenApiResponseAttribute : Attribute
{
/// <summary>
/// The HTTP status code
/// </summary>
public int StatusCode { get; }
/// <summary>
/// A short description of the response
/// </summary>
public string Description { get; set; }
/// <summary>
/// The content type of the response
/// </summary>
public string ContentType { get; set; } = "application/json";
/// <summary>
/// The type that represents the response schema
/// </summary>
public Type Type { get; set; }
public OpenApiResponseAttribute(int statusCode)
{
StatusCode = statusCode;
}
}
/// <summary>
/// Indicates that an operation requires a request body
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class OpenApiRequestBodyAttribute : Attribute
{
/// <summary>
/// Determines if the request body is required
/// </summary>
public bool Required { get; set; } = true;
/// <summary>
/// The content type of the request body
/// </summary>
public string ContentType { get; set; } = "application/json";
/// <summary>
/// The type that represents the request body schema
/// </summary>
public Type Type { get; set; }
/// <summary>
/// Description of the request body
/// </summary>
public string Description { get; set; }
public OpenApiRequestBodyAttribute()
{
}
}
/// <summary>
/// Describes a parameter for the operation
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class OpenApiParameterAttribute : Attribute
{
/// <summary>
/// The name of the parameter
/// </summary>
public string Name { get; }
/// <summary>
/// The location of the parameter
/// </summary>
public ParameterLocation In { get; set; } = ParameterLocation.Path;
/// <summary>
/// Determines whether this parameter is mandatory
/// </summary>
public bool Required { get; set; } = true;
/// <summary>
/// A brief description of the parameter
/// </summary>
public string Description { get; set; }
/// <summary>
/// The type of the parameter
/// </summary>
public Type Type { get; set; } = typeof(string);
public OpenApiParameterAttribute(string name)
{
Name = name;
}
}
/// <summary>
/// The location of the parameter
/// </summary>
public enum ParameterLocation
{
Query,
Header,
Path,
Cookie
}
}

View File

@@ -210,6 +210,11 @@ namespace PepperDash.Essentials.Core.Web
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
});
AddRoute(new HttpCwsRoute("swagger") {
Name = "OpenAPI Documentation",
RouteHandler = new SwaggerHandler(_server.GetRouteCollection(), BasePath)
});
// If running on an appliance
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
{

View File

@@ -2,12 +2,22 @@
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
using System;
using Serilog.Events;
using Newtonsoft.Json.Converters;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[HttpPost]
[OpenApiOperation(
Summary = "AppDebug",
Description = "Get or set application debug level settings",
OperationId = "appDebug")]
[OpenApiRequestBody(Description = "Debug level configuration")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
public class AppDebugRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -4,6 +4,7 @@ using Crestron.SimplSharpPro.EthernetCommunication;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
using Serilog.Events;
using System;
using System.Collections.Generic;
@@ -13,7 +14,15 @@ using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
[HttpGet]
[HttpPost]
[OpenApiOperation(
Summary = "DebugSession",
Description = "Start or stop a WebSocket debug session",
OperationId = "debugSession")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
{
public DebugSessionRequestHandler()
: base(true)

View File

@@ -3,10 +3,20 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
using Serilog.Events;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpPost]
[OpenApiOperation(
Summary = "DevJson",
Description = "Send a command to a specific device",
OperationId = "sendDeviceCommand")]
[OpenApiParameter("deviceKey", Description = "The key of the device to send the command to")]
[OpenApiRequestBody(Description = "Device command data")]
[OpenApiResponse(200, Description = "Command executed successfully")]
[OpenApiResponse(400, Description = "Bad Request")]
public class DevJsonRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,17 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "DevList",
Description = "Retrieve a list of all devices in the system",
OperationId = "getDevices")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(404, Description = "Not Found")]
public class DevListRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -3,9 +3,19 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "DevMethods",
Description = "Retrieve available methods for a specific device",
OperationId = "getDeviceMethods")]
[OpenApiParameter("deviceKey", Description = "The key of the device")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Device not found")]
public class DevMethodsRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,19 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "DevProps",
Description = "Retrieve properties for a specific device",
OperationId = "getDeviceProperties")]
[OpenApiParameter("deviceKey", Description = "The key of the device")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Device not found")]
public class DevPropsRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -1,8 +1,15 @@
using Crestron.SimplSharp.WebScripting;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpPost]
[OpenApiOperation(
Summary = "DisableAllStreamDebug",
Description = "Disable stream debugging for all devices",
OperationId = "disableAllStreamDebug")]
[OpenApiResponse(200, Description = "Successful response")]
public class DisableAllStreamDebugRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,19 @@
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[HttpPost]
[OpenApiOperation(
Summary = "DoNotLoadConfigOnNextBoot",
Description = "Get or set flag to prevent configuration loading on next boot",
OperationId = "doNotLoadConfigOnNextBoot")]
[OpenApiRequestBody(Description = "Configuration loading flag")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
public class DoNotLoadConfigOnNextBootRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,19 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "GetFeedbacksForDeviceKey",
Description = "Get feedback values from a specific device",
OperationId = "getDeviceFeedbacks")]
[OpenApiParameter("deviceKey", Description = "The key of the device to get feedbacks from")]
[OpenApiResponse(200, Description = "Device feedback values")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Device not found")]
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -3,9 +3,19 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "GetJoinMapsForBridgeKey",
Description = "Retrieve all join maps for a specific bridge",
OperationId = "getJoinMapForBridge")]
[OpenApiParameter("bridgeKey", Description = "The key of the bridge")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Bridge not found")]
public class GetJoinMapForBridgeKeyRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,21 @@
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "GetJoinMapsForDeviceKey",
Description = "Retrieve join map for a specific device within a bridge",
OperationId = "getJoinMapForDevice")]
[OpenApiParameter("bridgeKey", Description = "The key of the bridge")]
[OpenApiParameter("deviceKey", Description = "The key of the device")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Bridge not found")]
[OpenApiResponse(500, Description = "Device join map not found")]
public class GetJoinMapForDeviceKeyRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,16 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "GetPaths",
Description = "Get available API paths and routes",
OperationId = "getApiPaths")]
[OpenApiResponse(200, Description = "Successful response")]
public class GetRoutesHandler:WebApiBaseRequestHandler
{
private HttpCwsRouteCollection routeCollection;

View File

@@ -1,13 +1,23 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
public class GetRoutingPortsHandler : WebApiBaseRequestHandler
[HttpGet]
[OpenApiOperation(
Summary = "Get Routing Ports for a device",
Description = "Retrieve routing input and output ports for a specific device",
OperationId = "getDeviceRoutingPorts")]
[OpenApiParameter("deviceKey", Description = "The key of the device")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Device not found")]
public class GetRoutingPortsHandler : WebApiBaseRequestHandler
{
public GetRoutingPortsHandler() : base(true) { }

View File

@@ -1,12 +1,19 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
using System.Linq;
using System.Text;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
public class GetTieLinesRequestHandler : WebApiBaseRequestHandler
[HttpGet]
[OpenApiOperation(
Summary = "Get TieLines",
Description = "Retrieve a list of all tie lines in the system",
OperationId = "getTieLines")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
public class GetTieLinesRequestHandler : WebApiBaseRequestHandler
{
public GetTieLinesRequestHandler() : base(true) { }

View File

@@ -2,9 +2,19 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "GetTypesByFilter",
Description = "Retrieve device types filtered by a specific category",
OperationId = "getDeviceTypesByFilter")]
[OpenApiParameter("filter", Description = "The filter criteria for device types")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Filtered device types not found")]
public class GetTypesByFilterRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,18 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "GetTypes",
Description = "Retrieve a list of all available device types",
OperationId = "getDeviceTypes")]
[OpenApiResponse(200, Description = "Successful response", ContentType = "application/json")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Device factory not found")]
public class GetTypesRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -3,9 +3,16 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpPost]
[OpenApiOperation(
Summary = "Load Config",
Description = "Load configuration",
OperationId = "loadConfig")]
[OpenApiResponse(200, Description = "Configuration load initiated successfully")]
public class LoadConfigRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,17 @@
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "ReportVersions",
Description = "Get version information for loaded assemblies",
OperationId = "getVersions")]
[OpenApiResponse(200, Description = "List of loaded assemblies with version information")]
[OpenApiResponse(500, Description = "Internal Server Error")]
public class ReportVersionsRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -3,9 +3,16 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpPost]
[OpenApiOperation(
Summary = "Restart Program",
Description = "Restart the program",
OperationId = "restartProgram")]
[OpenApiResponse(200, Description = "Program restart initiated successfully")]
public class RestartProgramRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -3,9 +3,20 @@ using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpPost]
[OpenApiOperation(
Summary = "SetDeviceStreamDebug",
Description = "Configure stream debugging settings for a device",
OperationId = "setDeviceStreamDebug")]
[OpenApiRequestBody(Description = "Device stream debug configuration")]
[OpenApiResponse(200, Description = "Successful response")]
[OpenApiResponse(400, Description = "Bad Request")]
[OpenApiResponse(404, Description = "Device not found")]
[OpenApiResponse(500, Description = "Internal Server Error")]
public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -2,9 +2,16 @@
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[HttpGet]
[OpenApiOperation(
Summary = "ShowConfig",
Description = "Retrieve the current system configuration",
OperationId = "getConfig")]
[OpenApiResponse(200, Description = "Current system configuration")]
public class ShowConfigRequestHandler : WebApiBaseRequestHandler
{
/// <summary>

View File

@@ -0,0 +1,470 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web.Attributes;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
public class SwaggerHandler : WebApiBaseRequestHandler
{
private HttpCwsRouteCollection routeCollection;
private string basePath;
public SwaggerHandler(HttpCwsRouteCollection routeCollection, string basePath)
{
this.routeCollection = routeCollection;
this.basePath = basePath;
}
protected override void HandleGet(HttpCwsContext context)
{
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var hostname = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
var serverUrl = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{basePath}"
: $"https://{currentIp}/cws{basePath}";
var openApiDoc = GenerateOpenApiDocument(serverUrl);
var response = JsonConvert.SerializeObject(openApiDoc, Formatting.Indented);
context.Response.StatusCode = 200;
context.Response.ContentType = "application/json";
context.Response.Headers.Add("Content-Type", "application/json");
context.Response.Write(response, false);
context.Response.End();
}
private object GenerateOpenApiDocument(string serverUrl)
{
var paths = new Dictionary<string, object>();
// Add paths based on existing routes
foreach (var route in routeCollection)
{
var pathKey = "/" + route.Url;
var pathItem = GeneratePathItem(route);
if (pathItem != null)
{
paths[pathKey] = pathItem;
}
}
return new
{
openapi = "3.0.3",
info = new
{
title = "PepperDash Essentials API",
description = "RESTful API for PepperDash Essentials control system",
version = "1.0.0",
contact = new
{
name = "PepperDash Technology",
url = "https://www.pepperdash.com"
}
},
servers = new[]
{
new { url = serverUrl, description = "Essentials API Server" }
},
paths = paths,
components = new
{
schemas = GetSchemas()
}
};
}
private object GeneratePathItem(HttpCwsRoute route)
{
if (route.RouteHandler == null) return null;
var handlerType = route.RouteHandler.GetType();
var operations = new Dictionary<string, object>();
// Get HTTP method attributes from the handler class
var httpMethodAttributes = handlerType.GetCustomAttributes(typeof(HttpMethodAttribute), false)
.Cast<HttpMethodAttribute>()
.ToList();
// If no HTTP method attributes found, fall back to the original logic
if (!httpMethodAttributes.Any())
{
httpMethodAttributes = DetermineHttpMethodsFromRoute(route);
}
foreach (var methodAttr in httpMethodAttributes)
{
var operation = GenerateOperation(route, methodAttr.Method, handlerType);
if (operation != null)
{
operations[methodAttr.Method.ToLower()] = operation;
}
}
return operations.Count > 0 ? operations : null;
}
private List<HttpMethodAttribute> DetermineHttpMethodsFromRoute(HttpCwsRoute route)
{
var methods = new List<HttpMethodAttribute>();
var routeName = route.Name?.ToLower() ?? "";
var routeUrl = route.Url?.ToLower() ?? "";
// Fallback logic for routes without attributes
if (routeName.Contains("get") || routeUrl.Contains("devices") || routeUrl.Contains("config") ||
routeUrl.Contains("versions") || routeUrl.Contains("types") || routeUrl.Contains("tielines") ||
routeUrl.Contains("apipaths") || routeUrl.Contains("feedbacks") || routeUrl.Contains("properties") ||
routeUrl.Contains("methods") || routeUrl.Contains("joinmap") || routeUrl.Contains("routingports"))
{
methods.Add(new HttpGetAttribute());
}
if (routeName.Contains("command") || routeName.Contains("restart") || routeName.Contains("load") ||
routeName.Contains("debug") || routeName.Contains("disable"))
{
methods.Add(new HttpPostAttribute());
}
return methods;
}
private object GenerateOperation(HttpCwsRoute route, string method, Type handlerType)
{
var operation = new Dictionary<string, object>();
// Get OpenApiOperation attribute
var operationAttr = handlerType.GetCustomAttribute<OpenApiOperationAttribute>();
if (operationAttr != null)
{
operation["summary"] = operationAttr.Summary ?? route.Name ?? "API Operation";
operation["operationId"] = operationAttr.OperationId ?? route.Name?.Replace(" ", "") ?? "operation";
if (!string.IsNullOrEmpty(operationAttr.Description))
{
operation["description"] = operationAttr.Description;
}
if (operationAttr.Tags != null && operationAttr.Tags.Length > 0)
{
operation["tags"] = operationAttr.Tags;
}
}
else
{
// Fallback to route name
operation["summary"] = route.Name ?? "API Operation";
operation["operationId"] = route.Name?.Replace(" ", "") ?? "operation";
// Add fallback description
var fallbackDescription = GetFallbackDescription(route);
if (!string.IsNullOrEmpty(fallbackDescription))
{
operation["description"] = fallbackDescription;
}
}
// Get response attributes
var responses = new Dictionary<string, object>();
var responseAttrs = handlerType.GetCustomAttributes<OpenApiResponseAttribute>().ToList();
if (responseAttrs.Any())
{
foreach (var responseAttr in responseAttrs)
{
var responseObj = new Dictionary<string, object>
{
["description"] = responseAttr.Description ?? "Response"
};
if (!string.IsNullOrEmpty(responseAttr.ContentType))
{
responseObj["content"] = new Dictionary<string, object>
{
[responseAttr.ContentType] = new { schema = new { type = "object" } }
};
}
responses[responseAttr.StatusCode.ToString()] = responseObj;
}
}
else
{
// Default responses
responses["200"] = new
{
description = "Successful response",
content = new Dictionary<string, object>
{
["application/json"] = new { schema = new { type = "object" } }
}
};
responses["400"] = new { description = "Bad Request" };
responses["404"] = new { description = "Not Found" };
responses["500"] = new { description = "Internal Server Error" };
}
operation["responses"] = responses;
// Get parameter attributes
var parameterAttrs = handlerType.GetCustomAttributes<OpenApiParameterAttribute>().ToList();
var parameters = new List<object>();
// Add parameters from attributes
foreach (var paramAttr in parameterAttrs)
{
parameters.Add(new
{
name = paramAttr.Name,
@in = paramAttr.In.ToString().ToLower(),
required = paramAttr.Required,
schema = new { type = GetSchemaType(paramAttr.Type) },
description = paramAttr.Description ?? $"The {paramAttr.Name} parameter"
});
}
// Add parameters from URL path variables (fallback)
if (route.Url.Contains("{"))
{
var url = route.Url;
while (url.Contains("{"))
{
var start = url.IndexOf("{");
var end = url.IndexOf("}", start);
if (end > start)
{
var paramName = url.Substring(start + 1, end - start - 1);
// Only add if not already added from attributes
if (!parameters.Any(p => ((dynamic)p).name == paramName))
{
parameters.Add(new
{
name = paramName,
@in = "path",
required = true,
schema = new { type = "string" },
description = $"The {paramName} parameter"
});
}
url = url.Substring(end + 1);
}
else break;
}
}
if (parameters.Count > 0)
{
operation["parameters"] = parameters;
}
// Get request body attribute for POST operations
if (method == "POST")
{
var requestBodyAttr = handlerType.GetCustomAttribute<OpenApiRequestBodyAttribute>();
if (requestBodyAttr != null)
{
operation["requestBody"] = new
{
required = requestBodyAttr.Required,
description = requestBodyAttr.Description,
content = new Dictionary<string, object>
{
[requestBodyAttr.ContentType] = new Dictionary<string, object>
{
["schema"] = requestBodyAttr.Type != null
? (object)new Dictionary<string, object> { ["$ref"] = $"#/components/schemas/{requestBodyAttr.Type.Name}" }
: new Dictionary<string, object> { ["type"] = "object" }
}
}
};
}
else if (route.Name != null && route.Name.Contains("Command"))
{
// Fallback for command routes
operation["requestBody"] = new
{
required = true,
content = new Dictionary<string, object>
{
["application/json"] = new
{
schema = new Dictionary<string, object> { ["$ref"] = "#/components/schemas/DeviceCommand" }
}
}
};
}
}
return operation;
}
private string GetSchemaType(Type type)
{
if (type == typeof(string)) return "string";
if (type == typeof(int) || type == typeof(long)) return "integer";
if (type == typeof(bool)) return "boolean";
if (type == typeof(double) || type == typeof(float)) return "number";
return "string"; // default
}
private string GetFallbackDescription(HttpCwsRoute route)
{
var routeName = route.Name?.ToLower() ?? "";
var routeUrl = route.Url?.ToLower() ?? "";
if (routeUrl.Contains("devices") && !routeUrl.Contains("{"))
{
return "Retrieve a list of all devices in the system";
}
else if (routeUrl.Contains("versions"))
{
return "Get version information for loaded assemblies";
}
else if (routeUrl.Contains("config"))
{
return "Retrieve the current system configuration";
}
else if (routeUrl.Contains("devicecommands"))
{
return "Send a command to a specific device";
}
else if (routeUrl.Contains("devicefeedbacks"))
{
return "Get feedback values from a specific device";
}
else if (routeUrl.Contains("deviceproperties"))
{
return "Get properties of a specific device";
}
else if (routeUrl.Contains("devicemethods"))
{
return "Get available methods for a specific device";
}
else if (routeUrl.Contains("types"))
{
return routeUrl.Contains("{") ? "Get types filtered by the specified filter" : "Get all available types";
}
else if (routeUrl.Contains("tielines"))
{
return "Get information about tielines in the system";
}
else if (routeUrl.Contains("joinmap"))
{
return "Get join map information for bridge or device";
}
else if (routeUrl.Contains("routingports"))
{
return "Get routing ports for a specific device";
}
else if (routeUrl.Contains("apipaths"))
{
return "Get available API paths and routes";
}
else if (routeName.Contains("restart"))
{
return "Restart the program";
}
else if (routeName.Contains("debug"))
{
return "Debug operation";
}
return null;
}
private Dictionary<string, object> GetSchemas()
{
return new Dictionary<string, object>
{
["DeviceCommand"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["deviceKey"] = new { type = "string", description = "The key of the device" },
["methodName"] = new { type = "string", description = "The method to call on the device" },
["params"] = new { type = "array", items = new { type = "object" }, description = "Parameters for the method call" }
},
required = new[] { "deviceKey", "methodName" }
},
["Device"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["key"] = new { type = "string", description = "Device key" },
["name"] = new { type = "string", description = "Device name" },
["type"] = new { type = "string", description = "Device type" },
["isOnline"] = new { type = "boolean", description = "Device online status" }
}
},
["Feedback"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["BoolValues"] = new { type = "array", items = new Dictionary<string, object> { ["$ref"] = "#/components/schemas/BoolFeedback" } },
["IntValues"] = new { type = "array", items = new Dictionary<string, object> { ["$ref"] = "#/components/schemas/IntFeedback" } },
["SerialValues"] = new { type = "array", items = new Dictionary<string, object> { ["$ref"] = "#/components/schemas/StringFeedback" } }
}
},
["BoolFeedback"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["FeedbackKey"] = new { type = "string" },
["Value"] = new { type = "boolean" }
}
},
["IntFeedback"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["FeedbackKey"] = new { type = "string" },
["Value"] = new { type = "integer" }
}
},
["StringFeedback"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["FeedbackKey"] = new { type = "string" },
["Value"] = new { type = "string" }
}
},
["ApiRoutes"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["url"] = new { type = "string", description = "Base URL for the API" },
["routes"] = new { type = "array", items = new Dictionary<string, object> { ["$ref"] = "#/components/schemas/Route" } }
}
},
["Route"] = new
{
type = "object",
properties = new Dictionary<string, object>
{
["name"] = new { type = "string", description = "Route name" },
["url"] = new { type = "string", description = "Route URL pattern" }
}
}
};
}
}
}

View File

@@ -1,110 +1,199 @@
using Crestron.SimplSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using PepperDash.Essentials.Core.Routing;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using Feedback = PepperDash.Essentials.Core.Feedback;
namespace PepperDash.Essentials.Devices.Common.Displays
{
public abstract class DisplayBase : EssentialsDevice, IDisplay
/// <summary>
/// Abstract base class for display devices that provides common display functionality
/// including power control, input switching, and routing capabilities.
/// </summary>
public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources
{
private RoutingInputPort _currentInputPort;
public RoutingInputPort CurrentInputPort
{
get
{
return _currentInputPort;
}
private RoutingInputPort _currentInputPort;
protected set
{
if (_currentInputPort == value) return;
/// <summary>
/// Gets or sets the current input port that is selected on the display.
/// </summary>
public RoutingInputPort CurrentInputPort
{
get
{
return _currentInputPort;
}
_currentInputPort = value;
protected set
{
if (_currentInputPort == value) return;
InputChanged?.Invoke(this, _currentInputPort);
}
}
_currentInputPort = value;
public event InputChangedEventHandler InputChanged;
InputChanged?.Invoke(this, _currentInputPort);
}
}
public event SourceInfoChangeHandler CurrentSourceChange;
/// <summary>
/// Event that is raised when the input changes on the display.
/// </summary>
public event InputChangedEventHandler InputChanged;
public string CurrentSourceInfoKey { get; set; }
public SourceListItem CurrentSourceInfo
{
get
{
return _CurrentSourceInfo;
}
set
{
if (value == _CurrentSourceInfo) return;
/// <summary>
/// Event that is raised when the current source information changes.
/// </summary>
public event SourceInfoChangeHandler CurrentSourceChange;
var handler = CurrentSourceChange;
/// <summary>
/// Gets or sets the key of the current source information.
/// </summary>
public string CurrentSourceInfoKey { get; set; }
if (handler != null)
handler(_CurrentSourceInfo, ChangeType.WillChange);
/// <summary>
/// Gets or sets the current source information for the display.
/// </summary>
public SourceListItem CurrentSourceInfo
{
get
{
return _CurrentSourceInfo;
}
set
{
if (value == _CurrentSourceInfo) return;
_CurrentSourceInfo = value;
var handler = CurrentSourceChange;
if (handler != null)
handler(_CurrentSourceInfo, ChangeType.DidChange);
}
}
SourceListItem _CurrentSourceInfo;
if (handler != null)
handler(_CurrentSourceInfo, ChangeType.WillChange);
_CurrentSourceInfo = value;
if (handler != null)
handler(_CurrentSourceInfo, ChangeType.DidChange);
}
}
SourceListItem _CurrentSourceInfo;
/// <inheritdoc/>
public Dictionary<eRoutingSignalType, SourceListItem> CurrentSources { get; private set; }
/// <inheritdoc/>
public Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; private set; }
/// <summary>
/// Gets feedback indicating whether the display is currently cooling down after being powered off.
/// </summary>
public BoolFeedback IsCoolingDownFeedback { get; protected set; }
/// <summary>
/// Gets feedback indicating whether the display is currently warming up after being powered on.
/// </summary>
public BoolFeedback IsWarmingUpFeedback { get; private set; }
public UsageTracking UsageTracker { get; set; }
/// <summary>
/// Gets or sets the usage tracking instance for monitoring display usage statistics.
/// </summary>
public UsageTracking UsageTracker { get; set; }
/// <summary>
/// Gets or sets the warmup time in milliseconds for the display to become ready after power on.
/// </summary>
public uint WarmupTime { get; set; }
/// <summary>
/// Gets or sets the cooldown time in milliseconds for the display to fully power down.
/// </summary>
public uint CooldownTime { get; set; }
/// <summary>
/// Bool Func that will provide a value for the PowerIsOn Output. Must be implemented
/// by concrete sub-classes
/// Abstract function that must be implemented by derived classes to provide the cooling down feedback value.
/// Must be implemented by concrete sub-classes.
/// </summary>
abstract protected Func<bool> IsCoolingDownFeedbackFunc { get; }
abstract protected Func<bool> IsWarmingUpFeedbackFunc { get; }
/// <summary>
/// Abstract function that must be implemented by derived classes to provide the warming up feedback value.
/// Must be implemented by concrete sub-classes.
/// </summary>
abstract protected Func<bool> IsWarmingUpFeedbackFunc { get; }
/// <summary>
/// Timer used for managing display warmup timing.
/// </summary>
protected CTimer WarmupTimer;
/// <summary>
/// Timer used for managing display cooldown timing.
/// </summary>
protected CTimer CooldownTimer;
#region IRoutingInputs Members
/// <summary>
/// Gets the collection of input ports available on this display device.
/// </summary>
public RoutingPortCollection<RoutingInputPort> InputPorts { get; private set; }
#endregion
protected DisplayBase(string key, string name)
: base(key, name)
/// <summary>
/// Initializes a new instance of the DisplayBase class.
/// </summary>
/// <param name="key">The unique key identifier for this display device.</param>
/// <param name="name">The friendly name for this display device.</param>
protected DisplayBase(string key, string name)
: base(key, name)
{
IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc);
IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc);
InputPorts = new RoutingPortCollection<RoutingInputPort>();
CurrentSources = new Dictionary<eRoutingSignalType, SourceListItem>
{
{ eRoutingSignalType.Audio, null },
{ eRoutingSignalType.Video, null },
};
CurrentSourceKeys = new Dictionary<eRoutingSignalType, string>
{
{ eRoutingSignalType.Audio, string.Empty },
{ eRoutingSignalType.Video, string.Empty },
};
}
/// <summary>
/// Powers on the display device. Must be implemented by derived classes.
/// </summary>
public abstract void PowerOn();
/// <summary>
/// Powers off the display device. Must be implemented by derived classes.
/// </summary>
public abstract void PowerOff();
/// <summary>
/// Toggles the power state of the display device. Must be implemented by derived classes.
/// </summary>
public abstract void PowerToggle();
public virtual FeedbackCollection<Feedback> Feedbacks
/// <summary>
/// Gets the collection of feedback objects for this display device.
/// </summary>
public virtual FeedbackCollection<Feedback> Feedbacks
{
get
{
return new FeedbackCollection<Feedback>
return new FeedbackCollection<Feedback>
{
IsCoolingDownFeedback,
IsWarmingUpFeedback
@@ -112,30 +201,50 @@ namespace PepperDash.Essentials.Devices.Common.Displays
}
}
public abstract void ExecuteSwitch(object selector);
/// <summary>
/// Executes a switch to the specified input on the display device. Must be implemented by derived classes.
/// </summary>
/// <param name="selector">The selector object that identifies which input to switch to.</param>
public abstract void ExecuteSwitch(object selector);
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey,
EiscApiAdvanced bridge)
{
var joinMap = new DisplayControllerJoinMap(joinStart);
/// <summary>
/// Links the display device to an API using a trilist, join start, join map key, and bridge.
/// This overload uses serialized join map configuration.
/// </summary>
/// <param name="displayDevice">The display device to link.</param>
/// <param name="trilist">The BasicTriList for communication.</param>
/// <param name="joinStart">The starting join number for the device.</param>
/// <param name="joinMapKey">The key for the join map configuration.</param>
/// <param name="bridge">The EISC API bridge instance.</param>
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey,
EiscApiAdvanced bridge)
{
var joinMap = new DisplayControllerJoinMap(joinStart);
var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
if (!string.IsNullOrEmpty(joinMapSerialized))
joinMap = JsonConvert.DeserializeObject<DisplayControllerJoinMap>(joinMapSerialized);
if (!string.IsNullOrEmpty(joinMapSerialized))
joinMap = JsonConvert.DeserializeObject<DisplayControllerJoinMap>(joinMapSerialized);
if (bridge != null)
{
bridge.AddJoinMap(Key, joinMap);
}
else
{
Debug.LogMessage(LogEventLevel.Information,this,"Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
}
if (bridge != null)
{
bridge.AddJoinMap(Key, joinMap);
}
else
{
Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
}
LinkDisplayToApi(displayDevice, trilist, joinMap);
}
}
/// <summary>
/// Links the display device to an API using a trilist and join map.
/// This overload uses a pre-configured join map instance.
/// </summary>
/// <param name="displayDevice">The display device to link.</param>
/// <param name="trilist">The BasicTriList for communication.</param>
/// <param name="joinMap">The join map configuration for the device.</param>
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, DisplayControllerJoinMap joinMap)
{
Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X"));
@@ -268,68 +377,96 @@ namespace PepperDash.Essentials.Devices.Common.Displays
volumeDisplayWithFeedback.MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]);
}
}
}
public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback
/// <summary>
/// Abstract base class for two-way display devices that provide feedback capabilities.
/// Extends DisplayBase with routing feedback and power control feedback functionality.
/// </summary>
public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback
{
public StringFeedback CurrentInputFeedback { get; private set; }
/// <summary>
/// Gets feedback for the current input selection on the display.
/// </summary>
public StringFeedback CurrentInputFeedback { get; private set; }
abstract protected Func<string> CurrentInputFeedbackFunc { get; }
/// <summary>
/// Abstract function that must be implemented by derived classes to provide the current input feedback value.
/// Must be implemented by concrete sub-classes.
/// </summary>
abstract protected Func<string> CurrentInputFeedbackFunc { get; }
public BoolFeedback PowerIsOnFeedback { get; protected set; }
/// <summary>
/// Gets feedback indicating whether the display is currently powered on.
/// </summary>
public BoolFeedback PowerIsOnFeedback { get; protected set; }
abstract protected Func<bool> PowerIsOnFeedbackFunc { get; }
/// <summary>
/// Abstract function that must be implemented by derived classes to provide the power state feedback value.
/// Must be implemented by concrete sub-classes.
/// </summary>
abstract protected Func<bool> PowerIsOnFeedbackFunc { get; }
public static MockDisplay DefaultDisplay
{
get
/// <summary>
/// Gets the default mock display instance for testing and development purposes.
/// </summary>
public static MockDisplay DefaultDisplay
{
get
{
if (_DefaultDisplay == null)
_DefaultDisplay = new MockDisplay("default", "Default Display");
return _DefaultDisplay;
}
}
}
static MockDisplay _DefaultDisplay;
/// <summary>
/// Initializes a new instance of the TwoWayDisplayBase class.
/// </summary>
/// <param name="key">The unique key identifier for this display device.</param>
/// <param name="name">The friendly name for this display device.</param>
public TwoWayDisplayBase(string key, string name)
: base(key, name)
{
CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc);
CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc);
WarmupTime = 7000;
CooldownTime = 15000;
PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc);
PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc);
Feedbacks.Add(CurrentInputFeedback);
Feedbacks.Add(PowerIsOnFeedback);
Feedbacks.Add(CurrentInputFeedback);
Feedbacks.Add(PowerIsOnFeedback);
PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange;
PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange;
}
void PowerIsOnFeedback_OutputChange(object sender, EventArgs e)
{
if (UsageTracker != null)
{
if (PowerIsOnFeedback.BoolValue)
UsageTracker.StartDeviceUsage();
else
UsageTracker.EndDeviceUsage();
}
}
void PowerIsOnFeedback_OutputChange(object sender, EventArgs e)
{
if (UsageTracker != null)
{
if (PowerIsOnFeedback.BoolValue)
UsageTracker.StartDeviceUsage();
else
UsageTracker.EndDeviceUsage();
}
}
public event EventHandler<RoutingNumericEventArgs> NumericSwitchChange;
/// <summary>
/// Event that is raised when a numeric switch change occurs on the display.
/// </summary>
public event EventHandler<RoutingNumericEventArgs> NumericSwitchChange;
/// <summary>
/// Raise an event when the status of a switch object changes.
/// </summary>
/// <param name="e">Arguments defined as IKeyName sender, output, input, and eRoutingSignalType</param>
protected void OnSwitchChange(RoutingNumericEventArgs e)
{
var newEvent = NumericSwitchChange;
if (newEvent != null) newEvent(this, e);
}
/// <summary>
/// Raise an event when the status of a switch object changes.
/// </summary>
/// <param name="e">Arguments defined as IKeyName sender, output, input, and eRoutingSignalType</param>
protected void OnSwitchChange(RoutingNumericEventArgs e)
{
var newEvent = NumericSwitchChange;
if (newEvent != null) newEvent(this, e);
}
}
}

View File

@@ -1,4 +1,8 @@
using Crestron.SimplSharpPro;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
using Crestron.SimplSharpPro.UI;
using Newtonsoft.Json;
@@ -10,64 +14,105 @@ using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.DeviceInfo;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using PepperDash.Essentials.Core.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using Feedback = PepperDash.Essentials.Core.Feedback;
namespace PepperDash.Essentials.Touchpanel
{
//public interface IMobileControlTouchpanelController
//{
// StringFeedback AppUrlFeedback { get; }
// string DefaultRoomKey { get; }
// string DeviceKey { get; }
//}
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlTouchpanelController, ITheme
/// <summary>
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
/// and mobile control functionality for Crestron touchpanels.
/// </summary>
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
{
private readonly MobileControlTouchpanelProperties localConfig;
private IMobileControlRoomMessenger _bridge;
private string _appUrl;
/// <summary>
/// Gets feedback for the current application URL.
/// </summary>
public StringFeedback AppUrlFeedback { get; private set; }
private readonly StringFeedback QrCodeUrlFeedback;
private readonly StringFeedback McServerUrlFeedback;
private readonly StringFeedback UserCodeFeedback;
private readonly BoolFeedback _appOpenFeedback;
/// <summary>
/// Gets feedback indicating whether an application is currently open on the touchpanel.
/// </summary>
public BoolFeedback AppOpenFeedback => _appOpenFeedback;
private readonly BoolFeedback _zoomIncomingCallFeedback;
/// <summary>
/// Gets feedback indicating whether there is an incoming Zoom call.
/// </summary>
public BoolFeedback ZoomIncomingCallFeedback => _zoomIncomingCallFeedback;
private readonly BoolFeedback _zoomInCallFeedback;
/// <summary>
/// Event that is raised when device information changes.
/// </summary>
public event DeviceInfoChangeHandler DeviceInfoChanged;
/// <summary>
/// Gets feedback indicating whether a Zoom call is currently active.
/// </summary>
public BoolFeedback ZoomInCallFeedback => _zoomInCallFeedback;
/// <summary>
/// Gets the collection of feedback objects for this touchpanel controller.
/// </summary>
public FeedbackCollection<Feedback> Feedbacks { get; private set; }
/// <summary>
/// Gets the collection of Zoom-related feedback objects.
/// </summary>
public FeedbackCollection<Feedback> ZoomFeedbacks { get; private set; }
/// <summary>
/// Gets the default room key for this touchpanel controller.
/// </summary>
public string DefaultRoomKey => _config.DefaultRoomKey;
/// <summary>
/// Gets a value indicating whether to use direct server communication.
/// </summary>
public bool UseDirectServer => localConfig.UseDirectServer;
/// <summary>
/// Gets a value indicating whether this touchpanel acts as a Zoom Room controller.
/// </summary>
public bool ZoomRoomController => localConfig.ZoomRoomController;
/// <summary>
/// Gets the current theme for the touchpanel interface.
/// </summary>
public string Theme => localConfig.Theme;
/// <summary>
/// Gets feedback for the current theme setting.
/// </summary>
public StringFeedback ThemeFeedback { get; private set; }
/// <summary>
/// Gets device information including MAC address and IP address.
/// </summary>
public DeviceInfo DeviceInfo => new DeviceInfo();
public ReadOnlyCollection<ConnectedIpInformation> ConnectedIps => Panel.ConnectedIpList;
/// <summary>
/// Initializes a new instance of the MobileControlTouchpanelController class.
/// </summary>
/// <param name="key">The unique key identifier for this touchpanel controller.</param>
/// <param name="name">The friendly name for this touchpanel controller.</param>
/// <param name="panel">The touchpanel hardware device.</param>
/// <param name="config">The configuration properties for this controller.</param>
public MobileControlTouchpanelController(string key, string name, BasicTriListWithSmartObject panel, MobileControlTouchpanelProperties config) : base(key, name, panel, config)
{
localConfig = config;
@@ -139,6 +184,10 @@ namespace PepperDash.Essentials.Touchpanel
RegisterForExtenders();
}
/// <summary>
/// Updates the theme setting for this touchpanel controller and persists the change to configuration.
/// </summary>
/// <param name="theme">The new theme identifier to apply.</param>
public void UpdateTheme(string theme)
{
localConfig.Theme = theme;
@@ -271,6 +320,11 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Performs custom activation setup for the touchpanel controller, including
/// registering messengers and linking to mobile control.
/// </summary>
/// <returns>True if activation was successful; otherwise, false.</returns>
public override bool CustomActivate()
{
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
@@ -300,12 +354,20 @@ namespace PepperDash.Essentials.Touchpanel
return base.CustomActivate();
}
/// <summary>
/// Handles device extender signal changes for system reserved signals.
/// </summary>
/// <param name="currentDeviceExtender">The device extender that generated the signal change.</param>
/// <param name="args">The signal event arguments containing the changed signal information.</param>
protected override void ExtenderSystemReservedSigs_DeviceExtenderSigChange(DeviceExtender currentDeviceExtender, SigEventArgs args)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"System Device Extender args: ${args.Event}:${args.Sig}");
}
/// <summary>
/// Sets up the panel drivers and signal mappings for the specified room.
/// </summary>
/// <param name="roomKey">The room key to configure the panel drivers for.</param>
protected override void SetupPanelDrivers(string roomKey)
{
AppUrlFeedback.LinkInputSig(Panel.StringInput[1]);
@@ -366,6 +428,10 @@ namespace PepperDash.Essentials.Touchpanel
SetAppUrl(_bridge.AppUrl);
}
/// <summary>
/// Sets the application URL and updates the corresponding feedback.
/// </summary>
/// <param name="url">The new application URL to set.</param>
public void SetAppUrl(string url)
{
_appUrl = url;
@@ -391,6 +457,9 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Hides the currently open application on the touchpanel.
/// </summary>
public void HideOpenApp()
{
if (Panel is TswX70Base x70Panel)
@@ -406,6 +475,9 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Opens an application on the touchpanel. Note: X60 panels do not support Zoom app opening.
/// </summary>
public void OpenApp()
{
if (Panel is TswX70Base x70Panel)
@@ -421,6 +493,9 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Closes the currently open application on the touchpanel.
/// </summary>
public void CloseOpenApp()
{
if (Panel is TswX70Base x70Panel)
@@ -436,6 +511,9 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Ends the current Zoom call on the touchpanel.
/// </summary>
public void EndZoomCall()
{
if (Panel is TswX70Base x70Panel)
@@ -451,6 +529,10 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Updates the device information (MAC address and IP address) from the touchpanel
/// and raises the DeviceInfoChanged event.
/// </summary>
public void UpdateDeviceInfo()
{
if (Panel is TswXX70Base x70Panel)
@@ -487,14 +569,27 @@ namespace PepperDash.Essentials.Touchpanel
}
}
/// <summary>
/// Factory class for creating MobileControlTouchpanelController instances from device configuration.
/// Supports various Crestron touchpanel models including TSW, TS, CrestronApp, XPanel, and DGE series.
/// </summary>
public class MobileControlTouchpanelControllerFactory : EssentialsPluginDeviceFactory<MobileControlTouchpanelController>
{
/// <summary>
/// Initializes a new instance of the MobileControlTouchpanelControllerFactory class.
/// Sets up supported device type names and minimum framework version requirements.
/// </summary>
public MobileControlTouchpanelControllerFactory()
{
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
MinimumEssentialsFrameworkVersion = "2.0.0";
}
/// <summary>
/// Builds a MobileControlTouchpanelController device from the provided device configuration.
/// </summary>
/// <param name="dc">The device configuration containing the device properties and settings.</param>
/// <returns>A configured MobileControlTouchpanelController instance.</returns>
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
var comm = CommFactory.GetControlPropertiesConfig(dc);
@@ -557,7 +652,7 @@ namespace PepperDash.Essentials.Touchpanel
return new Ts1070(id, Global.ControlSystem);
else if (type == "dge1000")
return new Dge1000(id, Global.ControlSystem);
else
else
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "WARNING: Cannot create TSW controller with type '{0}'", type);

View File

@@ -1,4 +1,10 @@
using Crestron.SimplSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
@@ -9,12 +15,6 @@ using PepperDash.Essentials.Core.Web;
using PepperDash.Essentials.RoomBridges;
using PepperDash.Essentials.WebApiHandlers;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using WebSocketSharp;
using WebSocketSharp.Net;
using WebSocketSharp.Server;
@@ -60,7 +60,7 @@ namespace PepperDash.Essentials.WebSocketServer
private string lanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter));
private System.Net.IPAddress csIpAddress;
private System.Net.IPAddress csIpAddress;
private System.Net.IPAddress csSubnetMask;
@@ -122,7 +122,7 @@ namespace PepperDash.Essentials.WebSocketServer
_parent = parent;
// Set the default port to be 50000 plus the slot number of the program
Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
if (customPort != 0)
{
@@ -156,9 +156,9 @@ namespace PepperDash.Essentials.WebSocketServer
}
try
{
{
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
var csIpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
this.csSubnetMask = System.Net.IPAddress.Parse(csSubnetMask);
@@ -298,8 +298,6 @@ namespace PepperDash.Essentials.WebSocketServer
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
this.LogVerbose("Processor IP: {processorIp}", processorIp);
foreach (var touchpanel in touchpanels.Select(tp =>
{
var token = _secret.Tokens.FirstOrDefault((t) => t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase));
@@ -321,11 +319,25 @@ namespace PepperDash.Essentials.WebSocketServer
continue;
}
var appUrl = $"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
string ip = processorIp;
if (touchpanel.Touchpanel is IMobileControlCrestronTouchpanelController crestronTouchpanel)
{
ip = crestronTouchpanel.ConnectedIps.Any(ipInfo =>
{
if (System.Net.IPAddress.TryParse(ipInfo.DeviceIpAddress, out var parsedIp))
{
return csIpAddress.IsInSameSubnet(parsedIp, csSubnetMask);
}
this.LogWarning("Invalid IP address: {deviceIpAddress}", ipInfo.DeviceIpAddress);
return false;
}) ? csIpAddress.ToString() : processorIp;
}
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
this.LogVerbose("Sending URL {appUrl}", appUrl);
touchpanel.Messenger.UpdateAppUrl($"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
touchpanel.Messenger.UpdateAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
}
}
@@ -349,7 +361,7 @@ namespace PepperDash.Essentials.WebSocketServer
if (!Directory.Exists($"{userAppPath}{localConfigFolderName}"))
{
Directory.CreateDirectory($"{userAppPath}{localConfigFolderName}");
}
}
using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigFileName}", FileMode.Create, FileAccess.ReadWrite)))
{
@@ -358,7 +370,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogDebug("LAN Adapter ID: {lanAdapterId}", lanAdapterId);
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
var config = GetApplicationConfig(processorIp);
@@ -378,7 +390,7 @@ namespace PepperDash.Essentials.WebSocketServer
return;
}
if(csAdapterId == -1)
if (csAdapterId == -1)
{
this.LogDebug("CS LAN Adapter not found");
return;
@@ -389,8 +401,8 @@ namespace PepperDash.Essentials.WebSocketServer
using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigCsFileName}", FileMode.Create, FileAccess.ReadWrite)))
{
// Write the CS application configuration file. Used when a request comes in for the application config from the CS
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
var config = GetApplicationConfig(processorIp);
var contents = JsonConvert.SerializeObject(config, Formatting.Indented);
@@ -400,7 +412,7 @@ namespace PepperDash.Essentials.WebSocketServer
}
private MobileControlApplicationConfig GetApplicationConfig(string processorIp)
{
{
try
{
var config = new MobileControlApplicationConfig
@@ -430,10 +442,10 @@ namespace PepperDash.Essentials.WebSocketServer
}
catch (Exception ex)
{
this.LogError(ex, "Error getting application configuration");
this.LogError(ex, "Error getting application configuration");
return null;
}
}
}
/// <summary>
@@ -572,7 +584,7 @@ namespace PepperDash.Essentials.WebSocketServer
var values = s.Split(' ');
if(values.Length < 2)
if (values.Length < 2)
{
CrestronConsole.ConsoleCommandResponse("Invalid number of arguments. Please provide a room key and a grant code");
return;