Compare commits

..

7 Commits

Author SHA1 Message Date
Andrew Welker
592607f3c8 Merge pull request #1296 from PepperDash/feature/add-IHasCamerasMessenger 2025-07-24 18:53:05 -05:00
Neil Dorin
ea0a779f8b Merge branch 'feature/add-IHasCamerasMessenger' of https://github.com/PepperDash/Essentials into feature/add-IHasCamerasMessenger 2025-07-24 16:40:06 -06:00
Neil Dorin
86e4d2f7fb feat: Update SendFullStatus to target specific clients
Modified the `SendFullStatus` method to accept a `string clientId` parameter, allowing it to send status messages to specific clients. Updated the action for `"/fullStatus"` to pass the client ID and adjusted the `PostStatusMessage` call accordingly.
2025-07-24 16:39:28 -06:00
Neil Dorin
0069233e13 Update src/PepperDash.Essentials.Devices.Common/Cameras/CameraControl.cs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-24 16:16:05 -06:00
Neil Dorin
4048efb07e Merge branch 'main' into feature/add-IHasCamerasMessenger 2025-07-24 16:03:41 -06:00
Neil Dorin
f8455d4110 feat: Refactor IHasCamerasMessenger constructor parameters
Updated the constructor of `IHasCamerasMessenger` to reorder parameters, placing `IHasCameras cameraController` last. Added logic in `MobileControlSystemController.cs` to instantiate and add `IHasCamerasMessenger` for devices implementing the `IHasCameras` interface.
2025-07-17 11:14:32 -06:00
Neil Dorin
f006ed0076 feat: Add camera control interfaces and messenger classes
This commit introduces new interfaces in `CameraControl.cs` for various camera functionalities, including muting, panning, tilting, zooming, and auto modes. The `IHasCameras` interface is expanded to manage camera lists and selections, with the addition of `CameraSelectedEventArgs` for event handling.

In `CameraBaseMessenger.cs`, a new `IHasCamerasMessenger` class is created to facilitate communication for devices implementing the `IHasCameras` interface, along with the `IHasCamerasStateMessage` class to represent the state of camera devices. These changes enhance the overall camera control capabilities and improve interaction management within the application.
2025-07-16 16:54:57 -06:00
29 changed files with 290 additions and 865 deletions

View File

@@ -25,7 +25,6 @@
</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

@@ -1,193 +0,0 @@
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,11 +210,6 @@ 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,22 +2,12 @@
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,7 +4,6 @@ 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;
@@ -14,15 +13,7 @@ using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
[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 class DebugSessionRequestHandler : WebApiBaseRequestHandler
{
public DebugSessionRequestHandler()
: base(true)

View File

@@ -3,20 +3,10 @@ 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,17 +2,9 @@
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,19 +3,9 @@ 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,19 +2,9 @@
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,15 +1,8 @@
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,19 +2,9 @@
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,19 +2,9 @@
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,19 +3,9 @@ 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,21 +2,9 @@
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,16 +2,9 @@
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,23 +1,13 @@
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
{
[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 class GetRoutingPortsHandler : WebApiBaseRequestHandler
{
public GetRoutingPortsHandler() : base(true) { }

View File

@@ -1,19 +1,12 @@
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
{
[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 class GetTieLinesRequestHandler : WebApiBaseRequestHandler
{
public GetTieLinesRequestHandler() : base(true) { }

View File

@@ -2,19 +2,9 @@
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,18 +2,9 @@
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,16 +3,9 @@ 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,17 +2,9 @@
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,16 +3,9 @@ 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,20 +3,9 @@ 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,16 +2,9 @@
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

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

@@ -3,29 +3,60 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
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
}
public interface IHasCameras
/// <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);
}
@@ -42,7 +73,14 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// </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();
}
@@ -51,31 +89,71 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// </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>
/// The selected camera
/// </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; }
}
@@ -88,6 +166,9 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
}
/// <summary>
/// Interface for devices that have camera controls
/// </summary>
public interface IHasCameraControls
{
}
@@ -108,8 +189,19 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// </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();
}
@@ -118,8 +210,19 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// </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();
}
@@ -128,8 +231,19 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// </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();
}
@@ -138,25 +252,71 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// </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

@@ -6,11 +6,14 @@ using System.Collections.Generic;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Messenger for a CameraBase device
/// </summary>
public class CameraBaseMessenger : MessengerBase
{
/// <summary>
/// Device being bridged
/// </summary>
/// </summary>
public CameraBase Camera { get; set; }
/// <summary>
@@ -45,6 +48,9 @@ namespace PepperDash.Essentials.AppServer.Messengers
);
}
/// <summary>
/// Registers the actions for this messenger. This is called by the base class
/// </summary>
protected override void RegisterActions()
{
base.RegisterActions();

View File

@@ -0,0 +1,104 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Essentials.Devices.Common.Cameras;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Messenger for devices that implement the IHasCameras interface.
/// </summary>
public class IHasCamerasMessenger : MessengerBase
{
/// <summary>
/// Device being bridged that implements IHasCameras interface.
/// </summary>
public IHasCameras 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 IHasCamerasMessenger(string key, string messagePath , IHasCameras cameraController)
: base(key, messagePath, cameraController)
{
CameraController = cameraController ?? throw new ArgumentNullException("cameraController");
CameraController.CameraSelected += CameraController_CameraSelected;
}
private void CameraController_CameraSelected(object sender, CameraSelectedEventArgs e)
{
PostStatusMessage(new IHasCamerasStateMessage
{
SelectedCamera = e.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("/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 state = new IHasCamerasStateMessage
{
CameraList = CameraController.Cameras,
SelectedCamera = CameraController.SelectedCamera
};
PostStatusMessage(state, clientId);
}
}
/// <summary>
/// State message for devices that implement the IHasCameras interface.
/// </summary>
public class IHasCamerasStateMessage : DeviceStateMessageBase
{
/// <summary>
/// List of cameras available in the device.
/// </summary>
[JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)]
public List<CameraBase> CameraList { get; set; }
/// <summary>
/// The currently selected camera on the device.
/// </summary>
[JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)]
public CameraBase SelectedCamera { get; set; }
}
}

View File

@@ -907,6 +907,19 @@ namespace PepperDash.Essentials
messengerAdded = true;
}
if (device is IHasCameras cameras)
{
this.LogVerbose("Adding IHasCamerasMessenger for {deviceKey}", device.Key
);
var messenger = new IHasCamerasMessenger(
$"{device.Key}-cameras-{Key}",
$"/device/{device.Key}",
cameras
);
AddDefaultDeviceMessenger(messenger);
messengerAdded = true;
}
this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key);
if (device is EssentialsDevice)