Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
94909d2c7c Finalize CallStatusMessenger implementation - remove test files and verify clean implementation
Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com>
2025-08-13 21:16:35 +00:00
copilot-swe-agent[bot]
9946e9a9ae Implement CallStatusMessenger for interface-based devices and update MobileControlSystemController
Co-authored-by: ngenovese11 <23391587+ngenovese11@users.noreply.github.com>
2025-08-13 21:14:29 +00:00
copilot-swe-agent[bot]
db80b7b501 Initial plan 2025-08-13 20:58:29 +00:00
30 changed files with 866 additions and 1188 deletions

1
.gitignore vendored
View File

@@ -395,4 +395,3 @@ essentials-framework/Essentials Interfaces/PepperDash_Essentials_Interfaces/Pepp
_site/
api/
*.DS_Store
/._PepperDash.Essentials.4Series.sln

View File

@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<Version>2.15.1-local</Version>
<Version>2.12.1-local</Version>
<InformationalVersion>$(Version)</InformationalVersion>
<Authors>PepperDash Technology</Authors>
<Company>PepperDash Technology</Company>

View File

@@ -74,10 +74,6 @@ namespace PepperDash.Core
/// <summary>
/// Secure TCP/IP
/// </summary>
SecureTcpIp,
/// <summary>
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction
/// </summary>
ComBridge
SecureTcpIp
}
}

View File

@@ -43,7 +43,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.157" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />

View File

@@ -2,12 +2,14 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Crestron.SimplSharp;
using System.Reflection;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
using Crestron.SimplSharpPro.EthernetCommunication;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
using Serilog.Events;
@@ -353,22 +355,22 @@ namespace PepperDash.Essentials.Core.Bridges
/// </summary>
public class EiscApiPropertiesConfig
{
[JsonProperty("control")]
/// <summary>
/// Gets or sets the Control
/// </summary>
[JsonProperty("control")]
public EssentialsControlPropertiesConfig Control { get; set; }
[JsonProperty("devices")]
/// <summary>
/// Gets or sets the Devices
/// </summary>
[JsonProperty("devices")]
public List<ApiDevicePropertiesConfig> Devices { get; set; }
[JsonProperty("rooms")]
/// <summary>
/// Gets or sets the Rooms
/// </summary>
[JsonProperty("rooms")]
public List<ApiRoomPropertiesConfig> Rooms { get; set; }
@@ -377,22 +379,22 @@ namespace PepperDash.Essentials.Core.Bridges
/// </summary>
public class ApiDevicePropertiesConfig
{
[JsonProperty("deviceKey")]
/// <summary>
/// Gets or sets the DeviceKey
/// </summary>
[JsonProperty("deviceKey")]
public string DeviceKey { get; set; }
[JsonProperty("joinStart")]
/// <summary>
/// Gets or sets the JoinStart
/// </summary>
[JsonProperty("joinStart")]
public uint JoinStart { get; set; }
[JsonProperty("joinMapKey")]
/// <summary>
/// Gets or sets the JoinMapKey
/// </summary>
[JsonProperty("joinMapKey")]
public string JoinMapKey { get; set; }
}
@@ -401,22 +403,22 @@ namespace PepperDash.Essentials.Core.Bridges
/// </summary>
public class ApiRoomPropertiesConfig
{
[JsonProperty("roomKey")]
/// <summary>
/// Gets or sets the RoomKey
/// </summary>
[JsonProperty("roomKey")]
public string RoomKey { get; set; }
[JsonProperty("joinStart")]
/// <summary>
/// Gets or sets the JoinStart
/// </summary>
[JsonProperty("joinStart")]
public uint JoinStart { get; set; }
[JsonProperty("joinMapKey")]
/// <summary>
/// Gets or sets the JoinMapKey
/// </summary>
[JsonProperty("joinMapKey")]
public string JoinMapKey { get; set; }
}

View File

@@ -7,13 +7,6 @@ namespace PepperDash.Essentials.Core.Bridges
/// </summary>
public interface IBridgeAdvanced
{
/// <summary>
/// Links the bridge to the API using the provided trilist, join start, join map key, and bridge.
/// </summary>
/// <param name="trilist">The trilist to link to.</param>
/// <param name="joinStart">The starting join number.</param>
/// <param name="joinMapKey">The key for the join map.</param>
/// <param name="bridge">The EISC API bridge.</param>
void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge);
}
}

View File

@@ -1,138 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using Crestron.SimplSharp.CrestronSockets;
using Crestron.SimplSharpPro.DeviceSupport;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core.Bridges;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Devices;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Implements IBasicCommunication and sends all communication through an EISC
/// </summary>
[Description("Generic communication wrapper class for any IBasicCommunication type")]
public class CommBridge : EssentialsBridgeableDevice, IBasicCommunication
{
private EiscApiAdvanced eisc;
private IBasicCommunicationJoinMap joinMap;
/// <summary>
/// Event triggered when text is received through the communication bridge.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Event triggered when bytes are received through the communication bridge.
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Indicates whether the communication bridge is currently connected.
/// </summary>
public bool IsConnected { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="CommBridge"/> class.
/// </summary>
/// <param name="key">The unique key for the communication bridge.</param>
/// <param name="name">The display name for the communication bridge.</param>
public CommBridge(string key, string name)
: base(key, name)
{
}
/// <summary>
/// Sends a byte array through the communication bridge.
/// </summary>
/// <param name="bytes">The byte array to send.</param>
public void SendBytes(byte[] bytes)
{
if (eisc == null)
{
this.LogWarning("EISC is null, cannot send bytes.");
return;
}
eisc.Eisc.SetString(joinMap.SendText.JoinNumber, Encoding.ASCII.GetString(bytes, 0, bytes.Length));
}
/// <summary>
/// Sends a text string through the communication bridge.
/// </summary>
/// <param name="text">The text string to send.</param>
public void SendText(string text)
{
if (eisc == null)
{
this.LogWarning("EISC is null, cannot send text.");
return;
}
eisc.Eisc.SetString(joinMap.SendText.JoinNumber, text);
}
/// <summary>
/// Initiates a connection through the communication bridge.
/// </summary>
public void Connect()
{
if (eisc == null)
{
this.LogWarning("EISC is null, cannot connect.");
return;
}
eisc.Eisc.SetBool(joinMap.Connect.JoinNumber, true);
}
/// <summary>
/// Terminates the connection through the communication bridge.
/// </summary>
public void Disconnect()
{
if (eisc == null)
{
this.LogWarning("EISC is null, cannot disconnect.");
return;
}
eisc.Eisc.SetBool(joinMap.Connect.JoinNumber, false);
}
/// <inheritdoc />
public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
{
joinMap = new IBasicCommunicationJoinMap(joinStart);
var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
if (!string.IsNullOrEmpty(joinMapSerialized))
joinMap = JsonConvert.DeserializeObject<IBasicCommunicationJoinMap>(joinMapSerialized);
if (bridge != null)
{
bridge.AddJoinMap(Key, joinMap);
}
else
{
this.LogWarning("Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
}
this.LogDebug("Linking to Trilist '{0}'", trilist.ID.ToString("X"));
eisc = bridge;
trilist.SetBoolSigAction(joinMap.Connected.JoinNumber, (b) => IsConnected = b);
trilist.SetStringSigAction(joinMap.TextReceived.JoinNumber, (s) =>
{
TextReceived?.Invoke(this, new GenericCommMethodReceiveTextArgs(s));
BytesReceived?.Invoke(this, new GenericCommMethodReceiveBytesArgs(Encoding.ASCII.GetBytes(s)));
});
}
}
}

View File

@@ -1,9 +1,11 @@
using System;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DM;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
using Serilog.Events;
@@ -36,9 +38,9 @@ namespace PepperDash.Essentials.Core
/// Returns a comm method of either com port, TCP, SSH, and puts this into the DeviceManager
/// </summary>
/// <param name="deviceConfig">The Device config object</param>
/// <summary>
/// CreateCommForDevice method
/// </summary>
/// <summary>
/// CreateCommForDevice method
/// </summary>
public static IBasicCommunication CreateCommForDevice(DeviceConfig deviceConfig)
{
EssentialsControlPropertiesConfig controlConfig = GetControlPropertiesConfig(deviceConfig);
@@ -54,38 +56,35 @@ namespace PepperDash.Essentials.Core
case eControlMethod.Com:
comm = new ComPortController(deviceConfig.Key + "-com", GetComPort, controlConfig.ComParams.Value, controlConfig);
break;
case eControlMethod.ComBridge:
comm = new CommBridge(deviceConfig.Key + "-simpl", deviceConfig.Name + " Simpl");
break;
case eControlMethod.Cec:
comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort, controlConfig);
break;
case eControlMethod.Cec:
comm = new CecPortController(deviceConfig.Key + "-cec", GetCecPort, controlConfig);
break;
case eControlMethod.IR:
break;
case eControlMethod.Ssh:
{
var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password);
ssh.AutoReconnect = c.AutoReconnect;
if (ssh.AutoReconnect)
ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = ssh;
break;
}
case eControlMethod.Tcpip:
{
var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize);
tcp.AutoReconnect = c.AutoReconnect;
if (tcp.AutoReconnect)
tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = tcp;
break;
}
case eControlMethod.Udp:
{
var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize);
comm = udp;
break;
}
case eControlMethod.Ssh:
{
var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password);
ssh.AutoReconnect = c.AutoReconnect;
if(ssh.AutoReconnect)
ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = ssh;
break;
}
case eControlMethod.Tcpip:
{
var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize);
tcp.AutoReconnect = c.AutoReconnect;
if (tcp.AutoReconnect)
tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = tcp;
break;
}
case eControlMethod.Udp:
{
var udp = new GenericUdpServer(deviceConfig.Key + "-udp", c.Address, c.Port, c.BufferSize);
comm = udp;
break;
}
case eControlMethod.Telnet:
break;
case eControlMethod.SecureTcpIp:
@@ -99,7 +98,7 @@ namespace PepperDash.Essentials.Core
}
default:
break;
}
}
}
catch (Exception e)
{
@@ -108,14 +107,15 @@ namespace PepperDash.Essentials.Core
}
// put it in the device manager if it's the right flavor
if (comm is Device comDev)
var comDev = comm as Device;
if (comDev != null)
DeviceManager.AddDevice(comDev);
return comm;
}
/// <summary>
/// GetComPort method
/// </summary>
/// <summary>
/// GetComPort method
/// </summary>
public static ComPort GetComPort(EssentialsControlPropertiesConfig config)
{
var comPar = config.ComParams;
@@ -126,74 +126,74 @@ namespace PepperDash.Essentials.Core
return null;
}
/// <summary>
/// Gets an ICec port from a RoutingInput or RoutingOutput on a device
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
/// <summary>
/// GetCecPort method
/// </summary>
public static ICec GetCecPort(ControlPropertiesConfig config)
{
try
{
var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey);
/// <summary>
/// Gets an ICec port from a RoutingInput or RoutingOutput on a device
/// </summary>
/// <param name="config"></param>
/// <returns></returns>
/// <summary>
/// GetCecPort method
/// </summary>
public static ICec GetCecPort(ControlPropertiesConfig config)
{
try
{
var dev = DeviceManager.GetDeviceForKey(config.ControlPortDevKey);
Debug.LogMessage(LogEventLevel.Information, "GetCecPort: device '{0}' {1}", config.ControlPortDevKey, dev == null
? "is not valid, failed to get cec port"
: "found in device manager, attempting to get cec port");
Debug.LogMessage(LogEventLevel.Information, "GetCecPort: device '{0}' {1}", config.ControlPortDevKey, dev == null
? "is not valid, failed to get cec port"
: "found in device manager, attempting to get cec port");
if (dev == null)
return null;
if (dev == null)
return null;
if (String.IsNullOrEmpty(config.ControlPortName))
{
Debug.LogMessage(LogEventLevel.Information, "GetCecPort: '{0}' - Configuration missing 'ControlPortName'", config.ControlPortDevKey);
return null;
}
if (String.IsNullOrEmpty(config.ControlPortName))
{
Debug.LogMessage(LogEventLevel.Information, "GetCecPort: '{0}' - Configuration missing 'ControlPortName'", config.ControlPortDevKey);
return null;
}
var inputsOutputs = dev as IRoutingInputsOutputs;
if (inputsOutputs == null)
{
var inputsOutputs = dev as IRoutingInputsOutputs;
if (inputsOutputs == null)
{
Debug.LogMessage(LogEventLevel.Information, "GetCecPort: Device '{0}' does not support IRoutingInputsOutputs, failed to get CEC port called '{1}'",
config.ControlPortDevKey, config.ControlPortName);
return null;
}
return null;
}
var inputPort = inputsOutputs.InputPorts[config.ControlPortName];
if (inputPort != null && inputPort.Port is ICec)
if (inputPort != null && inputPort.Port is ICec)
return inputPort.Port as ICec;
var outputPort = inputsOutputs.OutputPorts[config.ControlPortName];
if (outputPort != null && outputPort.Port is ICec)
return outputPort.Port as ICec;
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Debug, "GetCecPort Exception Message: {0}", ex.Message);
Debug.LogMessage(LogEventLevel.Verbose, "GetCecPort Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.LogMessage(LogEventLevel.Information, "GetCecPort Exception InnerException: {0}", ex.InnerException);
}
var outputPort = inputsOutputs.OutputPorts[config.ControlPortName];
if (outputPort != null && outputPort.Port is ICec)
return outputPort.Port as ICec;
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Debug, "GetCecPort Exception Message: {0}", ex.Message);
Debug.LogMessage(LogEventLevel.Verbose, "GetCecPort Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.LogMessage(LogEventLevel.Information, "GetCecPort Exception InnerException: {0}", ex.InnerException);
}
Debug.LogMessage(LogEventLevel.Information, "GetCecPort: Device '{0}' does not have a CEC port called '{1}'",
config.ControlPortDevKey, config.ControlPortName);
return null;
}
}
/// <summary>
/// Helper to grab the IComPorts device for this PortDeviceKey. Key "controlSystem" will
/// return the ControlSystem object from the Global class.
/// </summary>
/// <returns>IComPorts device or null if the device is not found or does not implement IComPorts</returns>
/// <summary>
/// GetIComPortsDeviceFromManagedDevice method
/// </summary>
/// <summary>
/// GetIComPortsDeviceFromManagedDevice method
/// </summary>
public static IComPorts GetIComPortsDeviceFromManagedDevice(string ComPortDevKey)
{
if ((ComPortDevKey.Equals("controlSystem", System.StringComparison.OrdinalIgnoreCase)
@@ -210,81 +210,81 @@ namespace PepperDash.Essentials.Core
}
}
/// <summary>
/// Represents a EssentialsControlPropertiesConfig
/// </summary>
public class EssentialsControlPropertiesConfig :
ControlPropertiesConfig
{
/// <summary>
/// Represents a EssentialsControlPropertiesConfig
/// </summary>
public class EssentialsControlPropertiesConfig :
ControlPropertiesConfig
{
[JsonProperty("comParams", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(ComSpecJsonConverter))]
public ComPort.ComPortSpec? ComParams { get; set; }
[JsonProperty("comParams", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(ComSpecJsonConverter))]
public ComPort.ComPortSpec? ComParams { get; set; }
[JsonProperty("cresnetId", NullValueHandling = NullValueHandling.Ignore)]
public string CresnetId { get; set; }
[JsonProperty("cresnetId", NullValueHandling = NullValueHandling.Ignore)]
public string CresnetId { get; set; }
/// <summary>
/// Attempts to provide uint conversion of string CresnetId
/// </summary>
[JsonIgnore]
public uint CresnetIdInt
{
get
{
try
{
return Convert.ToUInt32(CresnetId, 16);
}
catch (Exception)
{
throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId));
}
}
}
/// <summary>
/// Attempts to provide uint conversion of string CresnetId
/// </summary>
[JsonIgnore]
public uint CresnetIdInt
{
get
{
try
{
return Convert.ToUInt32(CresnetId, 16);
}
catch (Exception)
{
throw new FormatException(string.Format("ERROR:Unable to convert Cresnet ID: {0} to hex. Error:\n{1}", CresnetId));
}
}
}
[JsonProperty("infinetId", NullValueHandling = NullValueHandling.Ignore)]
/// <summary>
/// Gets or sets the InfinetId
/// </summary>
public string InfinetId { get; set; }
[JsonProperty("infinetId", NullValueHandling = NullValueHandling.Ignore)]
/// <summary>
/// Gets or sets the InfinetId
/// </summary>
public string InfinetId { get; set; }
/// <summary>
/// Attepmts to provide uiont conversion of string InifinetId
/// </summary>
[JsonIgnore]
public uint InfinetIdInt
{
get
{
try
{
return Convert.ToUInt32(InfinetId, 16);
}
catch (Exception)
{
throw new FormatException(string.Format("ERROR:Unable to conver Infinet ID: {0} to hex. Error:\n{1}", InfinetId));
}
}
}
}
/// <summary>
/// Attepmts to provide uiont conversion of string InifinetId
/// </summary>
[JsonIgnore]
public uint InfinetIdInt
{
get
{
try
{
return Convert.ToUInt32(InfinetId, 16);
}
catch (Exception)
{
throw new FormatException(string.Format("ERROR:Unable to conver Infinet ID: {0} to hex. Error:\n{1}", InfinetId));
}
}
}
}
/// <summary>
/// Represents a IrControlSpec
/// </summary>
public class IrControlSpec
{
/// <summary>
/// Gets or sets the PortDeviceKey
/// </summary>
public string PortDeviceKey { get; set; }
/// <summary>
/// Gets or sets the PortNumber
/// </summary>
public uint PortNumber { get; set; }
/// <summary>
/// Gets or sets the File
/// </summary>
public string File { get; set; }
}
/// <summary>
/// Represents a IrControlSpec
/// </summary>
public class IrControlSpec
{
/// <summary>
/// Gets or sets the PortDeviceKey
/// </summary>
public string PortDeviceKey { get; set; }
/// <summary>
/// Gets or sets the PortNumber
/// </summary>
public uint PortNumber { get; set; }
/// <summary>
/// Gets or sets the File
/// </summary>
public string File { get; set; }
}
}

View File

@@ -19,11 +19,5 @@ namespace PepperDash.Essentials.Core.Config
/// </summary>
[JsonProperty("multicastAudioAddress", NullValueHandling = NullValueHandling.Ignore)]
public string MulticastAudioAddress { get; set; }
/// <summary>
/// The URL for the streaming device's media stream.
/// </summary>
[JsonProperty("streamUrl", NullValueHandling = NullValueHandling.Ignore)]
public string StreamUrl { get; set; }
}
}

View File

@@ -18,41 +18,41 @@ namespace PepperDash.Essentials.Core.Config
/// </summary>
public class DeviceConfig
{
[JsonProperty("key")]
/// <summary>
/// Gets or sets the Key
/// </summary>
[JsonProperty("key")]
public string Key { get; set; }
[JsonProperty("uid")]
/// <summary>
/// Gets or sets the Uid
/// </summary>
[JsonProperty("uid")]
public int Uid { get; set; }
[JsonProperty("name")]
/// <summary>
/// Gets or sets the Name
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("group")]
/// <summary>
/// Gets or sets the Group
/// </summary>
[JsonProperty("group")]
public string Group { get; set; }
[JsonProperty("type")]
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("properties")]
[JsonConverter(typeof(DevicePropertiesConverter))]
/// <summary>
/// Gets or sets the Properties
/// </summary>
[JsonProperty("properties")]
[JsonConverter(typeof(DevicePropertiesConverter))]
public JToken Properties { get; set; }
public DeviceConfig(DeviceConfig dc)
@@ -68,7 +68,7 @@ namespace PepperDash.Essentials.Core.Config
//Properties = JToken.FromObject(dc.Properties);
}
public DeviceConfig() { }
public DeviceConfig() {}
}
/// <summary>

View File

@@ -1,7 +1,6 @@
using Crestron.SimplSharpPro.DM.Streaming;
using System;
using System.Collections.Generic;
using Crestron.SimplSharpPro.DM.Streaming;
using PepperDash.Core;
namespace PepperDash.Essentials.Core
{
@@ -12,7 +11,7 @@ namespace PepperDash.Essentials.Core
/// subscribers when the port information is updated. Implementations of this interface should ensure that the <see
/// cref="PortInformationChanged"/> event is raised whenever the <see cref="NetworkPorts"/> collection
/// changes.</remarks>
public interface INvxNetworkPortInformation : IKeyed
public interface INvxNetworkPortInformation
{
/// <summary>
/// Occurs when the port information changes.

View File

@@ -202,15 +202,14 @@ namespace PepperDash.Essentials.Core
private static void ListDevices(string s)
{
CrestronConsole.ConsoleCommandResponse($"{Devices.Count} Devices registered with Device Manager:\r\n");
Debug.LogMessage(LogEventLevel.Information, "{0} Devices registered with Device Manager:", Devices.Count);
var sorted = Devices.Values.ToList();
sorted.Sort((a, b) => a.Key.CompareTo(b.Key));
foreach (var d in sorted)
{
var name = d is IKeyName ? (d as IKeyName).Name : "---";
CrestronConsole.ConsoleCommandResponse($" [{d.Key}] {name}\r\n");
Debug.LogMessage(LogEventLevel.Information, " [{0}] {1}", d.Key, name);
}
}
@@ -219,17 +218,28 @@ namespace PepperDash.Essentials.Core
var dev = GetDeviceForKey(devKey);
if (dev == null)
{
CrestronConsole.ConsoleCommandResponse($"Device '{devKey}' not found\r\n");
Debug.LogMessage(LogEventLevel.Information, "Device '{0}' not found", devKey);
return;
}
if (!(dev is IHasFeedback statusDev))
{
CrestronConsole.ConsoleCommandResponse($"Device '{devKey}' does not have visible feedbacks\r\n");
Debug.LogMessage(LogEventLevel.Information, "Device '{0}' does not have visible feedbacks", devKey);
return;
}
statusDev.DumpFeedbacksToConsole(true);
}
//static void ListDeviceCommands(string devKey)
//{
// var dev = GetDeviceForKey(devKey);
// if (dev == null)
// {
// Debug.LogMessage(LogEventLevel.Information, "Device '{0}' not found", devKey);
// return;
// }
// Debug.LogMessage(LogEventLevel.Information, "This needs to be reworked. Stay tuned.", devKey);
//}
private static void ListDeviceCommStatuses(string input)
{
@@ -239,6 +249,12 @@ namespace PepperDash.Essentials.Core
}
}
//static void DoDeviceCommand(string command)
//{
// Debug.LogMessage(LogEventLevel.Information, "Not yet implemented. Stay tuned");
//}
/// <summary>
/// AddDevice method
/// </summary>

View File

@@ -3,29 +3,16 @@ using PepperDash.Essentials.Core.Bridges;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Base class for devices that can be bridged to an EISC API.
/// </summary>
public abstract class EssentialsBridgeableDevice : EssentialsDevice, IBridgeAdvanced
public abstract class EssentialsBridgeableDevice:EssentialsDevice, IBridgeAdvanced
{
/// <summary>
/// Initializes a new instance of the <see cref="EssentialsBridgeableDevice"/> class with the specified key.
/// </summary>
/// <param name="key">The unique key for the device.</param>
protected EssentialsBridgeableDevice(string key) : base(key)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EssentialsBridgeableDevice"/> class with the specified key and name.
/// </summary>
/// <param name="key">The unique key for the device.</param>
/// <param name="name">The display name for the device.</param>
protected EssentialsBridgeableDevice(string key, string name) : base(key, name)
{
}
/// <inheritdoc />
public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge);
}
}

View File

@@ -1,14 +1,14 @@
using System;
using System.Linq;
using Crestron.SimplSharp;
using PepperDash.Core;
using Serilog.Events;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IHasFeedback
/// </summary>
/// <summary>
/// Defines the contract for IHasFeedback
/// </summary>
public interface IHasFeedback : IKeyed
{
/// <summary>
@@ -19,72 +19,47 @@ namespace PepperDash.Essentials.Core
}
/// <summary>
/// Extension methods for IHasFeedback
/// </summary>
public static class IHasFeedbackExtensions
{
/// <summary>
/// Gets the feedback type name for sorting purposes
/// </summary>
/// <param name="feedback">The feedback to get the type name for</param>
/// <returns>A string representing the feedback type</returns>
private static string GetFeedbackTypeName(Feedback feedback)
{
if (feedback is BoolFeedback)
return "boolean";
else if (feedback is IntFeedback)
return "integer";
else if (feedback is StringFeedback)
return "string";
else
return feedback.GetType().Name;
}
/// <summary>
/// Dumps the feedbacks to the console
/// </summary>
/// <param name="source"></param>
/// <param name="getCurrentStates"></param>
public static void DumpFeedbacksToConsole(this IHasFeedback source, bool getCurrentStates)
{
Type t = source.GetType();
// get the properties and set them into a new collection of NameType wrappers
var props = t.GetProperties().Select(p => new PropertyNameType(p, t));
var feedbacks = source.Feedbacks;
if (feedbacks == null || feedbacks.Count == 0)
if (feedbacks != null)
{
CrestronConsole.ConsoleCommandResponse("No available feedbacks\r\n");
return;
}
CrestronConsole.ConsoleCommandResponse("Available feedbacks:\r\n");
// Sort feedbacks by type first, then by key
var sortedFeedbacks = feedbacks.OrderBy(f => GetFeedbackTypeName(f)).ThenBy(f => string.IsNullOrEmpty(f.Key) ? "" : f.Key);
foreach (var feedback in sortedFeedbacks)
{
string value = "";
string type = "";
if (getCurrentStates)
Debug.LogMessage(LogEventLevel.Information, source, "\n\nAvailable feedbacks:");
foreach (var f in feedbacks)
{
if (feedback is BoolFeedback)
string val = "";
string type = "";
if (getCurrentStates)
{
value = feedback.BoolValue.ToString();
type = "boolean";
}
else if (feedback is IntFeedback)
{
value = feedback.IntValue.ToString();
type = "integer";
}
else if (feedback is StringFeedback)
{
value = feedback.StringValue;
type = "string";
if (f is BoolFeedback)
{
val = f.BoolValue.ToString();
type = "boolean";
}
else if (f is IntFeedback)
{
val = f.IntValue.ToString();
type = "integer";
}
else if (f is StringFeedback)
{
val = f.StringValue;
type = "string";
}
}
Debug.LogMessage(LogEventLevel.Information, "{0,-12} {1, -25} {2}", type,
(string.IsNullOrEmpty(f.Key) ? "-no key-" : f.Key), val);
}
CrestronConsole.ConsoleCommandResponse($" {type,-12} {(string.IsNullOrEmpty(feedback.Key) ? "-no key-" : feedback.Key),-25} {value}\r\n");
}
else
Debug.LogMessage(LogEventLevel.Information, source, "No available outputs:");
}
}
}

View File

@@ -25,7 +25,7 @@
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.157" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
</ItemGroup>
<ItemGroup>
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />

View File

@@ -9,11 +9,11 @@ using Serilog.Events;
namespace PepperDash.Essentials.Room.Config
{
/// <summary>
/// Represents a EssentialsRoomConfigHelper
/// </summary>
public class EssentialsRoomConfigHelper
{
/// <summary>
/// Represents a EssentialsRoomConfigHelper
/// </summary>
public class EssentialsRoomConfigHelper
{
/// <summary>
/// GetEmergency method
/// </summary>
@@ -31,100 +31,100 @@ namespace PepperDash.Essentials.Room.Config
return null;
}
/// <summary>
///
/// </summary>
/// <param name="props"></param>
/// <param name="room"></param>
/// <returns></returns>
/// <summary>
/// GetMicrophonePrivacy method
/// </summary>
public static MicrophonePrivacyController GetMicrophonePrivacy(
EssentialsRoomPropertiesConfig props, IPrivacy room)
{
var microphonePrivacy = props.MicrophonePrivacy;
if (microphonePrivacy == null)
{
Debug.LogMessage(LogEventLevel.Information, "Cannot create microphone privacy with null properties");
return null;
}
// Get the MicrophonePrivacy device from the device manager
var mP = (DeviceManager.GetDeviceForKey(props.MicrophonePrivacy.DeviceKey) as MicrophonePrivacyController);
// Set this room as the IPrivacy device
if (mP == null)
{
Debug.LogMessage(LogEventLevel.Information, "ERROR: Selected device {0} is not MicrophonePrivacyController", props.MicrophonePrivacy.DeviceKey);
return null;
}
mP.SetPrivacyDevice(room);
/// <summary>
///
/// </summary>
/// <param name="props"></param>
/// <param name="room"></param>
/// <returns></returns>
/// <summary>
/// GetMicrophonePrivacy method
/// </summary>
public static MicrophonePrivacyController GetMicrophonePrivacy(
EssentialsRoomPropertiesConfig props, IPrivacy room)
{
var microphonePrivacy = props.MicrophonePrivacy;
if (microphonePrivacy == null)
{
Debug.LogMessage(LogEventLevel.Information, "Cannot create microphone privacy with null properties");
return null;
}
// Get the MicrophonePrivacy device from the device manager
var mP = (DeviceManager.GetDeviceForKey(props.MicrophonePrivacy.DeviceKey) as MicrophonePrivacyController);
// Set this room as the IPrivacy device
if (mP == null)
{
Debug.LogMessage(LogEventLevel.Information, "ERROR: Selected device {0} is not MicrophonePrivacyController", props.MicrophonePrivacy.DeviceKey);
return null;
}
mP.SetPrivacyDevice(room);
var behaviour = props.MicrophonePrivacy.Behaviour.ToLower();
var behaviour = props.MicrophonePrivacy.Behaviour.ToLower();
if (behaviour == null)
{
Debug.LogMessage(LogEventLevel.Information, "WARNING: No behaviour defined for MicrophonePrivacyController");
return null;
}
if (behaviour == "trackroomstate")
{
// Tie LED enable to room power state
if (behaviour == null)
{
Debug.LogMessage(LogEventLevel.Information, "WARNING: No behaviour defined for MicrophonePrivacyController");
return null;
}
if (behaviour == "trackroomstate")
{
// Tie LED enable to room power state
var essRoom = room as IEssentialsRoom;
essRoom.OnFeedback.OutputChange += (o, a) =>
{
{
if (essRoom.OnFeedback.BoolValue)
mP.EnableLeds = true;
else
mP.EnableLeds = false;
};
mP.EnableLeds = true;
else
mP.EnableLeds = false;
};
mP.EnableLeds = essRoom.OnFeedback.BoolValue;
}
else if (behaviour == "trackcallstate")
{
// Tie LED enable to room power state
}
else if (behaviour == "trackcallstate")
{
// Tie LED enable to room power state
var inCallRoom = room as IHasInCallFeedback;
inCallRoom.InCallFeedback.OutputChange += (o, a) =>
{
{
if (inCallRoom.InCallFeedback.BoolValue)
mP.EnableLeds = true;
else
mP.EnableLeds = false;
};
mP.EnableLeds = true;
else
mP.EnableLeds = false;
};
mP.EnableLeds = inCallRoom.InCallFeedback.BoolValue;
}
}
return mP;
}
return mP;
}
}
}
/// <summary>
/// Represents a EssentialsRoomPropertiesConfig
/// </summary>
public class EssentialsRoomPropertiesConfig
{
[JsonProperty("addresses")]
public EssentialsRoomAddressPropertiesConfig Addresses { get; set; }
/// <summary>
/// Represents a EssentialsRoomPropertiesConfig
/// </summary>
public class EssentialsRoomPropertiesConfig
{
[JsonProperty("addresses")]
public EssentialsRoomAddressPropertiesConfig Addresses { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("emergency")]
public EssentialsRoomEmergencyConfig Emergency { get; set; }
[JsonProperty("emergency")]
public EssentialsRoomEmergencyConfig Emergency { get; set; }
[JsonProperty("help")]
/// <summary>
/// Gets or sets the Help
/// </summary>
public EssentialsHelpPropertiesConfig Help { get; set; }
/// <summary>
/// Gets or sets the Help
/// </summary>
[JsonProperty("help")]
public EssentialsHelpPropertiesConfig Help { get; set; }
/// <summary>
/// Gets or sets the HelpMessage
/// </summary>
[JsonProperty("helpMessage")]
public string HelpMessage { get; set; }
[JsonProperty("helpMessage")]
/// <summary>
/// Gets or sets the HelpMessage
/// </summary>
public string HelpMessage { get; set; }
/// <summary>
/// Read this value to get the help message. It checks for the old and new config format.
@@ -133,94 +133,94 @@ namespace PepperDash.Essentials.Room.Config
{
get
{
if (Help != null && !string.IsNullOrEmpty(Help.Message))
if(Help != null && !string.IsNullOrEmpty(Help.Message))
{
return Help.Message;
}
else
{
return HelpMessage;
return HelpMessage;
}
}
}
/// <summary>
/// Gets or sets the Environment
/// </summary>
[JsonProperty("environment")]
public EssentialsEnvironmentPropertiesConfig Environment { get; set; }
[JsonProperty("environment")]
/// <summary>
/// Gets or sets the Environment
/// </summary>
public EssentialsEnvironmentPropertiesConfig Environment { get; set; }
/// <summary>
/// Gets or sets the LogoLight
/// </summary>
[JsonProperty("logo")]
public EssentialsLogoPropertiesConfig LogoLight { get; set; }
[JsonProperty("logo")]
/// <summary>
/// Gets or sets the LogoLight
/// </summary>
public EssentialsLogoPropertiesConfig LogoLight { get; set; }
[JsonProperty("logoDark")]
/// <summary>
/// Gets or sets the LogoDark
/// </summary>
[JsonProperty("logoDark")]
public EssentialsLogoPropertiesConfig LogoDark { get; set; }
[JsonProperty("microphonePrivacy")]
/// <summary>
/// Gets or sets the MicrophonePrivacy
/// </summary>
public EssentialsRoomMicrophonePrivacyConfig MicrophonePrivacy { get; set; }
/// <summary>
/// Gets or sets the MicrophonePrivacy
/// </summary>
[JsonProperty("microphonePrivacy")]
public EssentialsRoomMicrophonePrivacyConfig MicrophonePrivacy { get; set; }
[JsonProperty("occupancy")]
/// <summary>
/// Gets or sets the Occupancy
/// </summary>
public EssentialsRoomOccSensorConfig Occupancy { get; set; }
/// <summary>
/// Gets or sets the Occupancy
/// </summary>
[JsonProperty("occupancy")]
public EssentialsRoomOccSensorConfig Occupancy { get; set; }
[JsonProperty("oneButtonMeeting")]
/// <summary>
/// Gets or sets the OneButtonMeeting
/// </summary>
public EssentialsOneButtonMeetingPropertiesConfig OneButtonMeeting { get; set; }
/// <summary>
/// Gets or sets the OneButtonMeeting
/// </summary>
[JsonProperty("oneButtonMeeting")]
public EssentialsOneButtonMeetingPropertiesConfig OneButtonMeeting { get; set; }
[JsonProperty("shutdownVacancySeconds")]
/// <summary>
/// Gets or sets the ShutdownVacancySeconds
/// </summary>
public int ShutdownVacancySeconds { get; set; }
/// <summary>
/// Gets or sets the ShutdownVacancySeconds
/// </summary>
[JsonProperty("shutdownVacancySeconds")]
public int ShutdownVacancySeconds { get; set; }
[JsonProperty("shutdownPromptSeconds")]
/// <summary>
/// Gets or sets the ShutdownPromptSeconds
/// </summary>
public int ShutdownPromptSeconds { get; set; }
/// <summary>
/// Gets or sets the ShutdownPromptSeconds
/// </summary>
[JsonProperty("shutdownPromptSeconds")]
public int ShutdownPromptSeconds { get; set; }
[JsonProperty("tech")]
/// <summary>
/// Gets or sets the Tech
/// </summary>
public EssentialsRoomTechConfig Tech { get; set; }
/// <summary>
/// Gets or sets the Tech
/// </summary>
[JsonProperty("tech")]
public EssentialsRoomTechConfig Tech { get; set; }
/// <summary>
/// Gets or sets the Volumes
/// </summary>
[JsonProperty("volumes")]
public EssentialsRoomVolumesConfig Volumes { get; set; }
[JsonProperty("volumes")]
/// <summary>
/// Gets or sets the Volumes
/// </summary>
public EssentialsRoomVolumesConfig Volumes { get; set; }
[JsonProperty("fusion")]
/// <summary>
/// Gets or sets the Fusion
/// </summary>
[JsonProperty("fusion")]
public EssentialsRoomFusionConfig Fusion { get; set; }
[JsonProperty("essentialsRoomUiBehaviorConfig", NullValueHandling=NullValueHandling.Ignore)]
/// <summary>
/// Gets or sets the UiBehavior
/// </summary>
[JsonProperty("essentialsRoomUiBehaviorConfig", NullValueHandling = NullValueHandling.Ignore)]
public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; }
/// <summary>
/// Gets or sets the ZeroVolumeWhenSwtichingVolumeDevices
/// </summary>
[JsonProperty("zeroVolumeWhenSwtichingVolumeDevices")]
public bool ZeroVolumeWhenSwtichingVolumeDevices { get; set; }
[JsonProperty("zeroVolumeWhenSwtichingVolumeDevices")]
/// <summary>
/// Gets or sets the ZeroVolumeWhenSwtichingVolumeDevices
/// </summary>
public bool ZeroVolumeWhenSwtichingVolumeDevices { get; set; }
/// <summary>
/// Indicates if this room represents a combination of other rooms
@@ -236,7 +236,7 @@ namespace PepperDash.Essentials.Room.Config
LogoLight = new EssentialsLogoPropertiesConfig();
LogoDark = new EssentialsLogoPropertiesConfig();
}
}
}
/// <summary>
/// Represents a EssentialsRoomUiBehaviorConfig
@@ -327,15 +327,15 @@ namespace PepperDash.Essentials.Room.Config
}
/// <summary>
/// Represents a EssentialsEnvironmentPropertiesConfig
/// </summary>
public class EssentialsEnvironmentPropertiesConfig
{
/// <summary>
/// Gets or sets the Enabled
/// </summary>
public bool Enabled { get; set; }
/// <summary>
/// Represents a EssentialsEnvironmentPropertiesConfig
/// </summary>
public class EssentialsEnvironmentPropertiesConfig
{
/// <summary>
/// Gets or sets the Enabled
/// </summary>
public bool Enabled { get; set; }
[JsonProperty("deviceKeys")]
/// <summary>
@@ -348,7 +348,7 @@ namespace PepperDash.Essentials.Room.Config
DeviceKeys = new List<string>();
}
}
}
/// <summary>
/// Represents a EssentialsRoomFusionConfig
@@ -359,7 +359,7 @@ namespace PepperDash.Essentials.Room.Config
{
get
{
try
try
{
return Convert.ToUInt32(IpId, 16);
}
@@ -367,7 +367,7 @@ namespace PepperDash.Essentials.Room.Config
{
throw new FormatException(string.Format("ERROR:Unable to convert IP ID: {0} to hex. Error:\n{1}", IpId));
}
}
}
@@ -390,17 +390,17 @@ namespace PepperDash.Essentials.Room.Config
/// </summary>
public class EssentialsRoomMicrophonePrivacyConfig
{
[JsonProperty("deviceKey")]
/// <summary>
/// Gets or sets the DeviceKey
/// </summary>
public string DeviceKey { get; set; }
[JsonProperty("deviceKey")]
/// <summary>
/// Gets or sets the DeviceKey
/// </summary>
public string DeviceKey { get; set; }
[JsonProperty("behaviour")]
/// <summary>
/// Gets or sets the Behaviour
/// </summary>
public string Behaviour { get; set; }
[JsonProperty("behaviour")]
/// <summary>
/// Gets or sets the Behaviour
/// </summary>
public string Behaviour { get; set; }
}
/// <summary>
@@ -408,23 +408,23 @@ namespace PepperDash.Essentials.Room.Config
/// </summary>
public class EssentialsHelpPropertiesConfig
{
[JsonProperty("message")]
/// <summary>
/// Gets or sets the Message
/// </summary>
public string Message { get; set; }
[JsonProperty("message")]
/// <summary>
/// Gets or sets the Message
/// </summary>
public string Message { get; set; }
[JsonProperty("showCallButton")]
public bool ShowCallButton { get; set; }
/// <summary>
[JsonProperty("showCallButton")]
public bool ShowCallButton { get; set; }
/// <summary>
/// Defaults to "Call Help Desk"
/// </summary>
[JsonProperty("callButtonText")]
/// <summary>
/// Gets or sets the CallButtonText
/// </summary>
public string CallButtonText { get; set; }
[JsonProperty("callButtonText")]
/// <summary>
/// Gets or sets the CallButtonText
/// </summary>
public string CallButtonText { get; set; }
public EssentialsHelpPropertiesConfig()
{
@@ -437,23 +437,23 @@ namespace PepperDash.Essentials.Room.Config
/// </summary>
public class EssentialsOneButtonMeetingPropertiesConfig
{
[JsonProperty("enable")]
/// <summary>
/// Gets or sets the Enable
/// </summary>
public bool Enable { get; set; }
[JsonProperty("enable")]
/// <summary>
/// Gets or sets the Enable
/// </summary>
public bool Enable { get; set; }
}
public class EssentialsRoomAddressPropertiesConfig
{
[JsonProperty("phoneNumber")]
public string PhoneNumber { get; set; }
[JsonProperty("phoneNumber")]
public string PhoneNumber { get; set; }
[JsonProperty("sipAddress")]
/// <summary>
/// Gets or sets the SipAddress
/// </summary>
public string SipAddress { get; set; }
[JsonProperty("sipAddress")]
/// <summary>
/// Gets or sets the SipAddress
/// </summary>
public string SipAddress { get; set; }
}
@@ -462,14 +462,14 @@ namespace PepperDash.Essentials.Room.Config
/// </summary>
public class EssentialsLogoPropertiesConfig
{
[JsonProperty("type")]
/// <summary>
/// Gets or sets the Type
/// </summary>
public string Type { get; set; }
[JsonProperty("type")]
/// <summary>
/// Gets or sets the Type
/// </summary>
public string Type { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// GetLogoUrlLight method
/// </summary>
@@ -478,7 +478,7 @@ namespace PepperDash.Essentials.Room.Config
if (Type == "url")
return Url;
if (Type == "system")
return string.Format("http://{0}:8080/logo.png",
return string.Format("http://{0}:8080/logo.png",
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0));
return null;
}
@@ -502,22 +502,22 @@ namespace PepperDash.Essentials.Room.Config
/// </summary>
public class EssentialsRoomOccSensorConfig
{
[JsonProperty("deviceKey")]
/// <summary>
/// Gets or sets the DeviceKey
/// </summary>
public string DeviceKey { get; set; }
[JsonProperty("deviceKey")]
/// <summary>
/// Gets or sets the DeviceKey
/// </summary>
public string DeviceKey { get; set; }
[JsonProperty("timeoutMinutes")]
public int TimeoutMinutes { get; set; }
[JsonProperty("timeoutMinutes")]
public int TimeoutMinutes { get; set; }
}
public class EssentialsRoomTechConfig
{
[JsonProperty("password")]
/// <summary>
/// Gets or sets the Password
/// </summary>
public string Password { get; set; }
}
public class EssentialsRoomTechConfig
{
[JsonProperty("password")]
/// <summary>
/// Gets or sets the Password
/// </summary>
public string Password { get; set; }
}
}

View File

@@ -14,14 +14,14 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core
{
////*****************************************************************************
///// <summary>
///// Base class for all subpage reference list controllers
///// </summary>
//public class SubpageReferenceListController
//{
// public SubpageReferenceList TheList { get; protected set; }
//}
////*****************************************************************************
/// <summary>
/// Base class for all subpage reference list controllers
/// </summary>
//public class SubpageReferenceListController
//{
// public SubpageReferenceList TheList { get; protected set; }
//}
//*****************************************************************************
/// <summary>
@@ -34,26 +34,26 @@ namespace PepperDash.Essentials.Core
public ushort Count
{
get { return SetNumberOfItemsSig.UShortValue; }
set { SetNumberOfItemsSig.UShortValue = value; }
set { SetNumberOfItemsSig.UShortValue = value; }
}
public ushort MaxDefinedItems { get; private set; }
/// <summary>
/// Gets or sets the ScrollToItemSig
/// </summary>
/// <summary>
/// Gets or sets the ScrollToItemSig
/// </summary>
public UShortInputSig ScrollToItemSig { get; private set; }
UShortInputSig SetNumberOfItemsSig;
/// <summary>
/// Gets or sets the BoolIncrement
/// </summary>
/// <summary>
/// Gets or sets the BoolIncrement
/// </summary>
public uint BoolIncrement { get; protected set; }
/// <summary>
/// Gets or sets the UShortIncrement
/// </summary>
/// <summary>
/// Gets or sets the UShortIncrement
/// </summary>
public uint UShortIncrement { get; protected set; }
/// <summary>
/// Gets or sets the StringIncrement
/// </summary>
/// <summary>
/// Gets or sets the StringIncrement
/// </summary>
public uint StringIncrement { get; protected set; }
protected readonly SmartObject SRL;
@@ -87,8 +87,8 @@ namespace PepperDash.Essentials.Core
SRL.SigChange += new SmartObjectSigChangeEventHandler(SRL_SigChange);
}
else
Debug.LogMessage(LogEventLevel.Information, "ERROR: TriList 0x{0:X2} Cannot load smart object {1}. Verify correct SGD file is loaded",
triList.ID, smartObjectId);
Debug.LogMessage(LogEventLevel.Information, "ERROR: TriList 0x{0:X2} Cannot load smart object {1}. Verify correct SGD file is loaded",
triList.ID, smartObjectId);
}
/// <summary>
@@ -96,17 +96,17 @@ namespace PepperDash.Essentials.Core
/// DOES NOT adjust Count
/// </summary>
/// <param name="item"></param>
/// <summary>
/// AddItem method
/// </summary>
/// <summary>
/// AddItem method
/// </summary>
public void AddItem(SubpageReferenceListItem item)
{
Items.Add(item);
}
/// <summary>
/// Clear method
/// </summary>
/// <summary>
/// Clear method
/// </summary>
public void Clear()
{
// If a line item needs to disconnect an CueActionPair or do something to release RAM
@@ -116,12 +116,12 @@ namespace PepperDash.Essentials.Core
// Clean up the SRL
Count = 1;
ScrollToItemSig.UShortValue = 1;
ScrollToItemSig.UShortValue = 1;
}
/// <summary>
/// Refresh method
/// </summary>
/// <summary>
/// Refresh method
/// </summary>
public void Refresh()
{
foreach (var item in Items) item.Refresh();
@@ -157,7 +157,7 @@ namespace PepperDash.Essentials.Core
public UShortOutputSig GetUShortOutputSig(uint index, uint sigNum)
{
if (sigNum > UShortIncrement) return null;
return SRL.UShortOutput.FirstOrDefault(s => s.Name.Equals(GetUShortOutputSigName(index, sigNum)));
return SRL.UShortOutput.FirstOrDefault(s => s.Name.Equals(GetUShortOutputSigName(index, sigNum)));
}
/// <summary>
@@ -172,7 +172,7 @@ namespace PepperDash.Essentials.Core
public StringOutputSig GetStringOutputSig(uint index, uint sigNum)
{
if (sigNum > StringIncrement) return null;
return SRL.StringOutput.FirstOrDefault(s => s.Name.Equals(GetStringOutputSigName(index, sigNum)));
return SRL.StringOutput.FirstOrDefault(s => s.Name.Equals(GetStringOutputSigName(index, sigNum)));
}
/// <summary>
@@ -263,9 +263,9 @@ namespace PepperDash.Essentials.Core
/// </summary>
/// <param name="currentDevice"></param>
/// <param name="args"></param>
/// <summary>
/// SRL_SigChange method
/// </summary>
/// <summary>
/// SRL_SigChange method
/// </summary>
public static void SRL_SigChange(GenericBase currentDevice, SmartObjectEventArgs args)
{
var uo = args.Sig.UserObject;

View File

@@ -6,9 +6,9 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Represents a ModalDialog
/// </summary>
/// <summary>
/// Represents a ModalDialog
/// </summary>
public class ModalDialog
{
/// <summary>
@@ -19,10 +19,10 @@ namespace PepperDash.Essentials.Core
/// Bool press 3992
/// </summary>
public const uint Button2Join = 3992;
/// <summary>
/// 3993
/// </summary>
public const uint CancelButtonJoin = 3993;
/// <summary>
/// 3993
/// </summary>
public const uint CancelButtonJoin = 3993;
/// <summary>
///For visibility of single button. Bool feedback 3994
/// </summary>
@@ -35,19 +35,19 @@ namespace PepperDash.Essentials.Core
/// Shows the timer guage if in use. Bool feedback 3996
/// </summary>
public const uint TimerVisibleJoin = 3996;
/// <summary>
/// Visibility join to show "X" button 3997
/// </summary>
public const uint CancelVisibleJoin = 3997;
/// <summary>
/// Visibility join to show "X" button 3997
/// </summary>
public const uint CancelVisibleJoin = 3997;
/// <summary>
/// Shows the modal subpage. Boolean feeback join 3999
/// </summary>
public const uint ModalVisibleJoin = 3999;
///// <summary>
///// The seconds value of the countdown timer. Ushort join 3991
///// </summary>
//public const uint TimerSecondsJoin = 3991;
/// <summary>
/// The seconds value of the countdown timer. Ushort join 3991
/// </summary>
//public const uint TimerSecondsJoin = 3991;
/// <summary>
/// The full ushort value of the countdown timer for a gauge. Ushort join 3992
/// </summary>
@@ -77,15 +77,15 @@ namespace PepperDash.Essentials.Core
/// <summary>
/// Returns true when modal is showing
/// </summary>
public bool ModalIsVisible
{
get { return TriList.BooleanInput[ModalVisibleJoin].BoolValue; }
public bool ModalIsVisible
{
get { return TriList.BooleanInput[ModalVisibleJoin].BoolValue; }
}
/// <summary>
///
/// </summary>
public bool CanCancel { get; private set; }
/// <summary>
///
/// </summary>
public bool CanCancel { get; private set; }
BasicTriList TriList;
@@ -103,10 +103,10 @@ namespace PepperDash.Essentials.Core
TriList = triList;
// Attach actions to buttons
triList.SetSigFalseAction(Button1Join, () => OnModalComplete(1));
triList.SetSigFalseAction(Button1Join, () => OnModalComplete(1));
triList.SetSigFalseAction(Button2Join, () => OnModalComplete(2));
triList.SetSigFalseAction(CancelButtonJoin, () => { if (CanCancel) CancelDialog(); });
CanCancel = true;
triList.SetSigFalseAction(CancelButtonJoin, () => { if (CanCancel) CancelDialog(); });
CanCancel = true;
}
/// <summary>
@@ -117,7 +117,7 @@ namespace PepperDash.Essentials.Core
/// <param name="decreasingGauge">If the progress bar gauge needs to count down instead of up</param>
/// <param name="completeAction">The action to run when the dialog is dismissed. Parameter will be 1 or 2 if button pressed, or 0 if dialog times out</param>
/// <returns>True when modal is created.</returns>
public bool PresentModalDialog(uint numberOfButtons, string title, string iconName,
public bool PresentModalDialog(uint numberOfButtons, string title, string iconName,
string message, string button1Text,
string button2Text, bool showGauge, bool showCancel, Action<uint> completeAction)
{
@@ -151,15 +151,15 @@ namespace PepperDash.Essentials.Core
TriList.StringInput[Button2TextJoin].StringValue = button2Text;
}
// Show/hide guage
TriList.BooleanInput[TimerVisibleJoin].BoolValue = showGauge;
CanCancel = showCancel;
TriList.BooleanInput[CancelVisibleJoin].BoolValue = showCancel;
TriList.BooleanInput[TimerVisibleJoin].BoolValue = showGauge;
CanCancel = showCancel;
TriList.BooleanInput[CancelVisibleJoin].BoolValue = showCancel;
//Reveal and activate
TriList.BooleanInput[ModalVisibleJoin].BoolValue = true;
WakePanel();
WakePanel();
return true;
}
@@ -167,48 +167,48 @@ namespace PepperDash.Essentials.Core
return false;
}
/// <summary>
/// WakePanel method
/// </summary>
public void WakePanel()
{
try
{
var panel = TriList as TswFt5Button;
/// <summary>
/// WakePanel method
/// </summary>
public void WakePanel()
{
try
{
var panel = TriList as TswFt5Button;
if (panel != null && panel.ExtenderSystemReservedSigs.BacklightOffFeedback.BoolValue)
panel.ExtenderSystemReservedSigs.BacklightOn();
}
catch
{
Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?");
}
}
if (panel != null && panel.ExtenderSystemReservedSigs.BacklightOffFeedback.BoolValue)
panel.ExtenderSystemReservedSigs.BacklightOn();
}
catch
{
Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?");
}
}
/// <summary>
/// CancelDialog method
/// </summary>
/// <summary>
/// CancelDialog method
/// </summary>
public void CancelDialog()
{
OnModalComplete(0);
OnModalComplete(0);
}
/// <summary>
/// Hides dialog. Fires no action
/// </summary>
public void HideDialog()
{
TriList.BooleanInput[ModalVisibleJoin].BoolValue = false;
}
/// <summary>
/// Hides dialog. Fires no action
/// </summary>
public void HideDialog()
{
TriList.BooleanInput[ModalVisibleJoin].BoolValue = false;
}
// When the modal is cleared or times out, clean up the various bits
void OnModalComplete(uint buttonNum)
{
TriList.BooleanInput[ModalVisibleJoin].BoolValue = false;
var action = ModalCompleteAction;
if (action != null)
action(buttonNum);
var action = ModalCompleteAction;
if (action != null)
action(buttonNum);
}
}

View File

@@ -29,6 +29,6 @@
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.157" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,221 @@
using System;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Devices.Common.Codec;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Provides a messaging bridge for devices that implement call status interfaces
/// without requiring VideoCodecBase inheritance
/// </summary>
public class CallStatusMessenger : MessengerBase
{
/// <summary>
/// Device with dialer capabilities
/// </summary>
protected IHasDialer Dialer { get; private set; }
/// <summary>
/// Device with content sharing capabilities (optional)
/// </summary>
protected IHasContentSharing ContentSharing { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="dialer"></param>
/// <param name="messagePath"></param>
public CallStatusMessenger(string key, IHasDialer dialer, string messagePath)
: base(key, messagePath, dialer as IKeyName)
{
Dialer = dialer ?? throw new ArgumentNullException(nameof(dialer));
dialer.CallStatusChange += Dialer_CallStatusChange;
// Check for optional content sharing interface
if (dialer is IHasContentSharing contentSharing)
{
ContentSharing = contentSharing;
contentSharing.SharingContentIsOnFeedback.OutputChange += SharingContentIsOnFeedback_OutputChange;
contentSharing.SharingSourceFeedback.OutputChange += SharingSourceFeedback_OutputChange;
}
}
/// <summary>
/// Handles call status changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Dialer_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e)
{
try
{
SendFullStatus();
}
catch (Exception ex)
{
this.LogError(ex, "Error handling call status change: {error}", ex.Message);
}
}
/// <summary>
/// Handles content sharing status changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SharingContentIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
PostStatusMessage(JToken.FromObject(new
{
sharingContentIsOn = e.BoolValue
}));
}
/// <summary>
/// Handles sharing source changes
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SharingSourceFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
PostStatusMessage(JToken.FromObject(new
{
sharingSource = e.StringValue
}));
}
/// <summary>
/// Gets active calls from the dialer
/// </summary>
/// <returns></returns>
private object GetActiveCalls()
{
// Try to get active calls if the dialer has an ActiveCalls property
var dialerType = Dialer.GetType();
var activeCallsProperty = dialerType.GetProperty("ActiveCalls");
if (activeCallsProperty != null && activeCallsProperty.PropertyType == typeof(System.Collections.Generic.List<CodecActiveCallItem>))
{
var activeCalls = activeCallsProperty.GetValue(Dialer) as System.Collections.Generic.List<CodecActiveCallItem>;
return activeCalls ?? new System.Collections.Generic.List<CodecActiveCallItem>();
}
// Return basic call status if no ActiveCalls property
return new { isInCall = Dialer.IsInCall };
}
/// <summary>
/// Sends full status message
/// </summary>
public void SendFullStatus()
{
var status = new
{
isInCall = Dialer.IsInCall,
calls = GetActiveCalls()
};
// Add content sharing status if available
if (ContentSharing != null)
{
var statusWithSharing = new
{
isInCall = Dialer.IsInCall,
calls = GetActiveCalls(),
sharingContentIsOn = ContentSharing.SharingContentIsOnFeedback.BoolValue,
sharingSource = ContentSharing.SharingSourceFeedback.StringValue
};
PostStatusMessage(JToken.FromObject(statusWithSharing));
}
else
{
PostStatusMessage(JToken.FromObject(status));
}
}
/// <summary>
/// Registers actions for call control
/// </summary>
protected override void RegisterActions()
{
base.RegisterActions();
AddAction("/fullStatus", (id, content) => SendFullStatus());
// Basic call control actions
AddAction("/dial", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
Dialer.Dial(msg.Value);
});
AddAction("/endAllCalls", (id, content) => Dialer.EndAllCalls());
AddAction("/dtmf", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
Dialer.SendDtmf(msg.Value);
});
// Call-specific actions (if active calls are available)
AddAction("/endCallById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
var call = GetCallWithId(msg.Value);
if (call != null)
Dialer.EndCall(call);
});
AddAction("/acceptById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
var call = GetCallWithId(msg.Value);
if (call != null)
Dialer.AcceptCall(call);
});
AddAction("/rejectById", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<string>>();
var call = GetCallWithId(msg.Value);
if (call != null)
Dialer.RejectCall(call);
});
// Content sharing actions if available
if (ContentSharing != null)
{
AddAction("/startSharing", (id, content) => ContentSharing.StartSharing());
AddAction("/stopSharing", (id, content) => ContentSharing.StopSharing());
}
}
/// <summary>
/// Finds a call by ID
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private CodecActiveCallItem GetCallWithId(string id)
{
// Try to get call using reflection for ActiveCalls property
var dialerType = Dialer.GetType();
var activeCallsProperty = dialerType.GetProperty("ActiveCalls");
if (activeCallsProperty != null && activeCallsProperty.PropertyType == typeof(System.Collections.Generic.List<CodecActiveCallItem>))
{
var activeCalls = activeCallsProperty.GetValue(Dialer) as System.Collections.Generic.List<CodecActiveCallItem>;
if (activeCalls != null)
{
return activeCalls.FirstOrDefault(c => c.Id.Equals(id));
}
}
return null;
}
}
}

View File

@@ -140,19 +140,17 @@ namespace PepperDash.Essentials.AppServer.Messengers
if (Camera is IHasCameraPresets presetsCamera)
{
AddAction("/recallPreset", (id, content) =>
for (int i = 1; i <= 6; i++)
{
var msg = content.ToObject<MobileControlSimpleContent<int>>();
var preset = i;
AddAction("/cameraPreset" + i, (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<int>>();
presetsCamera.PresetSelect(msg.Value);
});
presetsCamera.PresetSelect(msg.Value);
});
AddAction("/storePreset", (id, content) =>
{
var msg = content.ToObject<MobileControlSimpleContent<int>>();
presetsCamera.PresetStore(msg.Value, string.Empty);
});
}
}
}
@@ -166,8 +164,9 @@ namespace PepperDash.Essentials.AppServer.Messengers
return;
}
timerHandler(Camera.Key, cameraAction);
timerHandler(state.Value, cameraAction);
cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase));
}
/// <summary>

View File

@@ -1,77 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using System.Collections.Generic;
namespace PepperDash.Essentials.AppServer.Messengers
{
/// <summary>
/// Represents a IBasicVideoMuteWithFeedbackMessenger
/// </summary>
public class IBasicVideoMuteWithFeedbackMessenger : MessengerBase
{
private readonly IBasicVideoMuteWithFeedback device;
public IBasicVideoMuteWithFeedbackMessenger(string key, string messagePath, IBasicVideoMuteWithFeedback device)
: base(key, messagePath, device as IKeyName)
{
this.device = device;
}
/// <summary>
/// SendFullStatus method
/// </summary>
public void SendFullStatus()
{
var messageObj = new IBasicVideoMuteWithFeedbackMessage
{
VideoMuteState = device.VideoMuteIsOn.BoolValue
};
PostStatusMessage(messageObj);
}
protected override void RegisterActions()
{
base.RegisterActions();
AddAction("/fullStatus", (id, content) => SendFullStatus());
AddAction("/videoMuteToggle", (id, content) =>
{
device.VideoMuteToggle();
});
AddAction("/videoMuteOn", (id, content) =>
{
device.VideoMuteOn();
});
AddAction("/videoMuteOff", (id, content) =>
{
device.VideoMuteOff();
});
device.VideoMuteIsOn.OutputChange += VideoMuteIsOnFeedback_OutputChange;
}
private void VideoMuteIsOnFeedback_OutputChange(object sender, FeedbackEventArgs args)
{
PostStatusMessage(JToken.FromObject(new
{
videoMuteState = args.BoolValue
})
);
}
}
/// <summary>
/// Represents a IBasicVideoMuteWithFeedbackMessage
/// </summary>
public class IBasicVideoMuteWithFeedbackMessage : DeviceStateMessageBase
{
[JsonProperty("videoMuteState")]
public bool VideoMuteState { get; set; }
}
}

View File

@@ -33,7 +33,7 @@
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.157" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System.Collections.Generic;
namespace PepperDash.Essentials
{
@@ -9,34 +9,25 @@ namespace PepperDash.Essentials
/// </summary>
public class MobileControlConfig
{
/// <summary>
/// Gets or sets the ServerUrl
/// </summary>
[JsonProperty("serverUrl")]
public string ServerUrl { get; set; }
/// <summary>
/// Gets or sets the ClientAppUrl
/// </summary>
[JsonProperty("clientAppUrl")]
public string ClientAppUrl { get; set; }
/// <summary>
/// Gets or sets the DirectServer
/// </summary>
[JsonProperty("directServer")]
public MobileControlDirectServerPropertiesConfig DirectServer { get; set; }
[JsonProperty("applicationConfig")]
/// <summary>
/// Gets or sets the ApplicationConfig
/// </summary>
[JsonProperty("applicationConfig")]
public MobileControlApplicationConfig ApplicationConfig { get; set; } = null;
[JsonProperty("enableApiServer")]
/// <summary>
/// Gets or sets the EnableApiServer
/// </summary>
[JsonProperty("enableApiServer")]
public bool EnableApiServer { get; set; } = true;
}
@@ -45,42 +36,27 @@ namespace PepperDash.Essentials
/// </summary>
public class MobileControlDirectServerPropertiesConfig
{
[JsonProperty("enableDirectServer")]
/// <summary>
/// Gets or sets the EnableDirectServer
/// </summary>
[JsonProperty("enableDirectServer")]
public bool EnableDirectServer { get; set; }
[JsonProperty("port")]
/// <summary>
/// Gets or sets the Port
/// </summary>
[JsonProperty("port")]
public int Port { get; set; }
[JsonProperty("logging")]
/// <summary>
/// Gets or sets the Logging
/// </summary>
[JsonProperty("logging")]
public MobileControlLoggingConfig Logging { get; set; }
/// <summary>
/// Gets or sets the AutomaticallyForwardPortToCSLAN
/// </summary>
[JsonProperty("automaticallyForwardPortToCSLAN")]
public bool? AutomaticallyForwardPortToCSLAN { get; set; }
/// <summary>
/// Gets or sets the CSLanUiDeviceKeys
/// </summary>
/// <remarks>
/// A list of device keys for the CS LAN UI. These devices will get the CS LAN IP address instead of the LAN IP Address
/// </remarks>
[JsonProperty("csLanUiDeviceKeys")]
public List<string> CSLanUiDeviceKeys { get; set; }
/// <summary>
/// Initializes a new instance of the MobileControlDirectServerPropertiesConfig class.
/// </summary>
public MobileControlDirectServerPropertiesConfig()
{
Logging = new MobileControlLoggingConfig();
@@ -92,26 +68,26 @@ namespace PepperDash.Essentials
/// </summary>
public class MobileControlLoggingConfig
{
[JsonProperty("enableRemoteLogging")]
/// <summary>
/// Gets or sets the EnableRemoteLogging
/// </summary>
[JsonProperty("enableRemoteLogging")]
public bool EnableRemoteLogging { get; set; }
[JsonProperty("host")]
/// <summary>
/// Gets or sets the Host
/// </summary>
[JsonProperty("host")]
public string Host { get; set; }
[JsonProperty("port")]
/// <summary>
/// Gets or sets the Port
/// </summary>
[JsonProperty("port")]
public int Port { get; set; }
}
/// <summary>
@@ -119,16 +95,16 @@ namespace PepperDash.Essentials
/// </summary>
public class MobileControlRoomBridgePropertiesConfig
{
[JsonProperty("key")]
/// <summary>
/// Gets or sets the Key
/// </summary>
[JsonProperty("key")]
public string Key { get; set; }
[JsonProperty("roomKey")]
/// <summary>
/// Gets or sets the RoomKey
/// </summary>
[JsonProperty("roomKey")]
public string RoomKey { get; set; }
}
@@ -137,71 +113,53 @@ namespace PepperDash.Essentials
/// </summary>
public class MobileControlSimplRoomBridgePropertiesConfig
{
[JsonProperty("eiscId")]
/// <summary>
/// Gets or sets the EiscId
/// </summary>
[JsonProperty("eiscId")]
public string EiscId { get; set; }
}
/// <summary>
/// Represents a MobileControlApplicationConfig
/// </summary>
public class MobileControlApplicationConfig
{
/// <summary>
/// Gets or sets the ApiPath
/// </summary>
[JsonProperty("apiPath")]
public string ApiPath { get; set; }
[JsonProperty("gatewayAppPath")]
/// <summary>
/// Gets or sets the GatewayAppPath
/// </summary>
[JsonProperty("gatewayAppPath")]
public string GatewayAppPath { get; set; }
/// <summary>
/// Gets or sets the EnableDev
/// </summary>
[JsonProperty("enableDev")]
public bool? EnableDev { get; set; }
[JsonProperty("logoPath")]
/// <summary>
/// Gets or sets the LogoPath
/// </summary>
[JsonProperty("logoPath")]
public string LogoPath { get; set; }
/// <summary>
/// Gets or sets the IconSet
/// </summary>
[JsonProperty("iconSet")]
[JsonConverter(typeof(StringEnumConverter))]
public MCIconSet? IconSet { get; set; }
/// <summary>
/// Gets or sets the LoginMode
/// </summary>
[JsonProperty("loginMode")]
public string LoginMode { get; set; }
/// <summary>
/// Gets or sets the Modes
/// </summary>
[JsonProperty("modes")]
public Dictionary<string, McMode> Modes { get; set; }
[JsonProperty("enableRemoteLogging")]
/// <summary>
/// Gets or sets the Logging
/// </summary>
[JsonProperty("enableRemoteLogging")]
public bool Logging { get; set; }
[JsonProperty("partnerMetadata", NullValueHandling = NullValueHandling.Ignore)]
/// <summary>
/// Gets or sets the PartnerMetadata
/// </summary>
[JsonProperty("partnerMetadata", NullValueHandling = NullValueHandling.Ignore)]
public List<MobileControlPartnerMetadata> PartnerMetadata { get; set; }
}
@@ -210,22 +168,22 @@ namespace PepperDash.Essentials
/// </summary>
public class MobileControlPartnerMetadata
{
[JsonProperty("role")]
/// <summary>
/// Gets or sets the Role
/// </summary>
[JsonProperty("role")]
public string Role { get; set; }
[JsonProperty("description")]
/// <summary>
/// Gets or sets the Description
/// </summary>
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("logoPath")]
/// <summary>
/// Gets or sets the LogoPath
/// </summary>
[JsonProperty("logoPath")]
public string LogoPath { get; set; }
}
@@ -234,22 +192,21 @@ namespace PepperDash.Essentials
/// </summary>
public class McMode
{
[JsonProperty("listPageText")]
/// <summary>
/// Gets or sets the ListPageText
/// </summary>
[JsonProperty("listPageText")]
public string ListPageText { get; set; }
[JsonProperty("loginHelpText")]
/// <summary>
/// Gets or sets the LoginHelpText
/// </summary>
[JsonProperty("loginHelpText")]
public string LoginHelpText { get; set; }
[JsonProperty("passcodePageText")]
/// <summary>
/// Gets or sets the PasscodePageText
/// </summary>
[JsonProperty("passcodePageText")]
public string PasscodePageText { get; set; }
}
@@ -258,19 +215,8 @@ namespace PepperDash.Essentials
/// </summary>
public enum MCIconSet
{
/// <summary>
/// Google icon set
/// </summary>
GOOGLE,
/// <summary>
/// Habanero icon set
/// </summary>
HABANERO,
/// <summary>
/// Neo icon set
/// </summary>
NEO
}
}

View File

@@ -27,6 +27,7 @@ using PepperDash.Essentials.Core.Shades;
using PepperDash.Essentials.Core.Web;
using PepperDash.Essentials.Devices.Common.AudioCodec;
using PepperDash.Essentials.Devices.Common.Cameras;
using PepperDash.Essentials.Devices.Common.Codec;
using PepperDash.Essentials.Devices.Common.Displays;
using PepperDash.Essentials.Devices.Common.Lighting;
using PepperDash.Essentials.Devices.Common.SoftCodec;
@@ -505,25 +506,6 @@ namespace PepperDash.Essentials
messengerAdded = true;
}
if (device is IBasicVideoMuteWithFeedback)
{
var deviceKey = device.Key;
this.LogVerbose(
"Adding IBasicVideoMuteWithFeedback for {deviceKey}",
deviceKey
);
var videoMuteControlDevice = device as IBasicVideoMuteWithFeedback;
var messenger = new IBasicVideoMuteWithFeedbackMessenger(
$"{device.Key}-videoMute-{Key}",
string.Format("/device/{0}", deviceKey),
videoMuteControlDevice
);
AddDefaultDeviceMessenger(messenger);
messengerAdded = true;
}
if (device is ILightingScenes || device is LightingBase)
{
var deviceKey = device.Key;
@@ -579,6 +561,21 @@ namespace PepperDash.Essentials
messengerAdded = true;
}
else if (device is IHasDialer dialer && !messengerAdded)
{
this.LogVerbose(
"Adding CallStatusMessenger for {deviceKey}", device.Key);
var messenger = new CallStatusMessenger(
$"{device.Key}-callStatus-{Key}",
dialer,
$"/device/{device.Key}"
);
AddDefaultDeviceMessenger(messenger);
messengerAdded = true;
}
if (device is AudioCodecBase audioCodec)
{

View File

@@ -38,7 +38,7 @@
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.157" />
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
using Crestron.SimplSharpPro.UI;
@@ -108,11 +106,6 @@ namespace PepperDash.Essentials.Touchpanel
public ReadOnlyCollection<ConnectedIpInformation> ConnectedIps => Panel.ConnectedIpList;
private System.Net.IPAddress csIpAddress;
private System.Net.IPAddress csSubnetMask;
/// <summary>
/// Initializes a new instance of the MobileControlTouchpanelController class.
/// </summary>
@@ -189,13 +182,6 @@ namespace PepperDash.Essentials.Touchpanel
};
RegisterForExtenders();
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
var csIpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
this.csSubnetMask = System.Net.IPAddress.Parse(csSubnetMask);
this.csIpAddress = System.Net.IPAddress.Parse(csIpAddress);
}
/// <summary>
@@ -395,81 +381,19 @@ namespace PepperDash.Essentials.Touchpanel
McServerUrlFeedback.LinkInputSig(Panel.StringInput[3]);
UserCodeFeedback.LinkInputSig(Panel.StringInput[4]);
Panel.IpInformationChange += (sender, args) =>
{
if (args.Connected)
{
this.LogVerbose("Connection from IP: {ip}", args.DeviceIpAddress);
this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue);
var appUrl = GetUrlWithCorrectIp(_appUrl);
Panel.StringInput[1].StringValue = appUrl;
SetAppUrl(appUrl);
}
else
{
this.LogVerbose("Disconnection from IP: {ip}", args.DeviceIpAddress);
}
};
Panel.OnlineStatusChange += (sender, args) =>
{
UpdateFeedbacks();
this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue);
UpdateFeedbacks();
Panel.StringInput[1].StringValue = _appUrl;
Panel.StringInput[1].StringValue = AppUrlFeedback.StringValue;
Panel.StringInput[2].StringValue = QrCodeUrlFeedback.StringValue;
Panel.StringInput[3].StringValue = McServerUrlFeedback.StringValue;
Panel.StringInput[4].StringValue = UserCodeFeedback.StringValue;
};
}
/// <summary>
/// Gets the URL with the correct IP address based on the connected devices and the Crestron processor's IP address.
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
private string GetUrlWithCorrectIp(string url)
{
var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
if(csIpAddress == null || csSubnetMask == null || url == null)
{
this.LogWarning("CS IP Address Subnet Mask or url is null, cannot determine correct IP for URL");
return url;
}
this.LogVerbose("Processor IP: {processorIp}, CS IP: {csIpAddress}, CS Subnet Mask: {csSubnetMask}", processorIp, csIpAddress, csSubnetMask);
this.LogVerbose("Connected IP Count: {connectedIps}", ConnectedIps.Count);
var ip = ConnectedIps.Any(ipInfo =>
{
if (System.Net.IPAddress.TryParse(ipInfo.DeviceIpAddress, out var parsedIp))
{
return csIpAddress.IsInSameSubnet(parsedIp, csSubnetMask);
}
this.LogWarning("Invalid IP address: {deviceIpAddress}", ipInfo.DeviceIpAddress);
return false;
}) ? csIpAddress.ToString() : processorIp;
var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
if (match.Success)
{
string ipa = match.Groups[1].Value;
// ip will be "192.168.1.100"
}
// replace ipa with ip but leave the rest of the string intact
var updatedUrl = Regex.Replace(url, @"^http://[^:/]+", $"http://{ip}");
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
return updatedUrl;
}
private void SubscribeForMobileControlUpdates()
{
foreach (var dev in DeviceManager.AllDevices)
@@ -519,8 +443,7 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary>
public void SetAppUrl(string url)
{
_appUrl = GetUrlWithCorrectIp(url);
_appUrl = url;
AppUrlFeedback.FireUpdate();
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -42,14 +41,8 @@ namespace PepperDash.Essentials.WebSocketServer
private HttpServer _server;
/// <summary>
/// Gets the HttpServer instance
/// </summary>
public HttpServer Server => _server;
/// <summary>
/// Gets the collection of UI client contexts
/// </summary>
public Dictionary<string, UiClientContext> UiClients { get; private set; }
private readonly MobileControlSystemController _parent;
@@ -68,20 +61,17 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
private string LanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter));
private string lanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter));
private readonly System.Net.IPAddress csIpAddress;
private System.Net.IPAddress csIpAddress;
private readonly System.Net.IPAddress csSubnetMask;
private System.Net.IPAddress csSubnetMask;
/// <summary>
/// The path for the WebSocket messaging
/// </summary>
private readonly string _wsPath = "/mc/api/ui/join/";
/// <summary>
/// Gets the WebSocket path
/// </summary>
public string WsPath => _wsPath;
/// <summary>
@@ -99,9 +89,6 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public int Port { get; private set; }
/// <summary>
/// Gets the user app URL prefix
/// </summary>
public string UserAppUrlPrefix
{
get
@@ -114,9 +101,6 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
/// <summary>
/// Gets the count of connected UI clients
/// </summary>
public int ConnectedUiClientsCount
{
get
@@ -135,9 +119,6 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
/// <summary>
/// Initializes a new instance of the MobileControlWebsocketServer class.
/// </summary>
public MobileControlWebsocketServer(string key, int customPort, MobileControlSystemController parent)
: base(key)
{
@@ -346,26 +327,17 @@ namespace PepperDash.Essentials.WebSocketServer
}
string ip = processorIp;
// Moved to the MobileControlTouchpanelController class in the GetUrlWithCorrectIp method
// triggered by the Panel.IpInformationChange event so that we know we have the necessary info
// to make the determination of which IP to use.
//if (touchpanel.Touchpanel is IMobileControlCrestronTouchpanelController crestronTouchpanel && csIpAddress != null)
//{
// ip = crestronTouchpanel.ConnectedIps.Any(ipInfo =>
// {
// if (System.Net.IPAddress.TryParse(ipInfo.DeviceIpAddress, out var parsedIp))
// {
// return csIpAddress.IsInSameSubnet(parsedIp, csSubnetMask);
// }
// this.LogWarning("Invalid IP address: {deviceIpAddress}", ipInfo.DeviceIpAddress);
// return false;
// }) ? csIpAddress.ToString() : processorIp;
//}
if (_parent.Config.DirectServer.CSLanUiDeviceKeys != null && _parent.Config.DirectServer.CSLanUiDeviceKeys.Any(k => k.Equals(touchpanel.Touchpanel.Key, StringComparison.InvariantCultureIgnoreCase)) && csIpAddress != null)
if (touchpanel.Touchpanel is IMobileControlCrestronTouchpanelController crestronTouchpanel && csIpAddress != null)
{
ip = csIpAddress.ToString();
ip = crestronTouchpanel.ConnectedIps.Any(ipInfo =>
{
if (System.Net.IPAddress.TryParse(ipInfo.DeviceIpAddress, out var parsedIp))
{
return csIpAddress.IsInSameSubnet(parsedIp, csSubnetMask);
}
this.LogWarning("Invalid IP address: {deviceIpAddress}", ipInfo.DeviceIpAddress);
return false;
}) ? csIpAddress.ToString() : processorIp;
}
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
@@ -649,9 +621,6 @@ namespace PepperDash.Essentials.WebSocketServer
CrestronConsole.ConsoleCommandResponse($"Token: {token}");
}
/// <summary>
/// Validates the grant code against the room key
/// </summary>
public (string, string) ValidateGrantCode(string grantCode, string roomKey)
{
var bridge = _parent.GetRoomBridge(roomKey);
@@ -665,9 +634,6 @@ namespace PepperDash.Essentials.WebSocketServer
return ValidateGrantCode(grantCode, bridge);
}
/// <summary>
/// Validates the grant code against the room key
/// </summary>
public (string, string) ValidateGrantCode(string grantCode, MobileControlBridgeBase bridge)
{
// TODO: Authenticate grant code passed in
@@ -689,9 +655,6 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
/// <summary>
/// Generates a new client token for the specified bridge
/// </summary>
public (string, string) GenerateClientToken(MobileControlBridgeBase bridge, string touchPanelKey = "")
{
var key = Guid.NewGuid().ToString();

View File

@@ -1,5 +1,5 @@
using System;
using System.IO.Compression;
using System;
using System.Linq;
using System.Reflection;
using Crestron.SimplSharp;
@@ -41,6 +41,29 @@ namespace PepperDash.Essentials
SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true;
Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose);
// AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
}
private Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
{
var assemblyName = new AssemblyName(args.Name).Name;
if (assemblyName == "PepperDash_Core")
{
return Assembly.LoadFrom("PepperDashCore.dll");
}
if (assemblyName == "PepperDash_Essentials_Core")
{
return Assembly.LoadFrom("PepperDash.Essentials.Core.dll");
}
if (assemblyName == "Essentials Devices Common")
{
return Assembly.LoadFrom("PepperDash.Essentials.Devices.Common.dll");
}
return null;
}
/// <summary>
@@ -244,8 +267,6 @@ namespace PepperDash.Essentials
// _ = new ProcessorExtensionDeviceFactory();
// _ = new MobileControlFactory();
LoadAssets();
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration");
var filesReady = SetupFilesystem();
@@ -547,142 +568,5 @@ namespace PepperDash.Essentials
return false;
}
}
private static void LoadAssets()
{
var applicationDirectory = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
Debug.LogMessage(LogEventLevel.Information, "Searching: {applicationDirectory:l} for embedded assets - {Destination}", applicationDirectory.FullName, Global.FilePathPrefix);
var zipFiles = applicationDirectory.GetFiles("assets*.zip");
if (zipFiles.Length > 1)
{
throw new Exception("Multiple assets zip files found. Cannot continue.");
}
if (zipFiles.Length == 1)
{
var zipFile = zipFiles[0];
var assetsRoot = System.IO.Path.GetFullPath(Global.FilePathPrefix);
if (!assetsRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) && !assetsRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
{
assetsRoot += Path.DirectorySeparatorChar;
}
Debug.LogMessage(LogEventLevel.Information, "Found assets zip file: {zipFile:l}... Unzipping...", zipFile.FullName);
using (var archive = ZipFile.OpenRead(zipFile.FullName))
{
foreach (var entry in archive.Entries)
{
var destinationPath = Path.Combine(Global.FilePathPrefix, entry.FullName);
var fullDest = System.IO.Path.GetFullPath(destinationPath);
if (!fullDest.StartsWith(assetsRoot, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
if (string.IsNullOrEmpty(entry.Name))
{
Directory.CreateDirectory(destinationPath);
continue;
}
// If a directory exists where a file should go, delete it
if (Directory.Exists(destinationPath))
Directory.Delete(destinationPath, true);
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
entry.ExtractToFile(destinationPath, true);
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
}
}
}
// cleaning up zip files
foreach (var file in zipFiles)
{
File.Delete(file.FullName);
}
var htmlZipFiles = applicationDirectory.GetFiles("htmlassets*.zip");
if (htmlZipFiles.Length > 1)
{
throw new Exception("Multiple htmlassets zip files found in application directory. Please ensure only one htmlassets*.zip file is present and retry.");
}
if (htmlZipFiles.Length == 1)
{
var htmlZipFile = htmlZipFiles[0];
var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
var userOrNvramDir = programDir.Parent;
var rootDir = userOrNvramDir?.Parent;
if (rootDir == null)
{
throw new Exception($"Unable to determine root directory for html extraction. Current path: {Global.FilePathPrefix}");
}
var htmlDir = Path.Combine(rootDir.FullName, "html");
var htmlRoot = System.IO.Path.GetFullPath(htmlDir);
if (!htmlRoot.EndsWith(Path.DirectorySeparatorChar.ToString()) &&
!htmlRoot.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
{
htmlRoot += Path.DirectorySeparatorChar;
}
Debug.LogMessage(LogEventLevel.Information, "Found htmlassets zip file: {zipFile:l}... Unzipping...", htmlZipFile.FullName);
using (var archive = ZipFile.OpenRead(htmlZipFile.FullName))
{
foreach (var entry in archive.Entries)
{
var destinationPath = Path.Combine(htmlDir, entry.FullName);
var fullDest = System.IO.Path.GetFullPath(destinationPath);
if (!fullDest.StartsWith(htmlRoot, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException($"Entry '{entry.FullName}' is trying to extract outside of the target directory.");
if (string.IsNullOrEmpty(entry.Name))
{
Directory.CreateDirectory(destinationPath);
continue;
}
// Only delete the file if it exists and is a file, not a directory
if (File.Exists(destinationPath))
File.Delete(destinationPath);
var parentDir = Path.GetDirectoryName(destinationPath);
if (!string.IsNullOrEmpty(parentDir))
Directory.CreateDirectory(parentDir);
entry.ExtractToFile(destinationPath, true);
Debug.LogMessage(LogEventLevel.Information, "Extracted: {entry:l} to {Destination}", entry.FullName, destinationPath);
}
}
}
// cleaning up html zip files
foreach (var file in htmlZipFiles)
{
File.Delete(file.FullName);
}
var jsonFiles = applicationDirectory.GetFiles("*configurationFile*.json");
if (jsonFiles.Length > 1)
{
throw new Exception("Multiple configuration files found. Cannot continue.");
}
if (jsonFiles.Length == 1)
{
var jsonFile = jsonFiles[0];
var finalPath = Path.Combine(Global.FilePathPrefix, jsonFile.Name);
Debug.LogMessage(LogEventLevel.Information, "Found configuration file: {jsonFile:l}... Moving to: {Destination}", jsonFile.FullName, finalPath);
if (File.Exists(finalPath))
{
Debug.LogMessage(LogEventLevel.Information, "Removing existing configuration file: {Destination}", finalPath);
File.Delete(finalPath);
}
jsonFile.MoveTo(finalPath);
}
}
}
}

View File

@@ -20,9 +20,9 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug 4.7.2|AnyCPU'">
<DebugType>full</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<DocumentationFile>bin\$(Configuration)\PepperDashEssentials.xml</DocumentationFile>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<DocumentationFile>bin\$(Configuration)\PepperDashEssentials.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include="Example Configuration\EssentialsHuddleSpaceRoom\configurationFile-HuddleSpace-2-Source.json">
@@ -48,8 +48,7 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.157" />
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />