Refator: Refactor timer implementation across multiple classes to use System.Timers.Timer instead of CTimer for improved consistency and performance.

- Updated RelayControlledShade to utilize Timer for output pulsing.
- Refactored MockVC to replace CTimer with Timer for call status simulation.
- Modified VideoCodecBase to enhance documentation and improve feedback handling.
- Removed obsolete IHasCamerasMessenger and updated related classes to use IHasCamerasWithControls.
- Adjusted PressAndHoldHandler to implement Timer for button hold actions.
- Enhanced logging throughout MobileControl and RoomBridges for better debugging and information tracking.
- Cleaned up unnecessary comments and improved exception handling in various classes.
This commit is contained in:
Neil Dorin 2026-03-30 11:44:15 -06:00
parent b4d53dbe0e
commit 7076eafc21
56 changed files with 1343 additions and 2197 deletions

View file

@ -1,715 +0,0 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.Config;
using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common.Cameras;
/// <summary>
/// Camera driver for cameras that use the VISCA control protocol. This driver supports both IP and RS-232 control depending on the communication method passed in. It also supports pan/tilt speed control, presets, and focus control.
/// </summary>
public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode
{
private readonly CameraViscaPropertiesConfig PropertiesConfig;
/// <summary>
/// Gets or sets the Communication
/// </summary>
public IBasicCommunication Communication { get; private set; }
/// <summary>
/// Gets or sets the CommunicationMonitor
/// </summary>
public StatusMonitorBase CommunicationMonitor { get; private set; }
/// <summary>
/// Used to store the actions to parse inquiry responses as the inquiries are sent
/// </summary>
private readonly CrestronQueue<Action<byte[]>> InquiryResponseQueue;
/// <summary>
/// Camera ID (Default 1)
/// </summary>
public byte ID = 0x01;
public byte ResponseID;
/// <summary>
/// Slow speed value for pan movement
/// </summary>
public byte PanSpeedSlow = 0x10;
/// <summary>
/// Slow speed value for tilt movement
/// </summary>
public byte TiltSpeedSlow = 0x10;
public byte PanSpeedFast = 0x13;
public byte TiltSpeedFast = 0x13;
// private bool IsMoving;
private bool IsZooming;
bool _powerIsOn;
public bool PowerIsOn
{
get
{
return _powerIsOn;
}
private set
{
if (value != _powerIsOn)
{
_powerIsOn = value;
PowerIsOnFeedback.FireUpdate();
CameraIsOffFeedback.FireUpdate();
}
}
}
const byte ZoomInCmd = 0x02;
const byte ZoomOutCmd = 0x03;
const byte ZoomStopCmd = 0x00;
/// <summary>
/// Used to determine when to move the camera at a faster speed if a direction is held
/// </summary>
CTimer SpeedTimer;
// TODO: Implment speed timer for PTZ controls
long FastSpeedHoldTimeMs = 2000;
byte[] IncomingBuffer = new byte[] { };
public BoolFeedback PowerIsOnFeedback { get; private set; }
public CameraVisca(string key, string name, IBasicCommunication comm, CameraViscaPropertiesConfig props) :
base(key, name)
{
InquiryResponseQueue = new CrestronQueue<Action<byte[]>>(15);
Presets = props.Presets;
PropertiesConfig = props;
ID = (byte)(props.Id + 0x80);
ResponseID = (byte)((props.Id * 0x10) + 0x80);
SetupCameraSpeeds();
OutputPorts.Add(new RoutingOutputPort("videoOut", eRoutingSignalType.Video, eRoutingPortConnectionType.None, null, this, true));
// Default to all capabilties
Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus;
Communication = comm;
if (comm is ISocketStatus socket)
{
// This instance uses IP control
socket.ConnectionChange += new EventHandler<GenericSocketStatusChageEventArgs>(Socket_ConnectionChange);
}
else
{
// This instance uses RS-232 control
}
Communication.BytesReceived += new EventHandler<GenericCommMethodReceiveBytesArgs>(Communication_BytesReceived);
PowerIsOnFeedback = new BoolFeedback(() => { return PowerIsOn; });
CameraIsOffFeedback = new BoolFeedback(() => { return !PowerIsOn; });
if (props.CommunicationMonitorProperties != null)
{
CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, props.CommunicationMonitorProperties);
}
else
{
CommunicationMonitor = new GenericCommunicationMonitor(this, Communication, 20000, 120000, 300000, "\x81\x09\x04\x00\xFF");
}
DeviceManager.AddDevice(CommunicationMonitor);
}
/// <summary>
/// Sets up camera speed values based on config
/// </summary>
void SetupCameraSpeeds()
{
if (PropertiesConfig.FastSpeedHoldTimeMs > 0)
{
FastSpeedHoldTimeMs = PropertiesConfig.FastSpeedHoldTimeMs;
}
if (PropertiesConfig.PanSpeedSlow > 0)
{
PanSpeedSlow = (byte)PropertiesConfig.PanSpeedSlow;
}
if (PropertiesConfig.PanSpeedFast > 0)
{
PanSpeedFast = (byte)PropertiesConfig.PanSpeedFast;
}
if (PropertiesConfig.TiltSpeedSlow > 0)
{
TiltSpeedSlow = (byte)PropertiesConfig.TiltSpeedSlow;
}
if (PropertiesConfig.TiltSpeedFast > 0)
{
TiltSpeedFast = (byte)PropertiesConfig.TiltSpeedFast;
}
}
public override bool CustomActivate()
{
Communication.Connect();
CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); };
CommunicationMonitor.Start();
CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); };
CommunicationMonitor.Start();
CrestronConsole.AddNewConsoleCommand(s => Communication.Connect(), "con" + Key, "", ConsoleAccessLevelEnum.AccessOperator);
return true;
}
/// <summary>
/// LinkToApi method
/// </summary>
public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
{
LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge);
}
void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString());
if (e.Client.IsConnected)
{
}
else
{
}
}
void SendBytes(byte[] b)
{
if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1
Debug.LogMessage(LogEventLevel.Verbose, this, "Sending:{0}", ComTextHelper.GetEscapedText(b));
Communication.SendBytes(b);
}
void Communication_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs e)
{
var newBytes = new byte[IncomingBuffer.Length + e.Bytes.Length];
try
{
// This is probably not thread-safe buffering
// Append the incoming bytes with whatever is in the buffer
IncomingBuffer.CopyTo(newBytes, 0);
e.Bytes.CopyTo(newBytes, IncomingBuffer.Length);
if (Debug.Level == 2) // This check is here to prevent following string format from building unnecessarily on level 0 or 1
Debug.LogMessage(LogEventLevel.Verbose, this, "Received:{0}", ComTextHelper.GetEscapedText(newBytes));
byte[] message = new byte[] { };
// Search for the delimiter 0xFF character
for (int i = 0; i < newBytes.Length; i++)
{
if (newBytes[i] == 0xFF)
{
// i will be the index of the delmiter character
message = newBytes.Take(i).ToArray();
// Skip over what we just took and save the rest for next time
newBytes = newBytes.Skip(i).ToArray();
}
}
if (message.Length > 0)
{
// Check for matching ID
if (message[0] != ResponseID)
{
return;
}
switch (message[1])
{
case 0x40:
{
// ACK received
Debug.LogMessage(LogEventLevel.Verbose, this, "ACK Received");
break;
}
case 0x50:
{
if (message[2] == 0xFF)
{
// Completion received
Debug.LogMessage(LogEventLevel.Verbose, this, "Completion Received");
}
else
{
// Inquiry response received. Dequeue the next response handler and invoke it
if (InquiryResponseQueue.Count > 0)
{
var inquiryAction = InquiryResponseQueue.Dequeue();
inquiryAction.Invoke(message.Skip(2).ToArray());
}
else
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Response Queue is empty. Nothing to dequeue.");
}
}
break;
}
case 0x60:
{
// Error message
switch (message[2])
{
case 0x01:
{
// Message Length Error
Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Message Length Error");
break;
}
case 0x02:
{
// Syntax Error
Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Syntax Error");
break;
}
case 0x03:
{
// Command Buffer Full
Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Buffer Full");
break;
}
case 0x04:
{
// Command Cancelled
Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command Cancelled");
break;
}
case 0x05:
{
// No Socket
Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: No Socket");
break;
}
case 0x41:
{
// Command not executable
Debug.LogMessage(LogEventLevel.Verbose, this, "Error from device: Command not executable");
break;
}
}
break;
}
}
if (message == new byte[] { ResponseID, 0x50, 0x02, 0xFF })
{
PowerIsOn = true;
}
else if (message == new byte[] { ResponseID, 0x50, 0x03, 0xFF })
{
PowerIsOn = false;
}
}
}
catch (Exception err)
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Error parsing feedback: {0}", err);
}
finally
{
// Save whatever partial message is here
IncomingBuffer = newBytes;
}
}
/// <summary>
/// Sends a pan/tilt command. If the command is not for fastSpeed then it starts a timer to initiate fast speed.
/// </summary>
/// <param name="cmd"></param>
/// <param name="fastSpeed"></param>
private void SendPanTiltCommand(byte[] cmd, bool fastSpeedEnabled)
{
SendBytes(GetPanTiltCommand(cmd, fastSpeedEnabled));
if (!fastSpeedEnabled)
{
if (SpeedTimer != null)
{
StopSpeedTimer();
}
// Start the timer to send fast speed if still moving after FastSpeedHoldTime elapses
SpeedTimer = new CTimer((o) => SendPanTiltCommand(GetPanTiltCommand(cmd, true), true), FastSpeedHoldTimeMs);
}
}
private void StopSpeedTimer()
{
if (SpeedTimer != null)
{
SpeedTimer.Stop();
SpeedTimer.Dispose();
SpeedTimer = null;
}
}
/// <summary>
/// Generates the pan/tilt command with either slow or fast speed
/// </summary>
/// <param name="cmd"></param>
/// <param name="fastSpeed"></param>
/// <returns></returns>
private byte[] GetPanTiltCommand(byte[] cmd, bool fastSpeed)
{
byte panSpeed;
byte tiltSpeed;
if (!fastSpeed)
{
panSpeed = PanSpeedSlow;
tiltSpeed = TiltSpeedSlow;
}
else
{
panSpeed = PanSpeedFast;
tiltSpeed = TiltSpeedFast;
}
var temp = new byte[] { ID, 0x01, 0x06, 0x01, panSpeed, tiltSpeed };
int length = temp.Length + cmd.Length + 1;
byte[] sum = new byte[length];
temp.CopyTo(sum, 0);
cmd.CopyTo(sum, temp.Length);
sum[length - 1] = 0xFF;
return sum;
}
void SendPowerQuery()
{
SendBytes(new byte[] { ID, 0x09, 0x04, 0x00, 0xFF });
InquiryResponseQueue.Enqueue(HandlePowerResponse);
}
public void PowerOn()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x02, 0xFF });
SendPowerQuery();
}
void HandlePowerResponse(byte[] response)
{
switch (response[0])
{
case 0x02:
{
PowerIsOn = true;
break;
}
case 0x03:
{
PowerIsOn = false;
break;
}
}
}
public void PowerOff()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x00, 0x03, 0xFF });
SendPowerQuery();
}
public void PowerToggle()
{
if (PowerIsOnFeedback.BoolValue)
PowerOff();
else
PowerOn();
}
public void PanLeft()
{
SendPanTiltCommand(new byte[] { 0x01, 0x03 }, false);
// IsMoving = true;
}
public void PanRight()
{
SendPanTiltCommand(new byte[] { 0x02, 0x03 }, false);
// IsMoving = true;
}
public void PanStop()
{
Stop();
}
public void TiltDown()
{
SendPanTiltCommand(new byte[] { 0x03, 0x02 }, false);
// IsMoving = true;
}
public void TiltUp()
{
SendPanTiltCommand(new byte[] { 0x03, 0x01 }, false);
// IsMoving = true;
}
public void TiltStop()
{
Stop();
}
private void SendZoomCommand(byte cmd)
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x07, cmd, 0xFF });
}
public void ZoomIn()
{
SendZoomCommand(ZoomInCmd);
IsZooming = true;
}
public void ZoomOut()
{
SendZoomCommand(ZoomOutCmd);
IsZooming = true;
}
public void ZoomStop()
{
Stop();
}
public void Stop()
{
if (IsZooming)
{
SendZoomCommand(ZoomStopCmd);
IsZooming = false;
}
else
{
StopSpeedTimer();
SendPanTiltCommand(new byte[] { 0x03, 0x03 }, false);
// IsMoving = false;
}
}
public void PositionHome()
{
SendBytes(new byte[] { ID, 0x01, 0x06, 0x02, PanSpeedFast, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF });
SendBytes(new byte[] { ID, 0x01, 0x04, 0x47, 0x00, 0x00, 0x00, 0x00, 0xFF });
}
public void RecallPreset(int presetNumber)
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x02, (byte)presetNumber, 0xFF });
}
public void SavePreset(int presetNumber)
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x3F, 0x01, (byte)presetNumber, 0xFF });
}
#region IHasCameraPresets Members
public event EventHandler<EventArgs> PresetsListHasChanged;
/// <summary>
/// Raises the PresetsListHasChanged event
/// </summary>
protected void OnPresetsListHasChanged()
{
var handler = PresetsListHasChanged;
if (handler == null)
return;
handler.Invoke(this, EventArgs.Empty);
}
public List<CameraPreset> Presets { get; private set; }
public void PresetSelect(int preset)
{
RecallPreset(preset);
}
public void PresetStore(int preset, string description)
{
SavePreset(preset);
}
#endregion
#region IHasCameraFocusControl Members
public void FocusNear()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x03, 0xFF });
}
public void FocusFar()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x02, 0xFF });
}
public void FocusStop()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x08, 0x00, 0xFF });
}
public void TriggerAutoFocus()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x18, 0x01, 0xFF });
SendAutoFocusQuery();
}
#endregion
#region IHasAutoFocus Members
public void SetFocusModeAuto()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x02, 0xFF });
SendAutoFocusQuery();
}
public void SetFocusModeManual()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x03, 0xFF });
SendAutoFocusQuery();
}
public void ToggleFocusMode()
{
SendBytes(new byte[] { ID, 0x01, 0x04, 0x38, 0x10, 0xFF });
SendAutoFocusQuery();
}
#endregion
void SendAutoFocusQuery()
{
SendBytes(new byte[] { ID, 0x09, 0x04, 0x38, 0xFF });
InquiryResponseQueue.Enqueue(HandleAutoFocusResponse);
}
void HandleAutoFocusResponse(byte[] response)
{
switch (response[0])
{
case 0x02:
{
// Auto Mode
PowerIsOn = true;
break;
}
case 0x03:
{
// Manual Mode
PowerIsOn = false;
break;
}
}
}
#region IHasCameraOff Members
public BoolFeedback CameraIsOffFeedback { get; private set; }
public void CameraOff()
{
PowerOff();
}
#endregion
}
public class CameraViscaFactory : EssentialsDeviceFactory<CameraVisca>
{
public CameraViscaFactory()
{
TypeNames = new List<string>() { "cameravisca" };
}
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new CameraVisca Device");
var comm = CommFactory.CreateCommForDevice(dc);
var props = Newtonsoft.Json.JsonConvert.DeserializeObject<Cameras.CameraViscaPropertiesConfig>(
dc.Properties.ToString());
return new Cameras.CameraVisca(dc.Key, dc.Name, comm, props);
}
}
public class CameraViscaPropertiesConfig : CameraPropertiesConfig
{
/// <summary>
/// Control ID of the camera (1-7)
/// </summary>
[JsonProperty("id")]
public uint Id { get; set; }
/// <summary>
/// Slow Pan speed (0-18)
/// </summary>
[JsonProperty("panSpeedSlow")]
public uint PanSpeedSlow { get; set; }
/// <summary>
/// Fast Pan speed (0-18)
/// </summary>
[JsonProperty("panSpeedFast")]
public uint PanSpeedFast { get; set; }
/// <summary>
/// Slow tilt speed (0-18)
/// </summary>
[JsonProperty("tiltSpeedSlow")]
public uint TiltSpeedSlow { get; set; }
/// <summary>
/// Fast tilt speed (0-18)
/// </summary>
[JsonProperty("tiltSpeedFast")]
public uint TiltSpeedFast { get; set; }
/// <summary>
/// Time a button must be held before fast speed is engaged (Milliseconds)
/// </summary>
[JsonProperty("fastSpeedHoldTimeMs")]
public uint FastSpeedHoldTimeMs { get; set; }
}

View file

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

View file

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

View file

@ -8,7 +8,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
/// <summary>
/// Interface for devices that have cameras with controls
/// </summary>
public interface IHasCamerasWithControls : IKeyName, IKeyed
public interface IHasCamerasWithControls : IKeyName
{
/// <summary>
/// List of cameras on the device. This should be a list of IHasCameraControls objects

View file

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

View file

@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using System.Timers;
using PepperDash.Core;
using Serilog.Events;
@ -56,16 +56,14 @@ namespace PepperDash.Essentials.Devices.Common.Codec
}
}
private readonly CTimer _scheduleChecker;
private Timer _scheduleChecker;
/// <summary>
/// Initializes a new instance of the CodecScheduleAwareness class with default poll time
/// </summary>
public CodecScheduleAwareness()
{
Meetings = new List<Meeting>();
_scheduleChecker = new CTimer(CheckSchedule, null, 1000, 1000);
Init(1000);
}
/// <summary>
@ -73,10 +71,17 @@ namespace PepperDash.Essentials.Devices.Common.Codec
/// </summary>
/// <param name="pollTime">The poll time in milliseconds for checking schedule changes</param>
public CodecScheduleAwareness(long pollTime)
{
Init(pollTime);
}
private void Init(long pollTime)
{
Meetings = new List<Meeting>();
_scheduleChecker = new CTimer(CheckSchedule, null, pollTime, pollTime);
_scheduleChecker = new Timer(pollTime) { AutoReset = true };
_scheduleChecker.Elapsed += (s, e) => CheckSchedule(null);
_scheduleChecker.Start();
}
/// <summary>

View file

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
using PepperDash.Core;
@ -207,22 +207,26 @@ public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IBridgeAdvanced
{
_IsWarmingUp = true;
IsWarmingUpFeedback.FireUpdate();
new CTimer(o =>
var t = new Timer(10000) { AutoReset = false };
t.Elapsed += (s, e) =>
{
_IsWarmingUp = false;
IsWarmingUpFeedback.FireUpdate();
}, 10000);
};
t.Start();
}
void StartCoolingTimer()
{
_IsCoolingDown = true;
IsCoolingDownFeedback.FireUpdate();
new CTimer(o =>
var t = new Timer(7000) { AutoReset = false };
t.Elapsed += (s, e) =>
{
_IsCoolingDown = false;
IsCoolingDownFeedback.FireUpdate();
}, 7000);
};
t.Start();
}
#region IRoutingSink Members

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using PepperDash.Core;
@ -103,12 +103,12 @@ public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources,
/// <summary>
/// Timer used for managing display warmup timing.
/// </summary>
protected CTimer WarmupTimer;
protected Timer WarmupTimer;
/// <summary>
/// Timer used for managing display cooldown timing.
/// </summary>
protected CTimer CooldownTimer;
protected Timer CooldownTimer;
#region IRoutingInputs Members

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro.DeviceSupport;
using PepperDash.Core;
using PepperDash.Essentials.Core;
@ -17,12 +17,14 @@ namespace PepperDash.Essentials.Devices.Common.Displays;
/// </summary>
public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs<string>, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback
{
/// <inheritdoc />
public ISelectableItems<string> Inputs { get; private set; }
bool _PowerIsOn;
bool _IsWarmingUp;
bool _IsCoolingDown;
/// <inheritdoc />
protected override Func<bool> PowerIsOnFeedbackFunc
{
get
@ -33,6 +35,8 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
};
}
}
/// <inheritdoc />
protected override Func<bool> IsCoolingDownFeedbackFunc
{
get
@ -43,6 +47,8 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
};
}
}
/// <inheritdoc />
protected override Func<bool> IsWarmingUpFeedbackFunc
{
get
@ -53,13 +59,21 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
};
}
}
/// <inheritdoc />
protected override Func<string> CurrentInputFeedbackFunc { get { return () => Inputs.CurrentItem; } }
int VolumeHeldRepeatInterval = 200;
ushort VolumeInterval = 655;
ushort _FakeVolumeLevel = 31768;
bool _IsMuted;
Timer _volumeUpTimer;
Timer _volumeDownTimer;
/// <summary>
/// Constructor for MockDisplay
/// </summary> <param name="key"></param>
/// <param name="name"></param>
public MockDisplay(string key, string name)
: base(key, name)
{
@ -108,16 +122,19 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
_IsWarmingUp = true;
IsWarmingUpFeedback.InvokeFireUpdate();
// Fake power-up cycle
WarmupTimer = new CTimer(o =>
WarmupTimer = new Timer(WarmupTime) { AutoReset = false };
WarmupTimer.Elapsed += (s, e) =>
{
_IsWarmingUp = false;
_PowerIsOn = true;
IsWarmingUpFeedback.InvokeFireUpdate();
PowerIsOnFeedback.InvokeFireUpdate();
}, WarmupTime);
};
WarmupTimer.Start();
}
}
/// <inheritdoc />
public override void PowerOff()
{
// If a display has unreliable-power off feedback, just override this and
@ -127,17 +144,20 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
_IsCoolingDown = true;
IsCoolingDownFeedback.InvokeFireUpdate();
// Fake cool-down cycle
CooldownTimer = new CTimer(o =>
CooldownTimer = new Timer(CooldownTime) { AutoReset = false };
CooldownTimer.Elapsed += (s, e) =>
{
Debug.LogMessage(LogEventLevel.Verbose, "Cooldown timer ending", this);
_IsCoolingDown = false;
IsCoolingDownFeedback.InvokeFireUpdate();
_PowerIsOn = false;
PowerIsOnFeedback.InvokeFireUpdate();
}, CooldownTime);
};
CooldownTimer.Start();
}
}
/// <inheritdoc />
public override void PowerToggle()
{
if (PowerIsOnFeedback.BoolValue && !IsWarmingUpFeedback.BoolValue)
@ -146,6 +166,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
PowerOn();
}
/// <inheritdoc />
public override void ExecuteSwitch(object selector)
{
try
@ -184,6 +205,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
}
}
/// <inheritdoc />
public void SetInput(string selector)
{
ISelectableItem currentInput = null;
@ -206,6 +228,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
#region IBasicVolumeWithFeedback Members
/// <inheritdoc />
public IntFeedback VolumeLevelFeedback { get; private set; }
/// <summary>
@ -245,32 +268,44 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
#region IBasicVolumeControls Members
/// <inheritdoc />
public void VolumeUp(bool pressRelease)
{
//while (pressRelease)
//{
Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Down {0}", pressRelease);
if (pressRelease)
{
var newLevel = _FakeVolumeLevel + VolumeInterval;
SetVolume((ushort)newLevel);
CrestronEnvironment.Sleep(VolumeHeldRepeatInterval);
SetVolume((ushort)(_FakeVolumeLevel + VolumeInterval));
if (_volumeUpTimer == null)
{
_volumeUpTimer = new Timer(VolumeHeldRepeatInterval) { AutoReset = true };
_volumeUpTimer.Elapsed += (s, e) => SetVolume((ushort)(_FakeVolumeLevel + VolumeInterval));
_volumeUpTimer.Start();
}
}
else
{
_volumeUpTimer?.Stop();
_volumeUpTimer = null;
}
//}
}
/// <inheritdoc />
public void VolumeDown(bool pressRelease)
{
//while (pressRelease)
//{
Debug.LogMessage(LogEventLevel.Verbose, this, "Volume Up {0}", pressRelease);
if (pressRelease)
{
var newLevel = _FakeVolumeLevel - VolumeInterval;
SetVolume((ushort)newLevel);
CrestronEnvironment.Sleep(VolumeHeldRepeatInterval);
SetVolume((ushort)(_FakeVolumeLevel - VolumeInterval));
if (_volumeDownTimer == null)
{
_volumeDownTimer = new Timer(VolumeHeldRepeatInterval) { AutoReset = true };
_volumeDownTimer.Elapsed += (s, e) => SetVolume((ushort)(_FakeVolumeLevel - VolumeInterval));
_volumeDownTimer.Start();
}
}
else
{
_volumeDownTimer?.Stop();
_volumeDownTimer = null;
}
//}
}
/// <summary>
@ -284,6 +319,7 @@ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeA
#endregion
/// <inheritdoc />
public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
{
LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge);
@ -303,6 +339,7 @@ public class MockDisplayFactory : EssentialsDeviceFactory<MockDisplay>
TypeNames = new List<string>() { "mockdisplay", "mockdisplay2" };
}
/// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Mock Display Device");

View file

@ -1,5 +1,5 @@
using System.Collections.Generic;
using Crestron.SimplSharp;
using System.Timers;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
@ -84,7 +84,9 @@ public class RelayControlledShade : ShadeBase, IShadesOpenCloseStop
void PulseOutput(ISwitchedOutput output, int pulseTime)
{
output.On();
CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime);
var pulseTimer = new Timer(pulseTime) { AutoReset = false };
pulseTimer.Elapsed += (s, e) => output.Off();
pulseTimer.Start();
}
/// <summary>

View file

@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -170,12 +170,16 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
OnCallStatusChange(call);
//ActiveCallCountFeedback.FireUpdate();
// Simulate 2-second ring, then connecting, then connected
new CTimer(o =>
var dialTimer = new Timer(2000) { AutoReset = false };
dialTimer.Elapsed += (s, e) =>
{
call.Type = eCodecCallType.Video;
SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call);
new CTimer(oo => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000);
}, 2000);
var connectTimer = new Timer(1000) { AutoReset = false };
connectTimer.Elapsed += (ss, ee) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call);
connectTimer.Start();
};
dialTimer.Start();
}
/// <inheritdoc />
@ -188,12 +192,16 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
//ActiveCallCountFeedback.FireUpdate();
// Simulate 2-second ring, then connecting, then connected
new CTimer(o =>
var dialMeetingTimer = new Timer(2000) { AutoReset = false };
dialMeetingTimer.Elapsed += (s, e) =>
{
call.Type = eCodecCallType.Video;
SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call);
new CTimer(oo => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000);
}, 2000);
var connectTimer = new Timer(1000) { AutoReset = false };
connectTimer.Elapsed += (ss, ee) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call);
connectTimer.Start();
};
dialMeetingTimer.Start();
}
@ -226,7 +234,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
{
Debug.LogMessage(LogEventLevel.Debug, this, "AcceptCall");
SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connecting, call);
new CTimer(o => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call), 1000);
var acceptTimer = new Timer(1000) { AutoReset = false };
acceptTimer.Elapsed += (s, e) => SetNewCallStatusAndFireCallStatusChange(eCodecCallStatus.Connected, call);
acceptTimer.Start();
// should already be in active list
}
@ -624,7 +634,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
SupportsCameraOff = false;
Cameras = new List<CameraBase>();
Cameras = new List<IHasCameraControls>();
var internalCamera = new MockVCCamera(Key + "-camera1", "Near End", this);
@ -679,19 +689,19 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
/// <summary>
/// CameraSelected event. Fired when a camera is selected
/// </summary>
public event EventHandler<CameraSelectedEventArgs> CameraSelected;
public event EventHandler<CameraSelectedEventArgs<IHasCameraControls>> CameraSelected;
/// <summary>
/// Gets the list of cameras associated with this codec
/// </summary>
public List<CameraBase> Cameras { get; private set; }
public List<IHasCameraControls> Cameras { get; private set; }
private CameraBase _selectedCamera;
private IHasCameraControls _selectedCamera;
/// <summary>
/// Returns the selected camera
/// </summary>
public CameraBase SelectedCamera
public IHasCameraControls SelectedCamera
{
get
{
@ -702,7 +712,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
_selectedCamera = value;
SelectedCameraFeedback.FireUpdate();
ControllingFarEndCameraFeedback.FireUpdate();
CameraSelected?.Invoke(this, new CameraSelectedEventArgs(SelectedCamera));
CameraSelected?.Invoke(this, new CameraSelectedEventArgs<IHasCameraControls>(SelectedCamera));
}
}

View file

@ -24,10 +24,19 @@ using System.Threading.Tasks;
namespace PepperDash.Essentials.Devices.Common.VideoCodec;
/// <summary>
/// Base class for video codecs. Contains common properties, methods, and feedback for video codecs.
/// Also contains the logic to link commonly implemented interfaces to the API bridge.
/// </summary>
public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs,
IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode
{
private const int XSigEncoding = 28591;
/// <summary>
/// Maximum number of participants to display in the participant list.
/// This is used to limit the number of XSigs that need to be updated and processed on the control system when there are many participants in a call.
/// </summary>
protected const int MaxParticipants = 50;
private readonly byte[] _clearBytes = XSigHelpers.ClearOutputs();
@ -35,6 +44,10 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
private readonly BasicTriList _directoryTrilist;
private readonly VideoCodecControllerJoinMap _directoryJoinmap;
/// <summary>
/// Time format specifier for any time-related XSigs.
/// This should be set by the inheriting class based on the expected format of the codec (e.g. "hh:mm:ss" or "mm:ss")
/// </summary>
protected string _timeFormatSpecifier;
/// <summary>
@ -56,7 +69,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
SharingSourceFeedback = new StringFeedback("sharingSource", SharingSourceFeedbackFunc);
SharingContentIsOnFeedback = new BoolFeedback("sharingContentIsOn", SharingContentIsOnFeedbackFunc);
// TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set
MeetingsToDisplayFeedback = new IntFeedback(() => MeetingsToDisplay);
InputPorts = new RoutingPortCollection<RoutingInputPort>();
@ -110,7 +122,10 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
/// </summary>
public bool ShowSelfViewByDefault { get; protected set; }
/// <inheritdoc />
public bool SupportsCameraOff { get; protected set; }
/// <inheritdoc />
public bool SupportsCameraAutoMode { get; protected set; }
/// <summary>
@ -282,6 +297,8 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
/// Sends DTMF tones
/// </summary>
public abstract void SendDtmf(string s);
/// <inheritdoc />
public virtual void SendDtmf(string s, CodecActiveCallItem call) { }
#endregion
@ -746,7 +763,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
{
if (!args.DeviceOnLine) return;
// TODO [ ] Issue #868
trilist.SetString(joinMap.CurrentParticipants.JoinNumber, "\xFC");
UpdateParticipantsXSig(codec, trilist, joinMap);
};
@ -974,10 +990,8 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
{
if (!args.DeviceOnLine) return;
// TODO [ ] Issue #868
trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC");
UpdateMeetingsList(codec, trilist, joinMap);
// TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set
MeetingsToDisplayFeedback.LinkInputSig(trilist.UShortInput[joinMap.MeetingsToDisplay.JoinNumber]);
};
}
@ -1006,16 +1020,14 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
{
if (!args.DeviceOnLine) return;
// TODO [ ] Issue #868
trilist.SetString(joinMap.Schedule.JoinNumber, "\xFC");
UpdateMeetingsListXSig(_currentMeetings);
};
}
// TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set
private int _meetingsToDisplay = 3;
// TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set
/// <inheritdoc />
protected int MeetingsToDisplay
{
get { return _meetingsToDisplay; }
@ -1026,12 +1038,13 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
}
}
// TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set
/// <inheritdoc />
public IntFeedback MeetingsToDisplayFeedback { get; set; }
private string UpdateMeetingsListXSig(List<Meeting> meetings)
{
// TODO [ ] hotfix/videocodecbase-max-meeting-xsig-set
//const int _meetingsToDisplay = 3;
const int maxDigitals = 2;
const int maxStrings = 7;
@ -1291,7 +1304,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
Debug.LogMessage(LogEventLevel.Verbose, this, "Creating XSIG token array with size {0}", maxMethods * offset);
// TODO: Add code to generate XSig data
foreach (var method in contact.ContactMethods)
{
if (arrayIndex >= maxMethods * offset)
@ -1498,7 +1510,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
{
if (!args.DeviceOnLine) return;
// TODO [ ] #983
Debug.LogMessage(LogEventLevel.Information, this, "LinkVideoCodecCallControlsToApi: device is {0}, IsInCall {1}", args.DeviceOnLine ? "online" : "offline", IsInCall);
trilist.SetBool(joinMap.HookState.JoinNumber, IsInCall);
trilist.SetString(joinMap.CurrentCallData.JoinNumber, "\xFC");
@ -1531,12 +1542,10 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
tokenArray[stringIndex + 2] = new XSigSerialToken(stringIndex + 3, call.Direction.ToString());
tokenArray[stringIndex + 3] = new XSigSerialToken(stringIndex + 4, call.Type.ToString());
tokenArray[stringIndex + 4] = new XSigSerialToken(stringIndex + 5, call.Status.ToString());
if (call.Duration != null)
{
// May need to verify correct string format here
var dur = string.Format("{0:c}", call.Duration);
tokenArray[arrayIndex + 6] = new XSigSerialToken(stringIndex + 6, dur);
}
// May need to verify correct string format here
var dur = string.Format("{0:c}", call.Duration);
tokenArray[stringIndex + 5] = new XSigSerialToken(stringIndex + 6, dur);
arrayIndex += offset;
stringIndex += maxStrings;
@ -1879,7 +1888,6 @@ public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutpu
{
if (!args.DeviceOnLine) return;
// TODO [ ] Issue #868
trilist.SetString(joinMap.CameraPresetNames.JoinNumber, "\xFC");
SetCameraPresetNames(presetCodec.NearEndPresets);
};