Compare commits

...

11 Commits

Author SHA1 Message Date
Neil Dorin
9148cfd819 Merge pull request #1249 from PepperDash/generic-comm-monitor-issues 2025-04-11 12:32:12 -06:00
Andrew Welker
60550caf99 fix: add lock for threadsafety
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-11 13:26:01 -05:00
Andrew Welker
59baa74dd7 fix: multiple messages no longer sent
Due to how the `BeginPolling` method was written and being called, there
were situations where multiple PollTimers were created, causing there to
be multiple messages sent to the end point for each poll cycle.
2025-04-11 12:13:26 -05:00
Neil Dorin
bf31bf9e93 Merge pull request #1247 from PepperDash/messenger-fixes
ISelectableItemsMessenger & logging updates
2025-04-09 12:33:04 -06:00
Andrew Welker
ee8776cfb1 chore: remove unnecessary constructor overload 2025-04-09 12:04:32 -05:00
Andrew Welker
0b59990532 fix: add IHasInputsMessenger
In order to match up with how existing front-end apps are expecting to
recieve data for devices that implement the `IHasInputs<T>` interface,
there is now an IHasInputsMessengers that is implemented for devices
that implement `IHasInputs<string>`, `IHasInputs<int>` or
`IHasInputs<byte>` interfaces.
2025-04-09 11:48:05 -05:00
Andrew Welker
8d3fd343f1 fix: remove extraneous param for message logging 2025-04-09 10:53:42 -05:00
Andrew Welker
372274d9fa fix: add constructor that takes IHasInputs<T>
In order to satisfy the requirements for the `MessengerBase` class, the
`ISelectableItemsMessenger` class needs to take an `IHasInputs<T>`, as that is
the device that implements `IKeyName`. We may want to consider adding a
`IHasInputsMessenger` specifically for those devices that implement that
interface vs the `ISelectableItemsMessenger`.
2025-04-09 10:49:31 -05:00
Neil Dorin
403c03491c Merge pull request #1245 from PepperDash/fix-add-client
Update `mobileadduiclient` command & `CrestronGenericBaseDevice.CustomActivate` method
2025-04-08 12:47:08 -06:00
Andrew Welker
3770c2a47d fix: call the EssentialsDevice custom activate
`CrestronGenericBaseDevice` was NOT calling `base.CustomActivate()` in
it's `CustomActivate` override, causing the
`CreateMobileControlMessengers` method to not be called as expected when
plugin devices were inheriting from `CrestronGenericBaseDevice` or
`CrestronGenericBridgeableBaseDevice`.
2025-04-08 13:35:58 -05:00
Andrew Welker
5f4a1f768e fix: check for grant code in mobileadduiclient
If the grant code was not provided, the `mobileadduiclient` console
command would fail silently. The command now checks that the correct
number of arguments was provided and prints an error to the console of
one is missing
2025-04-08 13:32:50 -05:00
7 changed files with 240 additions and 103 deletions

View File

@@ -104,7 +104,7 @@ namespace PepperDash.Essentials.Core
Hardware.OnlineStatusChange += Hardware_OnlineStatusChange;
CommunicationMonitor.Start();
return true;
return base.CustomActivate();
}
/// <summary>

View File

@@ -1,16 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
using System.ComponentModel;
using PepperDash.Core;
using Serilog.Events;
using System.Threading;
using PepperDash.Core.Logging;
namespace PepperDash.Essentials.Core
{
@@ -31,35 +22,32 @@ namespace PepperDash.Essentials.Core
/// <summary>
/// Return true if the Client is ISocketStatus
/// </summary>
public bool IsSocket
{
get
{
return Client is ISocketStatus;
}
}
public bool IsSocket => Client is ISocketStatus;
long PollTime;
CTimer PollTimer;
string PollString;
Action PollAction;
/// <summary>
///
/// </summary>
/// <param name="client"></param>
/// <param name="pollTime">in MS, >= 5000</param>
/// <param name="warningTime">in MS, >= 5000</param>
/// <param name="errorTime">in MS, >= 5000</param>
/// <param name="pollString">String to send to comm</param>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime,
private readonly string PollString;
private readonly Action PollAction;
private readonly long PollTime;
private Timer PollTimer;
/// <summary>
/// GenericCommunicationMonitor constructor
///
/// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected
/// </summary>
/// <param name="parent">Parent device</param>
/// <param name="client">Communications Client</param>
/// <param name="pollTime">Time in MS for polling</param>
/// <param name="warningTime">Warning time in MS. If a message is not received before this elapsed time the status will be Warning</param>
/// <param name="errorTime">Error time in MS. If a message is not received before this elapsed time the status will be Error</param>
/// <param name="pollString">string to send for polling</param>
/// <exception cref="ArgumentException">Poll time must be less than warning and error time</exception>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime,
long warningTime, long errorTime, string pollString) :
base(parent, warningTime, errorTime)
{
if (pollTime > warningTime || pollTime > errorTime)
throw new ArgumentException("pollTime must be less than warning or errorTime");
//if (pollTime < 5000)
// throw new ArgumentException("pollTime cannot be less than 5000 ms");
Client = client;
PollTime = pollTime;
@@ -67,26 +55,41 @@ namespace PepperDash.Essentials.Core
if (IsSocket)
{
(Client as ISocketStatus).ConnectionChange += new EventHandler<GenericSocketStatusChageEventArgs>(socket_ConnectionChange);
(Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange;
}
}
/// <summary>
/// GenericCommunicationMonitor constructor with a bool to specify whether to monitor BytesReceived
///
/// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected
/// </summary>
/// <param name="parent">Parent device</param>
/// <param name="client">Communications Client</param>
/// <param name="pollTime">Time in MS for polling</param>
/// <param name="warningTime">Warning time in MS. If a message is not received before this elapsed time the status will be Warning</param>
/// <param name="errorTime">Error time in MS. If a message is not received before this elapsed time the status will be Error</param>
/// <param name="pollString">string to send for polling</param>
/// <param name="monitorBytesReceived">Use bytesReceived event instead of textReceived when true</param>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime,
long warningTime, long errorTime, string pollString, bool monitorBytesReceived) :
this(parent, client, pollTime, warningTime, errorTime, pollString)
{
SetMonitorBytesReceived(monitorBytesReceived);
MonitorBytesReceived = monitorBytesReceived;
}
/// <summary>
/// Poll is a provided action instead of string
/// GenericCommunicationMonitor constructor with a poll action instead of a poll string
///
/// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected
/// </summary>
/// <param name="parent"></param>
/// <param name="client"></param>
/// <param name="pollTime"></param>
/// <param name="warningTime"></param>
/// <param name="errorTime"></param>
/// <param name="pollBytes"></param>
/// <param name="parent">Parent device</param>
/// <param name="client">Communications Client</param>
/// <param name="pollTime">Time in MS for polling</param>
/// <param name="warningTime">Warning time in MS. If a message is not received before this elapsed time the status will be Warning</param>
/// <param name="errorTime">Error time in MS. If a message is not received before this elapsed time the status will be Error</param>
/// <param name="pollAction">Action to execute for polling</param>
/// <exception cref="ArgumentException">Poll time must be less than warning and error time</exception>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime,
long warningTime, long errorTime, Action pollAction) :
base(parent, warningTime, errorTime)
@@ -102,51 +105,67 @@ namespace PepperDash.Essentials.Core
if (IsSocket)
{
(Client as ISocketStatus).ConnectionChange += new EventHandler<GenericSocketStatusChageEventArgs>(socket_ConnectionChange);
(Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange;
}
}
/// <summary>
/// GenericCommunicationMonitor constructor with a poll action instead of a poll string and a bool to specify whether to monitor BytesReceived
///
/// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected
/// </summary>
/// <param name="parent">Parent device</param>
/// <param name="client">Communications Client</param>
/// <param name="pollTime">Time in MS for polling</param>
/// <param name="warningTime">Warning time in MS. If a message is not received before this elapsed time the status will be Warning</param>
/// <param name="errorTime">Error time in MS. If a message is not received before this elapsed time the status will be Error</param>
/// <param name="pollAction">Action to execute for polling</param>
/// <param name="monitorBytesReceived">Use bytesReceived event instead of textReceived when true</param>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, long pollTime,
long warningTime, long errorTime, Action pollAction, bool monitorBytesReceived) :
this(parent, client, pollTime, warningTime, errorTime, pollAction)
{
SetMonitorBytesReceived(monitorBytesReceived);
MonitorBytesReceived = monitorBytesReceived;
}
/// <summary>
/// Build the monitor from a config object
/// </summary>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client,
/// <summary>
/// GenericCommunicationMonitor constructor with a config object
///
/// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected
/// </summary>
/// <param name="parent">Parent Device</param>
/// <param name="client">Communications Client</param>
/// <param name="props"><see cref="CommunicationMonitorConfig">Communication Monitor Config</see> object</param>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client,
CommunicationMonitorConfig props) :
this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString)
{
if (IsSocket)
{
(Client as ISocketStatus).ConnectionChange += new EventHandler<GenericSocketStatusChageEventArgs>(socket_ConnectionChange);
(Client as ISocketStatus).ConnectionChange += Socket_ConnectionChange;
}
}
/// <summary>
/// Builds the monitor from a config object and takes a bool to specify whether to monitor BytesReceived
/// Default is to monitor TextReceived
/// GenericCommunicationMonitor constructor with a config object and a bool to specify whether to monitor BytesReceived
///
/// Note: If the client is a socket, the connection status will be monitored and the PollTimer will be started automatically when the client is connected
/// </summary>
/// <param name="parent"></param>
/// <param name="client"></param>
/// <param name="props"></param>
/// <param name="monitorBytesReceived"></param>
/// <param name="parent">Parent Device</param>
/// <param name="client">Communications Client</param>
/// <param name="props"><see cref="CommunicationMonitorConfig">Communication Monitor Config</see> object</param>
/// <param name="monitorBytesReceived">Use bytesReceived event instead of textReceived when true</param>
public GenericCommunicationMonitor(IKeyed parent, IBasicCommunication client, CommunicationMonitorConfig props, bool monitorBytesReceived) :
this(parent, client, props.PollInterval, props.TimeToWarning, props.TimeToError, props.PollString)
{
SetMonitorBytesReceived(monitorBytesReceived);
}
void SetMonitorBytesReceived(bool monitorBytesReceived)
{
MonitorBytesReceived = monitorBytesReceived;
}
/// <summary>
/// Start the poll cycle
/// </summary>
public override void Start()
{
if (MonitorBytesReceived)
@@ -163,7 +182,7 @@ namespace PepperDash.Essentials.Core
BeginPolling();
}
void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
private void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
{
if (!e.Client.IsConnected)
{
@@ -176,58 +195,65 @@ namespace PepperDash.Essentials.Core
{
// Start polling and set status to unknow and let poll result update the status to IsOk when a response is received
Status = MonitorStatus.StatusUnknown;
Start();
BeginPolling();
Start();
}
}
void BeginPolling()
private void BeginPolling()
{
Poll();
PollTimer = new CTimer(o => Poll(), null, PollTime, PollTime);
lock (_pollTimerLock)
{
if (PollTimer != null)
{
return;
}
PollTimer = new Timer(o => Poll(), null, 0, PollTime);
}
}
/// <summary>
/// Stop the poll cycle
/// </summary>
public override void Stop()
{
if(MonitorBytesReceived)
{
Client.BytesReceived -= this.Client_BytesReceived;
Client.BytesReceived -= Client_BytesReceived;
}
else
{
Client.TextReceived -= Client_TextReceived;
}
if (PollTimer != null)
StopErrorTimers();
if (PollTimer == null)
{
PollTimer.Stop();
PollTimer = null;
StopErrorTimers();
return;
}
PollTimer.Dispose();
PollTimer = null;
}
void Client_TextReceived(object sender, GenericCommMethodReceiveTextArgs e)
private void Client_TextReceived(object sender, GenericCommMethodReceiveTextArgs e)
{
DataReceived();
}
/// <summary>
/// Upon any receipt of data, set everything to ok!
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void Client_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e)
private void Client_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e)
{
DataReceived();
}
void DataReceived()
private void DataReceived()
{
Status = MonitorStatus.IsOk;
ResetErrorTimers();
}
void Poll()
private void Poll()
{
StartErrorTimers();
if (Client.IsConnected)
@@ -240,12 +266,14 @@ namespace PepperDash.Essentials.Core
}
else
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Comm not connected");
this.LogVerbose("Comm not connected");
}
}
}
/// <summary>
/// Communication Monitor Configuration from Essentials Configuration
/// </summary>
public class CommunicationMonitorConfig
{
public int PollInterval { get; set; }
@@ -253,6 +281,9 @@ namespace PepperDash.Essentials.Core
public int TimeToError { get; set; }
public string PollString { get; set; }
/// <summary>
/// Default constructor. Sets pollInterval to 30s, TimeToWarning to 120s, and TimeToError to 300s
/// </summary>
public CommunicationMonitorConfig()
{
PollInterval = 30000;

View File

@@ -0,0 +1,101 @@
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using System;
using System.Collections.Generic;
namespace PepperDash.Essentials.AppServer.Messengers
{
public class IHasInputsMessenger<TKey> : MessengerBase
{
private readonly IHasInputs<TKey> itemDevice;
/// <summary>
/// Constructs a messenger for a device that implements IHasInputs<typeparamref name="TKey"/>
/// </summary>
/// <param name="key"></param>
/// <param name="messagePath"></param>
/// <param name="device"></param>
public IHasInputsMessenger(string key, string messagePath, IHasInputs<TKey> device) : base(key, messagePath, device)
{
itemDevice = device;
}
protected override void RegisterActions()
{
base.RegisterActions();
AddAction("/fullStatus", (id, context) =>
{
SendFullStatus();
});
itemDevice.Inputs.ItemsUpdated += (sender, args) =>
{
SendFullStatus();
};
itemDevice.Inputs.CurrentItemChanged += (sender, args) =>
{
SendFullStatus();
};
foreach (var input in itemDevice.Inputs.Items)
{
var key = input.Key;
var localItem = input.Value;
AddAction($"/{key}", (id, content) =>
{
localItem.Select();
});
localItem.ItemUpdated += (sender, args) =>
{
SendFullStatus();
};
}
}
private void SendFullStatus()
{
try
{
this.LogInformation("Sending full status");
var stateObject = new IHasInputsStateMessage<TKey>
{
Inputs = new Inputs<TKey>
{
Items = itemDevice.Inputs.Items,
CurrentItem = itemDevice.Inputs.CurrentItem
}
};
PostStatusMessage(stateObject);
}
catch (Exception e)
{
this.LogError("Error sending full status: {0}", e.Message);
}
}
}
public class IHasInputsStateMessage<TKey> : DeviceStateMessageBase
{
[JsonProperty("inputs")]
public Inputs<TKey> Inputs { get; set; }
}
public class Inputs<TKey>
{
[JsonProperty("items")]
public Dictionary<TKey, ISelectableItem> Items { get; set; }
[JsonProperty("currentItem")]
public TKey CurrentItem { get; set; }
}
}

View File

@@ -1,6 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
@@ -10,11 +8,18 @@ using System.Collections.Generic;
namespace PepperDash.Essentials.AppServer.Messengers
{
public class ISelectableItemsMessenger<TKey> : MessengerBase
{
private static readonly JsonSerializer serializer = new JsonSerializer { Converters = { new StringEnumConverter() } };
{
private readonly ISelectableItems<TKey> itemDevice;
private readonly string _propName;
/// <summary>
/// Constructs a messenger for a device that implements ISelectableItems<typeparamref name="TKey"/>
/// </summary>
/// <param name="key"></param>
/// <param name="messagePath"></param>
/// <param name="device"></param>
/// <param name="propName"></param>
public ISelectableItemsMessenger(string key, string messagePath, ISelectableItems<TKey> device, string propName) : base(key, messagePath, device as IKeyName)
{
itemDevice = device;

View File

@@ -763,11 +763,10 @@ namespace PepperDash.Essentials
{
this.LogVerbose("Adding InputsMessenger<string> for {deviceKey}", device.Key);
var messenger = new ISelectableItemsMessenger<string>(
var messenger = new IHasInputsMessenger<string>(
$"{device.Key}-inputs-{Key}",
$"/device/{device.Key}",
stringInputs.Inputs,
"inputs"
stringInputs
);
AddDefaultDeviceMessenger(messenger);
@@ -779,11 +778,10 @@ namespace PepperDash.Essentials
{
this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key);
var messenger = new ISelectableItemsMessenger<byte>(
var messenger = new IHasInputsMessenger<byte>(
$"{device.Key}-inputs-{Key}",
$"/device/{device.Key}",
byteInputs.Inputs,
"inputs"
byteInputs
);
AddDefaultDeviceMessenger(messenger);
@@ -795,11 +793,10 @@ namespace PepperDash.Essentials
{
this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key);
var messenger = new ISelectableItemsMessenger<int>(
var messenger = new IHasInputsMessenger<int>(
$"{device.Key}-inputs-{Key}",
$"/device/{device.Key}",
intInputs.Inputs,
"inputs"
intInputs
);
AddDefaultDeviceMessenger(messenger);

View File

@@ -113,10 +113,7 @@ namespace PepperDash.Essentials
_server.SendMessageToAllClients(message);
_server.LogVerbose("Message TX To all clients: {message}", null, message);
_server.LogVerbose("Message TX To all clients: {message}", message);
}
catch (ThreadAbortException)
{
@@ -126,8 +123,6 @@ namespace PepperDash.Essentials
{
Debug.LogMessage(ex, "Caught an exception in the Transmit Processor");
}
}
#endregion
}

View File

@@ -679,6 +679,14 @@ namespace PepperDash.Essentials.WebSocketServer
}
var values = s.Split(' ');
if(values.Length < 2)
{
CrestronConsole.ConsoleCommandResponse("Invalid number of arguments. Please provide a room key and a grant code");
return;
}
var roomKey = values[0];
var grantCode = values[1];