Compare commits

...

23 Commits

Author SHA1 Message Date
Neil Dorin
8d55615837 Merge pull request #1280 from PepperDash/device-info-messenger 2025-06-26 16:08:49 -06:00
Andrew Welker
19e799f11d fix: debounce device info events
In some cases, multiple device info update events are triggering,
causing the queue to be flooded with multiple unneccessary messages containing the same info.
This clogs the queue and makes it harder for UIs to come online when Essentials restarts,
especially in systems with a lot of NVX devices.

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

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

When a user requests the _config.local.json, the processor will compare the remote IP address
with the CS LAN IP Address and determine if they're in the same subnet, the assumption being
that if the IP Addresses are in the same subnet, then the app or end user is on the CS LAN and needs the CS config.
If the addresses are in the same subnet, then the contents of the _config.cs.json will be returned.
2025-06-06 14:58:26 -05:00
Neil Dorin
e7c3fcbbd9 Merge pull request #1270 from PepperDash/dsp-base-fix
dsp base fix
2025-05-14 12:04:41 -06:00
Andrew Welker
0c7ec82529 fix: initialize dictionaries
fix #1167
2025-05-14 12:25:22 -05:00
Andrew Welker
feb99ecbb6 fix: use correct join for preset select 2025-05-14 12:25:21 -05:00
equinoy
d78b9ea313 Merge pull request #1267 from PepperDash/DGE-1000
feat: add support for mcdge1000 type in MobileControlTouchpanelContro…
2025-05-09 16:16:49 -04:00
equinoy
15172a5509 feat: add support for mcdge1000 type in MobileControlTouchpanelController 2025-05-09 16:06:33 -04:00
14 changed files with 1955 additions and 1480 deletions

View File

@@ -11,35 +11,35 @@ namespace PepperDash.Core
public class Device : IKeyName
{
/// <summary>
/// Unique Key
/// </summary>
/// <summary>
/// Unique Key
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// Name of the devie
/// </summary>
public string Name { get; protected set; }
/// <summary>
///
/// </summary>
public string Name { get; protected set; }
/// <summary>
///
/// </summary>
public bool Enabled { get; protected set; }
///// <summary>
///// A place to store reference to the original config object, if any. These values should
///// NOT be used as properties on the device as they are all publicly-settable values.
///// </summary>
//public DeviceConfig Config { get; private set; }
///// <summary>
///// Helper method to check if Config exists
///// </summary>
//public bool HasConfig { get { return Config != null; } }
///// <summary>
///// A place to store reference to the original config object, if any. These values should
///// NOT be used as properties on the device as they are all publicly-settable values.
///// </summary>
//public DeviceConfig Config { get; private set; }
///// <summary>
///// Helper method to check if Config exists
///// </summary>
//public bool HasConfig { get { return Config != null; } }
List<Action> _PreActivationActions;
List<Action> _PostActivationActions;
/// <summary>
///
/// </summary>
/// <summary>
///
/// </summary>
public static Device DefaultDevice { get { return _DefaultDevice; } }
static Device _DefaultDevice = new Device("Default", "Default");
@@ -54,27 +54,27 @@ namespace PepperDash.Core
Name = "";
}
/// <summary>
/// Constructor with key and name
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <summary>
/// Constructor with key and name
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
public Device(string key, string name) : this(key)
{
Name = name;
}
//public Device(DeviceConfig config)
// : this(config.Key, config.Name)
//{
// Config = config;
//}
//public Device(DeviceConfig config)
// : this(config.Key, config.Name)
//{
// Config = config;
//}
/// <summary>
/// Adds a pre activation action
/// </summary>
/// <param name="act"></param>
/// <summary>
/// Adds a pre activation action
/// </summary>
/// <param name="act"></param>
public void AddPreActivationAction(Action act)
{
if (_PreActivationActions == null)
@@ -82,10 +82,10 @@ namespace PepperDash.Core
_PreActivationActions.Add(act);
}
/// <summary>
/// Adds a post activation action
/// </summary>
/// <param name="act"></param>
/// <summary>
/// Adds a post activation action
/// </summary>
/// <param name="act"></param>
public void AddPostActivationAction(Action act)
{
if (_PostActivationActions == null)
@@ -93,55 +93,58 @@ namespace PepperDash.Core
_PostActivationActions.Add(act);
}
/// <summary>
/// Executes the preactivation actions
/// </summary>
public void PreActivate()
{
if (_PreActivationActions != null)
_PreActivationActions.ForEach(a => {
/// <summary>
/// Executes the preactivation actions
/// </summary>
public void PreActivate()
{
if (_PreActivationActions != null)
_PreActivationActions.ForEach(a =>
{
try
{
a.Invoke();
} catch (Exception e)
{
}
catch (Exception e)
{
Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this);
}
});
}
});
}
/// <summary>
/// Gets this device ready to be used in the system. Runs any added pre-activation items, and
/// all post-activation at end. Classes needing additional logic to
/// run should override CustomActivate()
/// </summary>
public bool Activate()
public bool Activate()
{
//if (_PreActivationActions != null)
// _PreActivationActions.ForEach(a => a.Invoke());
//if (_PreActivationActions != null)
// _PreActivationActions.ForEach(a => a.Invoke());
var result = CustomActivate();
//if(result && _PostActivationActions != null)
// _PostActivationActions.ForEach(a => a.Invoke());
return result;
//if(result && _PostActivationActions != null)
// _PostActivationActions.ForEach(a => a.Invoke());
return result;
}
/// <summary>
/// Executes the postactivation actions
/// </summary>
public void PostActivate()
{
if (_PostActivationActions != null)
_PostActivationActions.ForEach(a => {
try
{
a.Invoke();
}
catch (Exception e)
{
Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
}
});
}
/// <summary>
/// Executes the postactivation actions
/// </summary>
public void PostActivate()
{
if (_PostActivationActions != null)
_PostActivationActions.ForEach(a =>
{
try
{
a.Invoke();
}
catch (Exception e)
{
Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
}
});
}
/// <summary>
/// Called in between Pre and PostActivationActions when Activate() is called.
@@ -158,14 +161,14 @@ namespace PepperDash.Core
/// <returns></returns>
public virtual bool Deactivate() { return true; }
/// <summary>
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
/// </summary>
public virtual void Initialize()
{
}
/// <summary>
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
/// </summary>
public virtual void Initialize()
{
}
/// <summary>
/// <summary>
/// Helper method to check object for bool value false and fire an Action method
/// </summary>
/// <param name="o">Should be of type bool, others will be ignored</param>
@@ -175,5 +178,15 @@ namespace PepperDash.Core
if (o is bool && !(bool)o) a();
}
/// <summary>
/// Returns a string representation of the object, including its key and name.
/// </summary>
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
/// null or empty, "---" is used in place of the name.</remarks>
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
public override string ToString()
{
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
}
}
}

View File

@@ -0,0 +1,107 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>
/// This defines a device that has screens with layouts
/// Simply decorative
/// </summary>
public interface IHasScreensWithLayouts
{
/// <summary>
/// A dictionary of screens, keyed by screen ID, that contains information about each screen and its layouts.
/// </summary>
Dictionary<uint, ScreenInfo> Screens { get; }
/// <summary>
/// Applies a specific layout to a screen based on the provided screen ID and layout index.
/// </summary>
/// <param name="screenId"></param>
/// <param name="layoutIndex"></param>
void ApplyLayout(uint screenId, uint layoutIndex);
}
/// <summary>
/// Represents information about a screen and its layouts.
/// </summary>
public class ScreenInfo
{
/// <summary>
/// Indicates whether the screen is enabled or not.
/// </summary>
[JsonProperty("enabled")]
public bool Enabled { get; set; }
/// <summary>
/// The name of the screen.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// The index of the screen.
/// </summary>
[JsonProperty("screenIndex")]
public int ScreenIndex { get; set; }
/// <summary>
/// A dictionary of layout information for the screen, keyed by layout ID.
/// </summary>
[JsonProperty("layouts")]
public Dictionary<uint, LayoutInfo> Layouts { get; set; }
}
/// <summary>
/// Represents information about a layout on a screen.
/// </summary>
public class LayoutInfo
{
/// <summary>
/// The name of the layout.
/// </summary>
[JsonProperty("layoutName")]
public string LayoutName { get; set; }
/// <summary>
/// The index of the layout.
/// </summary>
[JsonProperty("layoutIndex")]
public int LayoutIndex { get; set; }
/// <summary>
/// The type of the layout, which can be "single", "double", "triple", or "quad".
/// </summary>
[JsonProperty("layoutType")]
public string LayoutType { get; set; }
/// <summary>
/// A dictionary of window configurations for the layout, keyed by window ID.
/// </summary>
[JsonProperty("windows")]
public Dictionary<uint, WindowConfig> Windows { get; set; }
}
/// <summary>
/// Represents the configuration of a window within a layout on a screen.
/// </summary>
public class WindowConfig
{
/// <summary>
/// The display label for the window
/// </summary>
[JsonProperty("label")]
public string Label { get; set; }
/// <summary>
/// The input for the window
/// </summary>
[JsonProperty("input")]
public string Input { get; set; }
}
}

View File

@@ -248,7 +248,7 @@ namespace PepperDash.Essentials.Core
foreach (var dev in Devices.Values.OfType<ICommunicationMonitor>())
{
CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}{Environment.NewLine}");
CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}\r\n");
}
}

View File

@@ -20,19 +20,20 @@ namespace PepperDash.Essentials.Core
public event EventHandler Initialized;
private bool _isInitialized;
public bool IsInitialized {
public bool IsInitialized
{
get { return _isInitialized; }
private set
{
private set
{
if (_isInitialized == value) return;
_isInitialized = value;
if (_isInitialized)
{
Initialized?.Invoke(this, new EventArgs());
}
}
}
}
protected EssentialsDevice(string key)
@@ -80,8 +81,9 @@ namespace PepperDash.Essentials.Core
/// <summary>
/// Override this method to build and create custom Mobile Control Messengers during the Activation phase
/// </summary>
protected virtual void CreateMobileControlMessengers() {
protected virtual void CreateMobileControlMessengers()
{
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Extensions for IPAddress to provide additional functionality such as getting broadcast address, network address, and checking if two addresses are in the same subnet.
/// </summary>
public static class IPAddressExtensions
{
/// <summary>
/// Get the broadcast address for a given IP address and subnet mask.
/// </summary>
/// <param name="address">Address to check</param>
/// <param name="subnetMask">Subnet mask in a.b.c.d format</param>
/// <returns>Broadcast address</returns>
/// <remarks>
/// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the broadcast address will be 192.168.1.255
/// </remarks>
/// <exception cref="ArgumentException"></exception>
public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask)
{
byte[] ipAdressBytes = address.GetAddressBytes();
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
for (int i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
}
return new IPAddress(broadcastAddress);
}
/// <summary>
/// Get the network address for a given IP address and subnet mask.
/// </summary>
/// <param name="address">Address to check</param>
/// <param name="subnetMask">Subnet mask in a.b.c.d</param>
/// <returns>Network Address</returns>
/// /// <remarks>
/// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the network address will be 192.168.1.0
/// </remarks>
/// <exception cref="ArgumentException"></exception>
public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)
{
byte[] ipAdressBytes = address.GetAddressBytes();
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
if (ipAdressBytes.Length != subnetMaskBytes.Length)
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
for (int i = 0; i < broadcastAddress.Length; i++)
{
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
}
return new IPAddress(broadcastAddress);
}
/// <summary>
/// Determine if two IP addresses are in the same subnet.
/// </summary>
/// <param name="address2">Address to check</param>
/// <param name="address">Second address to check</param>
/// <param name="subnetMask">Subnet mask to use to compare the 2 IP Address</param>
/// <returns>True if addresses are in the same subnet</returns>
/// <remarks>
/// If the input IP addresses are 192.168.1.100 and 192.168.1.200, and the subnet mask is 255.255.255.0, this will return true.
/// If the input IP addresses are 10.1.1.100 and 192.168.1.100, and the subnet mask is 255.255.255.0, this will return false.
/// </remarks>
public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask)
{
IPAddress network1 = address.GetNetworkAddress(subnetMask);
IPAddress network2 = address2.GetNetworkAddress(subnetMask);
return network1.Equals(network2);
}
}
}

View File

@@ -227,7 +227,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist);
for (int i = 0; i < joinMap.NumberOfPresets.JoinSpan; i++)
for (int i = 0; i < joinMap.PresetRecallStart.JoinSpan; i++)
{
int tempNum = i;

View File

@@ -0,0 +1,95 @@
using PepperDash.Core;
using PepperDash.Essentials.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.Codec.Cisco
{
/// <summary>
/// Describes the available tracking modes for a Cisco codec's Presenter Track feature.
/// </summary>
public enum ePresenterTrackMode
{
/// <summary>
/// Presenter Track is turned off.
/// </summary>
Off,
/// <summary>
/// Presenter Track follows the speaker's movements.
/// </summary>
Follow,
/// <summary>
/// Presenter Track is set to background mode, where it tracks the speaker but does not actively follow.
/// </summary>
Background,
/// <summary>
/// Presenter Track is set to persistent mode, where it maintains a fixed position or focus on the speaker.
/// </summary>
Persistent
}
/// <summary>
/// Describes the Presenter Track controls for a Cisco codec.
/// </summary>
public interface IPresenterTrack : IKeyed
{
/// <summary>
///
/// </summary>
bool PresenterTrackAvailability { get; }
/// <summary>
/// Feedback indicating whether Presenter Track is available.
/// </summary>
BoolFeedback PresenterTrackAvailableFeedback { get; }
/// <summary>
/// Feedback indicating the current status of Presenter Track is off
/// </summary>
BoolFeedback PresenterTrackStatusOffFeedback { get; }
/// <summary>
/// Feedback indicating the current status of Presenter Track is follow
/// </summary>
BoolFeedback PresenterTrackStatusFollowFeedback { get; }
/// <summary>
/// Feedback indicating the current status of Presenter Track is background
/// </summary>
BoolFeedback PresenterTrackStatusBackgroundFeedback { get; }
/// <summary>
/// Feedback indicating the current status of Presenter Track is persistent
/// </summary>
BoolFeedback PresenterTrackStatusPersistentFeedback { get; }
/// <summary>
/// Indicates the current status of Presenter Track.
/// </summary>
bool PresenterTrackStatus { get; }
/// <summary>
/// Turns off Presenter Track.
/// </summary>
void PresenterTrackOff();
/// <summary>
/// Turns on Presenter Track in follow mode.
/// </summary>
void PresenterTrackFollow();
/// <summary>
/// Turns on Presenter Track in background mode.
/// </summary>
void PresenterTrackBackground();
/// <summary>
/// Turns on Presenter Track in persistent mode.
/// </summary>
void PresenterTrackPersistent();
}
}

View File

@@ -0,0 +1,40 @@
using PepperDash.Core;
using PepperDash.Essentials.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.Codec.Cisco
{
/// <summary>
/// Describes the available tracking modes for a Cisco codec
/// </summary>
public interface ISpeakerTrack : IKeyed
{
/// <summary>
/// Indicates whether Speaker Track is available on the codec.
/// </summary>
bool SpeakerTrackAvailability { get; }
/// <summary>
///
/// </summary>
BoolFeedback SpeakerTrackAvailableFeedback { get; }
/// <summary>
/// Feedback indicating the current status of Speaker Track is off
/// </summary>
bool SpeakerTrackStatus { get; }
/// <summary>
/// Turns Speaker Track off
/// </summary>
void SpeakerTrackOff();
/// <summary>
/// Turns Speaker Track on
/// </summary>
void SpeakerTrackOn();
}
}

View File

@@ -18,9 +18,13 @@ namespace PepperDash.Essentials.Devices.Common.DSP
public Dictionary<string, DspControlPoint> SwitcherControlPoints { get; private set; }
public DspBase(string key, string name) :
base(key, name)
{
public DspBase(string key, string name) :
base(key, name)
{
LevelControlPoints = new Dictionary<string, IBasicVolumeWithFeedback>();
DialerControlPoints = new Dictionary<string, DspControlPoint>();
SwitcherControlPoints = new Dictionary<string, DspControlPoint>();
}

View File

@@ -12,20 +12,45 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
/// </summary>
public interface IHasCodecRoomPresets
{
/// <summary>
/// Event that is raised when the list of room presets has changed.
/// </summary>
event EventHandler<EventArgs> CodecRoomPresetsListHasChanged;
/// <summary>
/// List of near end presets that can be recalled.
/// </summary>
List<CodecRoomPreset> NearEndPresets { get; }
/// <summary>
/// List of far end presets that can be recalled.
/// </summary>
List<CodecRoomPreset> FarEndRoomPresets { get; }
/// <summary>
/// Selects a near end preset by its ID.
/// </summary>
/// <param name="preset"></param>
void CodecRoomPresetSelect(int preset);
void CodecRoomPresetStore(int preset, string description);
/// <summary>
/// Stores a near end preset with the given ID and description.
/// </summary>
/// <param name="preset"></param>
/// <param name="description"></param>
void CodecRoomPresetStore(int preset, string description);
/// <summary>
/// Selects a far end preset by its ID. This is typically used to recall a preset that has been defined on the far end codec.
/// </summary>
/// <param name="preset"></param>
void SelectFarEndPreset(int preset);
}
public static class RoomPresets
/// <summary>
/// Static class for converting non-generic RoomPresets to generic CameraPresets.
/// </summary>
public static class RoomPresets
{
/// <summary>
/// Converts non-generic RoomPresets to generic CameraPresets
@@ -47,6 +72,13 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
/// </summary>
public class CodecRoomPreset : PresetBase
{
/// <summary>
///
/// </summary>
/// <param name="id"></param>
/// <param name="description"></param>
/// <param name="def"></param>
/// <param name="isDef"></param>
public CodecRoomPreset(int id, string description, bool def, bool isDef)
: base(id, description, def, isDef)
{

View File

@@ -2,27 +2,69 @@
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core.DeviceInfo;
using System.Timers;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Facilitates communication of device information by providing mechanisms for status updates and device
/// information reporting.
/// </summary>
/// <remarks>The <see cref="DeviceInfoMessenger"/> class integrates with an <see
/// cref="IDeviceInfoProvider"/> to manage device-specific information. It uses a debounce timer to limit the
/// frequency of updates, ensuring efficient communication. The timer is initialized with a 1-second interval and
/// is disabled by default. This class also subscribes to device information change events and provides actions for
/// reporting full device status and triggering updates.</remarks>
public class DeviceInfoMessenger : MessengerBase
{
private readonly IDeviceInfoProvider _deviceInfoProvider;
private readonly Timer debounceTimer;
/// <summary>
/// Initializes a new instance of the <see cref="DeviceInfoMessenger"/> class, which facilitates communication
/// of device information.
/// </summary>
/// <remarks>The messenger uses a debounce timer to limit the frequency of certain operations. The
/// timer is initialized with a 1-second interval and is disabled by default.</remarks>
/// <param name="key">A unique identifier for the messenger instance.</param>
/// <param name="messagePath">The path used for sending and receiving messages.</param>
/// <param name="device">An implementation of <see cref="IDeviceInfoProvider"/> that provides device-specific information.</param>
public DeviceInfoMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device)
{
_deviceInfoProvider = device;
}
debounceTimer = new Timer(1000)
{
Enabled = false,
AutoReset = false
};
debounceTimer.Elapsed += DebounceTimer_Elapsed;
}
private void DebounceTimer_Elapsed(object sender, ElapsedEventArgs e)
{
PostStatusMessage(JToken.FromObject(new
{
deviceInfo = _deviceInfoProvider.DeviceInfo
}));
}
/// <summary>
/// Registers actions and event handlers for device information updates and status reporting.
/// </summary>
/// <remarks>This method sets up actions for handling device status updates and reporting full
/// device status. It also subscribes to the <see cref="IDeviceInfoProvider.DeviceInfoChanged"/> event to
/// trigger debounced updates when the device information changes.</remarks>
protected override void RegisterActions()
{
base.RegisterActions();
_deviceInfoProvider.DeviceInfoChanged += (o, a) =>
{
PostStatusMessage(JToken.FromObject(new
{
deviceInfo = a.DeviceInfo
}));
debounceTimer.Stop();
debounceTimer.Start();
};
AddAction("/fullStatus", (id, context) => PostStatusMessage(new DeviceInfoStateMessage
@@ -34,6 +76,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
}
}
/// <summary>
/// Represents a message containing the state information of a device, including detailed device information.
/// </summary>
/// <remarks>This class is used to encapsulate the state of a device along with its associated
/// information. It extends <see cref="DeviceStateMessageBase"/> to provide additional details about the
/// device.</remarks>
public class DeviceInfoStateMessage : DeviceStateMessageBase
{
[JsonProperty("deviceInfo")]

View File

@@ -491,7 +491,7 @@ namespace PepperDash.Essentials.Touchpanel
{
public MobileControlTouchpanelControllerFactory()
{
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel" };
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
MinimumEssentialsFrameworkVersion = "2.0.0";
}
@@ -555,7 +555,10 @@ namespace PepperDash.Essentials.Touchpanel
return new Tsw1070(id, Global.ControlSystem);
else if (type == "ts1070")
return new Ts1070(id, Global.ControlSystem);
else
else if (type == "dge1000")
return new Dge1000(id, Global.ControlSystem);
else
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "WARNING: Cannot create TSW controller with type '{0}'", type);
return null;

View File

@@ -0,0 +1,145 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.RoomBridges;
using Serilog.Events;
using System;
using System.Text.RegularExpressions;
using WebSocketSharp;
using WebSocketSharp.Server;
using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Represents the behaviour to associate with a UiClient for WebSocket communication
/// </summary>
public class UiClient : WebSocketBehavior
{
public MobileControlSystemController Controller { get; set; }
public string RoomKey { get; set; }
private string _clientId;
private DateTime _connectionTime;
public TimeSpan ConnectedDuration
{
get
{
if (Context.WebSocket.IsAlive)
{
return DateTime.Now - _connectionTime;
}
else
{
return new TimeSpan(0);
}
}
}
public UiClient()
{
}
protected override void OnOpen()
{
base.OnOpen();
var url = Context.WebSocket.Url;
Debug.LogMessage(LogEventLevel.Verbose, "New WebSocket Connection from: {0}", null, url);
var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)");
if (!match.Success)
{
_connectionTime = DateTime.Now;
return;
}
var clientId = match.Groups[1].Value;
_clientId = clientId;
if (Controller == null)
{
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");
_connectionTime = DateTime.Now;
}
var clientJoinedMessage = new MobileControlMessage
{
Type = "/system/clientJoined",
Content = JToken.FromObject(new
{
clientId,
roomKey = RoomKey,
})
};
Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage));
var bridge = Controller.GetRoomBridge(RoomKey);
if (bridge == null) return;
SendUserCodeToClient(bridge, clientId);
bridge.UserCodeChanged -= Bridge_UserCodeChanged;
bridge.UserCodeChanged += Bridge_UserCodeChanged;
// TODO: Future: Check token to see if there's already an open session using that token and reject/close the session
}
private void Bridge_UserCodeChanged(object sender, EventArgs e)
{
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
}
private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId)
{
var content = new
{
userCode = bridge.UserCode,
qrUrl = bridge.QrCodeUrl,
};
var message = new MobileControlMessage
{
Type = "/system/userCodeChanged",
ClientId = clientId,
Content = JToken.FromObject(content)
};
Controller.SendMessageObjectToDirectClient(message);
}
protected override void OnMessage(MessageEventArgs e)
{
base.OnMessage(e);
if (e.IsText && e.Data.Length > 0 && Controller != null)
{
// Forward the message to the controller to be put on the receive queue
Controller.HandleClientMessage(e.Data);
}
}
protected override void OnClose(CloseEventArgs e)
{
base.OnClose(e);
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Closing: {0} reason: {1}", null, e.Code, e.Reason);
}
protected override void OnError(ErrorEventArgs e)
{
base.OnError(e);
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Error: {exception} message: {message}", e.Exception, e.Message);
}
}
}