mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
Compare commits
56 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f05654638 | ||
|
|
4ca59e3186 | ||
|
|
a55e4811b3 | ||
|
|
5cac0bb6c8 | ||
|
|
44e5ff4c3e | ||
|
|
ec9753b19e | ||
|
|
12e5ff752d | ||
|
|
85a4b635f6 | ||
|
|
aff4c1e897 | ||
|
|
3afc73a446 | ||
|
|
3c9f5e95e0 | ||
|
|
f70c78bcc5 | ||
|
|
5b5619764f | ||
|
|
d21cef0e60 | ||
|
|
1719de195f | ||
|
|
6949253b1a | ||
|
|
e23c987516 | ||
|
|
14cd082076 | ||
|
|
36c15b8d95 | ||
|
|
fbf4c56403 | ||
|
|
f4cb17314d | ||
|
|
aa76551500 | ||
|
|
12828e8dd4 | ||
|
|
4dca16e9bd | ||
|
|
d4284bd59f | ||
|
|
a8ec831064 | ||
|
|
6ceddcf571 | ||
|
|
02f507ccb1 | ||
|
|
22ef9cf7c2 | ||
|
|
02216372bc | ||
|
|
c323c872fc | ||
|
|
1e63b10aa4 | ||
|
|
47e186d8f4 | ||
|
|
2f7789b374 | ||
|
|
588057f3ba | ||
|
|
64d60dacc2 | ||
|
|
8d3edde28c | ||
|
|
a220474101 | ||
|
|
b982219e2f | ||
|
|
e03e45ccf5 | ||
|
|
a7b839296e | ||
|
|
6a4e3d5be2 | ||
|
|
d18fca8a98 | ||
|
|
162a06f9e9 | ||
|
|
b32bab0d33 | ||
|
|
74a12c9671 | ||
|
|
92d13f29a9 | ||
|
|
6587444970 | ||
|
|
94c9e75d8f | ||
|
|
21217341b5 | ||
|
|
d0ab42ccb7 | ||
|
|
1b10910cc2 | ||
|
|
84c730b7a1 | ||
|
|
6db7581295 | ||
|
|
76311b83ff | ||
|
|
8817d70f07 |
26 changed files with 845 additions and 76 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>2.29.0-local</Version>
|
<Version>2.42.1-local</Version>
|
||||||
<InformationalVersion>$(Version)</InformationalVersion>
|
<InformationalVersion>$(Version)</InformationalVersion>
|
||||||
<Authors>PepperDash Technology</Authors>
|
<Authors>PepperDash Technology</Authors>
|
||||||
<Company>PepperDash Technology</Company>
|
<Company>PepperDash Technology</Company>
|
||||||
<Product>PepperDash Essentials</Product>
|
<Product>PepperDash Essentials</Product>
|
||||||
<Copyright>Copyright © 2025</Copyright>
|
<Copyright>Copyright © 2026</Copyright>
|
||||||
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
<RepositoryUrl>https://github.com/PepperDash/Essentials</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageTags>Crestron; 4series</PackageTags>
|
<PackageTags>Crestron; 4series</PackageTags>
|
||||||
|
|
@ -20,4 +20,7 @@
|
||||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
|
||||||
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyMetadata Include="PackageId" Value="$(PackageId)" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,8 @@ namespace PepperDash.Core
|
||||||
// Thread-safety lock for state changes
|
// Thread-safety lock for state changes
|
||||||
private readonly object _stateLock = new object();
|
private readonly object _stateLock = new object();
|
||||||
|
|
||||||
|
private volatile bool _isProgramStopping;
|
||||||
|
|
||||||
private bool disconnectLogged = false;
|
private bool disconnectLogged = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -207,11 +209,9 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
if (programEventType == eProgramStatusEventType.Stopping)
|
if (programEventType == eProgramStatusEventType.Stopping)
|
||||||
{
|
{
|
||||||
if (client != null)
|
_isProgramStopping = true;
|
||||||
{
|
this.LogDebug("Program stopping. Closing connection");
|
||||||
this.LogDebug("Program stopping. Closing connection");
|
Disconnect();
|
||||||
Disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,6 +228,12 @@ namespace PepperDash.Core
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isProgramStopping)
|
||||||
|
{
|
||||||
|
this.LogDebug("Skipping connect because program is stopping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ConnectEnabled = true;
|
ConnectEnabled = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -287,13 +293,7 @@ namespace PepperDash.Core
|
||||||
}
|
}
|
||||||
catch (SshConnectionException e)
|
catch (SshConnectionException e)
|
||||||
{
|
{
|
||||||
var ie = e.InnerException; // The details are inside!!
|
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
|
||||||
|
|
||||||
if (ie is SocketException)
|
|
||||||
{
|
|
||||||
this.LogError("CONNECTION failure: Cannot reach host");
|
|
||||||
this.LogVerbose(ie, "Exception details: ");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ie is System.Net.Sockets.SocketException socketException)
|
if (ie is System.Net.Sockets.SocketException socketException)
|
||||||
{
|
{
|
||||||
|
|
@ -301,20 +301,20 @@ namespace PepperDash.Core
|
||||||
Hostname, Port);
|
Hostname, Port);
|
||||||
this.LogVerbose(socketException, "SocketException details: ");
|
this.LogVerbose(socketException, "SocketException details: ");
|
||||||
}
|
}
|
||||||
if (ie is SshAuthenticationException)
|
else if (ie is SshAuthenticationException)
|
||||||
{
|
{
|
||||||
this.LogError("Authentication failure for username {userName}", Username);
|
this.LogError("Authentication failure for username {userName}", Username);
|
||||||
this.LogVerbose(ie, "AuthenticationException details: ");
|
this.LogVerbose(ie, "AuthenticationException details: ");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.LogError("Error on connect: {error}", ie.Message);
|
this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
|
||||||
this.LogVerbose(ie, "Exception details: ");
|
this.LogVerbose(ie ?? e, "Exception details: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect)
|
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -326,7 +326,7 @@ namespace PepperDash.Core
|
||||||
|
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect)
|
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -338,7 +338,7 @@ namespace PepperDash.Core
|
||||||
this.LogVerbose(e, "Exception details: ");
|
this.LogVerbose(e, "Exception details: ");
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect)
|
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -473,7 +473,7 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
connectLock.Release();
|
connectLock.Release();
|
||||||
}
|
}
|
||||||
if (AutoReconnect && ConnectEnabled)
|
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -516,7 +516,10 @@ namespace PepperDash.Core
|
||||||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||||
|
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
StartReconnectTimer();
|
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||||
|
{
|
||||||
|
StartReconnectTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -549,7 +552,10 @@ namespace PepperDash.Core
|
||||||
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||||
|
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
StartReconnectTimer();
|
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||||
|
{
|
||||||
|
StartReconnectTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.274" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
ICec Port;
|
ICec Port;
|
||||||
|
|
||||||
|
bool _cecSubscribed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -54,7 +56,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
Port = postActivationFunc(config);
|
Port = postActivationFunc(config);
|
||||||
|
|
||||||
Port.StreamCec.CecChange += StreamCec_CecChange;
|
TryEnsureCecSubscription();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +70,29 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
Port = port;
|
Port = port;
|
||||||
|
|
||||||
|
TryEnsureCecSubscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to the CEC change event once <see cref="ICec.StreamCec"/> is available.
|
||||||
|
/// Safe to call repeatedly; the subscription is only wired a single time.
|
||||||
|
/// If StreamCec is null during construction, this is retried when send methods invoke
|
||||||
|
/// this method later.
|
||||||
|
/// </summary>
|
||||||
|
void TryEnsureCecSubscription()
|
||||||
|
{
|
||||||
|
if (_cecSubscribed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Port?.StreamCec == null)
|
||||||
|
{
|
||||||
|
Debug.LogMessage(LogEventLevel.Warning, this, "StreamCec is not available; CEC feedback is deferred until the device is ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange);
|
Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange);
|
||||||
|
_cecSubscribed = true;
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, this, "Subscribed to CEC feedback");
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamCec_CecChange(Cec cecDevice, CecEventArgs args)
|
void StreamCec_CecChange(Cec cecDevice, CecEventArgs args)
|
||||||
|
|
@ -104,8 +128,9 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SendText(string text)
|
public void SendText(string text)
|
||||||
{
|
{
|
||||||
if (Port == null)
|
if (Port?.StreamCec == null)
|
||||||
return;
|
return;
|
||||||
|
TryEnsureCecSubscription();
|
||||||
this.PrintSentText(text);
|
this.PrintSentText(text);
|
||||||
Port.StreamCec.Send.StringValue = text;
|
Port.StreamCec.Send.StringValue = text;
|
||||||
}
|
}
|
||||||
|
|
@ -115,8 +140,9 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SendBytes(byte[] bytes)
|
public void SendBytes(byte[] bytes)
|
||||||
{
|
{
|
||||||
if (Port == null)
|
if (Port?.StreamCec == null)
|
||||||
return;
|
return;
|
||||||
|
TryEnsureCecSubscription();
|
||||||
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||||
this.PrintSentBytes(bytes);
|
this.PrintSentBytes(bytes);
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,14 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
{
|
{
|
||||||
var parsedConfig = JObject.Parse(fs.ReadToEnd());
|
var parsedConfig = JObject.Parse(fs.ReadToEnd());
|
||||||
|
|
||||||
// Check if it's a v2 config (check for "version" node)
|
// A config is v1 if it has separate "system" and "template" nodes that
|
||||||
// this means it's already merged by the Portal API
|
// need to be merged. A v2 config is already merged by the Portal API and
|
||||||
// from the v2 config tool
|
// will not have "system"/"template" nodes. This is independent of whether
|
||||||
var isV2Config = parsedConfig["versions"] != null;
|
// a "versions" node is present, which only carries version metadata and
|
||||||
|
// can appear on either a v1 or v2 config.
|
||||||
if (isV2Config)
|
var isV1Config = parsedConfig["system"] != null && parsedConfig["template"] != null;
|
||||||
|
|
||||||
|
if (!isV1Config)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
|
||||||
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
|
||||||
|
|
@ -148,6 +150,8 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, "Config file is a v1 format, merging system and template.");
|
||||||
|
|
||||||
// Extract SystemUrl and TemplateUrl into final config output
|
// Extract SystemUrl and TemplateUrl into final config output
|
||||||
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
|
||||||
|
|
||||||
|
|
@ -160,6 +164,13 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
{
|
{
|
||||||
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
|
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MergeConfigs does not carry the "versions" node forward, so it must be
|
||||||
|
// applied separately to ensure it's preserved in the merged config.
|
||||||
|
if (parsedConfig["versions"] != null)
|
||||||
|
{
|
||||||
|
ConfigObject.Versions = parsedConfig["versions"].ToObject<VersionData>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
|
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Versions
|
/// Gets or sets the Versions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[JsonProperty("versions")]
|
||||||
public VersionData Versions { get; set; }
|
public VersionData Versions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -134,12 +135,25 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
[JsonProperty("packages")]
|
[JsonProperty("packages")]
|
||||||
public List<NugetVersion> Packages { get; set; }
|
public List<NugetVersion> Packages { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the touchpanel wrapper app version
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("touchpanelWrapperApp")]
|
||||||
|
public NugetVersion TouchpanelWrapperApp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of user interface packages
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("userInterfaces")]
|
||||||
|
public List<NugetVersion> UserInterfaces { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
/// Initializes a new instance of the <see cref="VersionData"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VersionData()
|
public VersionData()
|
||||||
{
|
{
|
||||||
Packages = new List<NugetVersion>();
|
Packages = new List<NugetVersion>();
|
||||||
|
UserInterfaces = new List<NugetVersion>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,8 +171,20 @@ namespace PepperDash.Essentials.Core.Config
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the PackageId
|
/// Gets or sets the PackageId
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("packageId")]
|
[JsonProperty("packageId", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public string PackageId { get; set; }
|
public string PackageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the RepoUrl
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("repoUrl", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string RepoUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the human-readable name
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public string Name { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the contract for a wireless presentation endpoint that reports whether a wireless
|
||||||
|
/// sharing session is currently active. Implemented by platforms such as Crestron AirMedia,
|
||||||
|
/// Mersive Solstice, Barco ClickShare, Miracast/Teams receivers, etc. Allows consumers (e.g.
|
||||||
|
/// room plugins) to react to wireless sharing activity without taking a dependency on any
|
||||||
|
/// concrete device implementation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This refers specifically to wireless screen/device mirroring, as distinct from in-call
|
||||||
|
/// content sharing on a video conference.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IHasWirelessSharing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reports whether a wireless sharing session is currently active (content is being presented).
|
||||||
|
/// </summary>
|
||||||
|
BoolFeedback IsSharingFeedback { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when wireless sharing starts or stops. The event args carry the new sharing state.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<WirelessSharingEventArgs> SharingChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event arguments describing a change in wireless sharing state.
|
||||||
|
/// </summary>
|
||||||
|
public class WirelessSharingEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True if a wireless sharing session is active (content is being presented), false otherwise.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSharing { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="WirelessSharingEventArgs"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="isSharing">True if a wireless sharing session is active, false otherwise.</param>
|
||||||
|
public WirelessSharingEventArgs(bool isSharing)
|
||||||
|
{
|
||||||
|
IsSharing = isSharing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for network switches that support VLAN assignment on individual ports.
|
||||||
|
/// </summary>
|
||||||
|
public interface INetworkSwitchVlanManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the current access VLAN ID configured on the port.
|
||||||
|
/// Return -1 when the value is unavailable (e.g. the switch has not been polled yet
|
||||||
|
/// or the implementation does not support VLAN queries).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">Switch port identifier</param>
|
||||||
|
/// <returns>VLAN ID or -1 when unavailable</returns>
|
||||||
|
int GetPortCurrentVlan(string port);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the access VLAN of a single switch port.
|
||||||
|
/// The implementation is responsible for entering/exiting privileged/config mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">Switch port identifier (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco)</param>
|
||||||
|
/// <param name="vlanId">Target VLAN ID (1-4093)</param>
|
||||||
|
void SetPortVlan(string port, uint vlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for network switches that support Power over Ethernet (PoE) control on individual ports.
|
||||||
|
/// </summary>
|
||||||
|
public interface INetworkSwitchPoeManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables PoE power delivery on a single switch port.
|
||||||
|
/// The implementation is responsible for entering/exiting privileged/config mode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">Switch port identifier</param>
|
||||||
|
/// <param name="enabled">True to enable PoE; false to disable PoE</param>
|
||||||
|
void SetPortPoeState(string port, bool enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Standardized interface for network switch devices that support per-port PoE control
|
||||||
|
/// and VLAN assignment.
|
||||||
|
/// </summary>
|
||||||
|
public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Event that is raised when the state of a switch port changes, such as a VLAN change or PoE state change.
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<NetworkSwitchPortEventArgs> PortStateChanged;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes.
|
||||||
|
/// </summary>
|
||||||
|
public class NetworkSwitchPortEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The identifier of the port that changed state (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco).
|
||||||
|
/// </summary>
|
||||||
|
public string Port { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of event that occurred on the port (e.g. VLAN change, PoE enabled/disabled).
|
||||||
|
/// </summary>
|
||||||
|
public NetworkSwitchPortEventType EventType { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor for NetworkSwitchPortEventArgs
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="port">The identifier of the port that changed state</param>
|
||||||
|
/// <param name="eventType">The type of event that occurred on the port</param>
|
||||||
|
public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventType)
|
||||||
|
{
|
||||||
|
Port = port;
|
||||||
|
EventType = eventType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeration of network switch port state change event types (e.g. VLAN changes or PoE state changes).
|
||||||
|
/// </summary>
|
||||||
|
public enum NetworkSwitchPortEventType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the type of event is unknown or cannot be determined.
|
||||||
|
/// </summary>
|
||||||
|
Unknown,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a VLAN change is in progress on the port, either through a call to SetPortVlan or an external change detected by polling.
|
||||||
|
/// </summary>
|
||||||
|
VlanChangeInProgress,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan or an external change detected by polling.
|
||||||
|
/// </summary>
|
||||||
|
VlanChanged,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that PoE is being disabled on the port, either through a call to SetPortPoeState or an external change detected by polling.
|
||||||
|
/// </summary>
|
||||||
|
PoeDisableInProgress,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that PoE has been disabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
|
||||||
|
/// </summary>
|
||||||
|
PoEDisabled,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that PoE is being enabled on the port, either through a call to SetPortPoeState or an external change detected by polling.
|
||||||
|
/// </summary>
|
||||||
|
PoeEnableInProgress,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that PoE has been enabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
|
||||||
|
/// </summary>
|
||||||
|
PoEEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs
Normal file
14
src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace PepperDash.Essentials.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the contract for IHasDspPresetSave
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasDspPresetSave : IDspPresets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Saves the preset by key
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">key of preset to save</param>
|
||||||
|
void SavePreset(string key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
||||||
|
|
|
||||||
|
|
@ -343,12 +343,14 @@ namespace PepperDash.Essentials.Core
|
||||||
IndexTieLines();
|
IndexTieLines();
|
||||||
}
|
}
|
||||||
|
|
||||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>();
|
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>()
|
||||||
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>();
|
.Where(d => !(d is IRoutingInputsOutputs)).ToList();
|
||||||
|
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>()
|
||||||
|
.Where(d => !(d is IRoutingInputsOutputs)).ToList();
|
||||||
|
|
||||||
foreach (var sink in sinks.Where(d => !(d is IRoutingInputsOutputs)))
|
foreach (var sink in sinks)
|
||||||
{
|
{
|
||||||
foreach (var source in sources.Where(d => !(d is IRoutingInputsOutputs)))
|
foreach (var source in sources)
|
||||||
{
|
{
|
||||||
foreach (var inputPort in sink.InputPorts)
|
foreach (var inputPort in sink.InputPorts)
|
||||||
{
|
{
|
||||||
|
|
@ -373,6 +375,10 @@ namespace PepperDash.Essentials.Core
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.LogVerbose("Route mapped: {source} -> {sink} via {input}/{output}, type {type}",
|
||||||
|
source.Key, sink.Key,
|
||||||
|
inputPort.Key, outputPort.Key, audioOrSingleRoute.SignalType);
|
||||||
|
|
||||||
// Add to the appropriate collection(s) based on signal type
|
// Add to the appropriate collection(s) based on signal type
|
||||||
// Note: A single route descriptor with combined flags (e.g., AudioVideo) will be added once per matching signal type
|
// Note: A single route descriptor with combined flags (e.g., AudioVideo) will be added once per matching signal type
|
||||||
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio))
|
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio))
|
||||||
|
|
@ -404,6 +410,10 @@ namespace PepperDash.Essentials.Core
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.LogVerbose("Video route mapped: {source} -> {sink} via {input}/{output}",
|
||||||
|
source.Key, sink.Key,
|
||||||
|
inputPort.Key, outputPort.Key);
|
||||||
|
|
||||||
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
|
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -609,10 +619,12 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
// No direct tie? Run back out on the inputs' attached devices...
|
// No direct tie? Run back out on the inputs' attached devices...
|
||||||
// Only the ones that are routing devices
|
// Only the ones that are routing devices
|
||||||
var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs);
|
var midpointTieLines = destinationTieLines
|
||||||
|
.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs)
|
||||||
Debug.LogVerbose(destination, "Found {tieLineCount} tie lines to walk for {destinationKey}", midpointTieLines.Count(), destination.Key);
|
.ToList();
|
||||||
|
|
||||||
|
Debug.LogVerbose(destination, "Found {tieLineCount} tie lines to walk for {destinationKey}", midpointTieLines.Count, destination.Key);
|
||||||
|
|
||||||
//Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
|
//Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
|
||||||
if (alreadyCheckedDevices == null)
|
if (alreadyCheckedDevices == null)
|
||||||
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
alreadyCheckedDevices = new List<IRoutingInputsOutputs>();
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,11 @@ namespace PepperDash.Essentials.Core.Web
|
||||||
Name = "ReportVersions",
|
Name = "ReportVersions",
|
||||||
RouteHandler = new ReportVersionsRequestHandler()
|
RouteHandler = new ReportVersionsRequestHandler()
|
||||||
},
|
},
|
||||||
|
new HttpCwsRoute("packageManifest")
|
||||||
|
{
|
||||||
|
Name = "GetPackageManifest",
|
||||||
|
RouteHandler = new GetPackageManifestRequestHandler()
|
||||||
|
},
|
||||||
new HttpCwsRoute("appdebug")
|
new HttpCwsRoute("appdebug")
|
||||||
{
|
{
|
||||||
Name = "AppDebug",
|
Name = "AppDebug",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
using System.Linq;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using Crestron.SimplSharp.WebScripting;
|
using Crestron.SimplSharp.WebScripting;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using PepperDash.Core.Web.RequestHandlers;
|
using PepperDash.Core.Web.RequestHandlers;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a GetFeedbacksForDeviceRequestHandler
|
/// Represents a GetFeedbacksForDeviceRequestHandler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
|
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -76,7 +77,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
Value = feedback.IntValue
|
Value = feedback.IntValue
|
||||||
};
|
};
|
||||||
|
|
||||||
var stringFeedback =
|
var stringFeedback =
|
||||||
from feedback in device.Feedbacks.OfType<StringFeedback>()
|
from feedback in device.Feedbacks.OfType<StringFeedback>()
|
||||||
where !string.IsNullOrEmpty(feedback.Key)
|
where !string.IsNullOrEmpty(feedback.Key)
|
||||||
select new
|
select new
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Crestron.SimplSharp.WebScripting;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using PepperDash.Core.Web.RequestHandlers;
|
||||||
|
using PepperDash.Essentials.Core.Config;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a GetPackageManifestRequestHandler
|
||||||
|
/// </summary>
|
||||||
|
public class GetPackageManifestRequestHandler : WebApiBaseRequestHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// base(true) enables CORS support by default
|
||||||
|
/// </remarks>
|
||||||
|
public GetPackageManifestRequestHandler()
|
||||||
|
: base(true)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles GET method requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
protected override void HandleGet(HttpCwsContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = CloneVersionData(ConfigReader.ConfigObject?.Versions) ?? new VersionData();
|
||||||
|
|
||||||
|
PopulateEssentials(result);
|
||||||
|
PopulatePackages(result);
|
||||||
|
|
||||||
|
var js = JsonConvert.SerializeObject(result, Formatting.Indented);
|
||||||
|
|
||||||
|
context.Response.StatusCode = 200;
|
||||||
|
context.Response.StatusDescription = "OK";
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
|
||||||
|
context.Response.Write(js, false);
|
||||||
|
context.Response.End();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PepperDash.Core.Debug.LogMessage(ex, "Exception handling GET /packageManifest request");
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
context.Response.StatusDescription = "Internal Server Error";
|
||||||
|
context.Response.End();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deep-copies the config's VersionData so the live config object is never mutated
|
||||||
|
/// </summary>
|
||||||
|
private static VersionData CloneVersionData(VersionData source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonConvert.SerializeObject(source);
|
||||||
|
return JsonConvert.DeserializeObject<VersionData>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enriches (or creates) the essentials entry from the loaded PepperDash.Essentials.Core assembly
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulateEssentials(VersionData result)
|
||||||
|
{
|
||||||
|
var essentials = result.Essentials ?? new NugetVersion();
|
||||||
|
|
||||||
|
essentials.Version = Global.AssemblyVersion;
|
||||||
|
|
||||||
|
// The main program assembly (PackageId "PepperDashEssentials") is what's actually published
|
||||||
|
// to NuGet, but this handler lives in PepperDash.Essentials.Core, which can't reference that
|
||||||
|
// project's types directly (Essentials -> Core, not the reverse). PluginLoader.EssentialsAssembly.Assembly
|
||||||
|
// is unreliable (often left null - see PluginLoader.SetEssentialsAssembly), so look it up
|
||||||
|
// directly from the loaded AppDomain by its Directory.Build.props-embedded PackageId metadata
|
||||||
|
// (every project's .csproj sets its own PackageId explicitly), falling back to this handler's
|
||||||
|
// own (Core) assembly if it can't be found.
|
||||||
|
var essentialsAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.FirstOrDefault(a => string.Equals(GetAssemblyMetadataValue(a, "PackageId"), "PepperDashEssentials", StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? typeof(GetPackageManifestRequestHandler).Assembly;
|
||||||
|
|
||||||
|
var repoUrl = TrimTrailingGit(GetAssemblyMetadataValue(essentialsAssembly, "RepositoryUrl"));
|
||||||
|
if (!string.IsNullOrEmpty(repoUrl))
|
||||||
|
{
|
||||||
|
essentials.RepoUrl = repoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = GetAssemblyProduct(essentialsAssembly);
|
||||||
|
if (!string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
essentials.Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the PackageId embedded via Directory.Build.props' AssemblyMetadata item (the
|
||||||
|
// authoritative source, matching the actual published PackageId) over any config-supplied
|
||||||
|
// or hardcoded value.
|
||||||
|
var reflectedPackageId = GetAssemblyMetadataValue(essentialsAssembly, "PackageId");
|
||||||
|
if (!string.IsNullOrEmpty(reflectedPackageId))
|
||||||
|
{
|
||||||
|
essentials.PackageId = reflectedPackageId;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(essentials.PackageId))
|
||||||
|
{
|
||||||
|
essentials.PackageId = "PepperDashEssentials";
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Essentials = essentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges reflection data from loaded plugin assemblies with the config's packages list
|
||||||
|
/// </summary>
|
||||||
|
private static void PopulatePackages(VersionData result)
|
||||||
|
{
|
||||||
|
// Filter out null entries defensively - the packages list is deserialized from user-editable
|
||||||
|
// config JSON, so a malformed "packages": [null, ...] shouldn't throw and 500 the endpoint.
|
||||||
|
var configPackages = (result.Packages ?? new System.Collections.Generic.List<NugetVersion>())
|
||||||
|
.Where(p => p != null)
|
||||||
|
.ToList();
|
||||||
|
var matchedConfigPackages = new System.Collections.Generic.HashSet<NugetVersion>();
|
||||||
|
var mergedPackages = new System.Collections.Generic.List<NugetVersion>();
|
||||||
|
|
||||||
|
foreach (var loaded in PluginLoader.EssentialsPluginAssemblies.Where(a => a.Assembly != null))
|
||||||
|
{
|
||||||
|
var reflectedVersion = loaded.Version;
|
||||||
|
if (string.IsNullOrEmpty(reflectedVersion))
|
||||||
|
{
|
||||||
|
// Never emit an entry with no version - the extension's parser drops entries
|
||||||
|
// whose version isn't a string.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reflectedRepoUrl = TrimTrailingGit(GetAssemblyMetadataValue(loaded.Assembly, "RepositoryUrl"));
|
||||||
|
var reflectedName = GetAssemblyProduct(loaded.Assembly);
|
||||||
|
|
||||||
|
// Plugins built from a Directory.Build.props that embeds
|
||||||
|
// <AssemblyMetadata Include="PackageId" Value="$(PackageId)" /> carry their PackageId
|
||||||
|
// directly - this is authoritative and should be preferred over the title/name fallback chain.
|
||||||
|
var reflectedPackageId = GetAssemblyMetadataValue(loaded.Assembly, "PackageId");
|
||||||
|
|
||||||
|
var assemblyTitle = GetAssemblyTitle(loaded.Assembly);
|
||||||
|
var assemblyName = loaded.Assembly.GetName().Name;
|
||||||
|
var assemblyNameNoSeriesSuffix = StripTrailingSeriesSuffix(assemblyName);
|
||||||
|
|
||||||
|
var match = configPackages.FirstOrDefault(p =>
|
||||||
|
!matchedConfigPackages.Contains(p) &&
|
||||||
|
!string.IsNullOrEmpty(p.PackageId) &&
|
||||||
|
(string.Equals(p.PackageId, reflectedPackageId, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(p.PackageId, assemblyTitle, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(p.PackageId, assemblyName, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(p.PackageId, assemblyNameNoSeriesSuffix, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
if (match != null)
|
||||||
|
{
|
||||||
|
matchedConfigPackages.Add(match);
|
||||||
|
|
||||||
|
mergedPackages.Add(new NugetVersion
|
||||||
|
{
|
||||||
|
Name = !string.IsNullOrEmpty(match.Name) ? match.Name : reflectedName,
|
||||||
|
RepoUrl = !string.IsNullOrEmpty(match.RepoUrl) ? match.RepoUrl : reflectedRepoUrl,
|
||||||
|
PackageId = !string.IsNullOrEmpty(reflectedPackageId) ? reflectedPackageId : match.PackageId,
|
||||||
|
Version = reflectedVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Loaded but not present (or not matched) in config - emit the reflected PackageId
|
||||||
|
// when the assembly carries one, otherwise leave it null as before.
|
||||||
|
mergedPackages.Add(new NugetVersion
|
||||||
|
{
|
||||||
|
Name = reflectedName,
|
||||||
|
RepoUrl = reflectedRepoUrl,
|
||||||
|
PackageId = reflectedPackageId,
|
||||||
|
Version = reflectedVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configured but not currently loaded - pass through unchanged
|
||||||
|
mergedPackages.AddRange(configPackages.Where(p => !matchedConfigPackages.Contains(p)));
|
||||||
|
|
||||||
|
result.Packages = mergedPackages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyMetadataValue(Assembly assembly, string key)
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = assembly.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
|
||||||
|
.Cast<AssemblyMetadataAttribute>()
|
||||||
|
.FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
return match?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyProduct(Assembly assembly)
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = assembly.GetCustomAttributes(typeof(AssemblyProductAttribute), false)
|
||||||
|
.FirstOrDefault() as AssemblyProductAttribute;
|
||||||
|
|
||||||
|
return attribute?.Product;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAssemblyTitle(Assembly assembly)
|
||||||
|
{
|
||||||
|
if (assembly == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false)
|
||||||
|
.FirstOrDefault() as AssemblyTitleAttribute;
|
||||||
|
|
||||||
|
return attribute?.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StripTrailingSeriesSuffix(string assemblyName)
|
||||||
|
{
|
||||||
|
const string suffix = ".4Series";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(assemblyName) || !assemblyName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return assemblyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return assemblyName.Substring(0, assemblyName.Length - suffix.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string TrimTrailingGit(string repoUrl)
|
||||||
|
{
|
||||||
|
const string suffix = ".git";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(repoUrl) || !repoUrl.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return repoUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoUrl.Substring(0, repoUrl.Length - suffix.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -152,6 +152,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
||||||
|
|
||||||
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
|
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (Config.DisableAutoRaiseOnPowerOff)
|
||||||
|
{
|
||||||
|
this.LogDebug(
|
||||||
|
"Auto-raise on power-off disabled for {type}; leaving position unchanged (manual control only)",
|
||||||
|
Type
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!DisplayDevice.IsCoolingDownFeedback.BoolValue
|
!DisplayDevice.IsCoolingDownFeedback.BoolValue
|
||||||
&& Type == eScreenLiftControlType.lift
|
&& Type == eScreenLiftControlType.lift
|
||||||
|
|
@ -174,6 +183,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
||||||
{
|
{
|
||||||
if (DisplayDevice.IsWarmingUpFeedback.BoolValue)
|
if (DisplayDevice.IsWarmingUpFeedback.BoolValue)
|
||||||
{
|
{
|
||||||
|
if (Config.DisableAutoLowerOnPowerOn)
|
||||||
|
{
|
||||||
|
this.LogDebug(
|
||||||
|
"Auto-lower on power-on disabled for {type}; leaving position unchanged (manual control only)",
|
||||||
|
Type
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Lower();
|
Lower();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,5 +41,23 @@ namespace PepperDash.Essentials.Devices.Common.Shades
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("muteOnScreenUp")]
|
[JsonProperty("muteOnScreenUp")]
|
||||||
public bool MuteOnScreenUp { get; set; }
|
public bool MuteOnScreenUp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, this controller does NOT automatically lower when its assigned display powers on
|
||||||
|
/// (warms up). Manual Raise/Lower still work, and the power-off auto-raise is unaffected. Intended
|
||||||
|
/// for a projector screen that must not auto-drop in a public space for safety, while the projector
|
||||||
|
/// lift (a separate controller) can still drop automatically.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("disableAutoLowerOnPowerOn")]
|
||||||
|
public bool DisableAutoLowerOnPowerOn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, this controller does NOT automatically raise when its assigned display powers off
|
||||||
|
/// (cools down). Manual Raise/Lower still work, and the power-on auto-lower is unaffected. The
|
||||||
|
/// companion to <see cref="DisableAutoLowerOnPowerOn"/>; together they make a controller fully
|
||||||
|
/// manual while leaving other controllers (e.g. the lift) on their default automatic behavior.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("disableAutoRaiseOnPowerOff")]
|
||||||
|
public bool DisableAutoRaiseOnPowerOff { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@
|
||||||
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The device key
|
/// The device key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("key")]
|
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the Key
|
/// Gets or sets the Key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -19,19 +19,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The device name
|
/// The device name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("name")]
|
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The type of the message class
|
/// The type of the message class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("messageType")]
|
[JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public string MessageType => GetType().Name;
|
public string MessageType => GetType().Name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the MessageBasePath
|
/// Gets or sets the MessageBasePath
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("messageBasePath")]
|
[JsonProperty("messageBasePath", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
|
||||||
public string MessageBasePath { get; set; }
|
public string MessageBasePath { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The interfaces implmented by the device sending the messsage
|
/// The interfaces implmented by the device sending the messsage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("interfaces")]
|
[JsonProperty("interfaces", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
[Obsolete("Interfaces are no longer supported and will be removed in a future release. Interfaces for all devices are now retrieved via the /joinroom endpoint in the MobileControlWebsocketServer")]
|
||||||
public List<string> Interfaces { get; private set; }
|
public List<string> Interfaces { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
using System;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the version information reported by a connected Mobile Control UI client
|
||||||
|
/// </summary>
|
||||||
|
public class ConnectedClientVersionInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the client id
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("clientId")]
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the room key the client joined
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("roomKey")]
|
||||||
|
public string RoomKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the touchpanel key the client joined as, if any
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("touchpanelKey")]
|
||||||
|
public string TouchpanelKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the app version reported by the client (e.g. the React app's build-time APP_VERSION)
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("appVersion")]
|
||||||
|
public string AppVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the expected app version from the system config's versions.touchpanelWrapperApp, if configured
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("expectedAppVersion")]
|
||||||
|
public string ExpectedAppVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the UTC time the client last reported this version
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("lastSeen")]
|
||||||
|
public DateTime LastSeen { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a copy of this instance, safe for callers outside the owning lock to hold/mutate
|
||||||
|
/// without affecting the internally tracked instance
|
||||||
|
/// </summary>
|
||||||
|
public ConnectedClientVersionInfo Clone() => new ConnectedClientVersionInfo
|
||||||
|
{
|
||||||
|
ClientId = ClientId,
|
||||||
|
RoomKey = RoomKey,
|
||||||
|
TouchpanelKey = TouchpanelKey,
|
||||||
|
AppVersion = AppVersion,
|
||||||
|
ExpectedAppVersion = ExpectedAppVersion,
|
||||||
|
LastSeen = LastSeen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,11 +69,32 @@ namespace PepperDash.Essentials
|
||||||
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
||||||
new Dictionary<string, IMobileControlMessenger>();
|
new Dictionary<string, IMobileControlMessenger>();
|
||||||
|
|
||||||
|
private readonly Dictionary<string, ConnectedClientVersionInfo> _connectedClientVersions =
|
||||||
|
new Dictionary<string, ConnectedClientVersionInfo>(StringComparer.InvariantCultureIgnoreCase);
|
||||||
|
|
||||||
|
private readonly object _connectedClientVersionsLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the custom messengers with subscriptions
|
/// Get the custom messengers with subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions> Messengers => new ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions>(_messengers.Values.OfType<IMobileControlMessengerWithSubscriptions>().ToDictionary(k => k.Key, v => v));
|
public ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions> Messengers => new ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions>(_messengers.Values.OfType<IMobileControlMessengerWithSubscriptions>().ToDictionary(k => k.Key, v => v));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the most recently reported UI app version for each connected client, keyed by clientId
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyDictionary<string, ConnectedClientVersionInfo> ConnectedClientVersions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_connectedClientVersionsLock)
|
||||||
|
{
|
||||||
|
return new ReadOnlyDictionary<string, ConnectedClientVersionInfo>(
|
||||||
|
_connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the default messengers
|
/// Get the default messengers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -1782,6 +1803,28 @@ namespace PepperDash.Essentials
|
||||||
" Not Enabled in Config.\r\n"
|
" Not Enabled in Config.\r\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var connectedClientVersions = ConnectedClientVersions;
|
||||||
|
|
||||||
|
if (connectedClientVersions.Count == 0)
|
||||||
|
{
|
||||||
|
CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions: None reported yet\r\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions:\r\n");
|
||||||
|
foreach (var kv in connectedClientVersions)
|
||||||
|
{
|
||||||
|
var v = kv.Value;
|
||||||
|
var match = string.IsNullOrEmpty(v.ExpectedAppVersion) || string.Equals(v.ExpectedAppVersion, v.AppVersion, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
CrestronConsole.ConsoleCommandResponse(
|
||||||
|
$" Client: {v.ClientId} Touchpanel: {v.TouchpanelKey} Room: {v.RoomKey}\r\n" +
|
||||||
|
$" Reported: {v.AppVersion} Expected: {(string.IsNullOrEmpty(v.ExpectedAppVersion) ? "(not configured)" : v.ExpectedAppVersion)} Match: {(match ? "Yes" : "NO - MISMATCH")}\r\n" +
|
||||||
|
$" Last Seen (UTC): {v.LastSeen:yyyy-MM-dd HH:mm:ss}\r\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -2181,6 +2224,8 @@ namespace PepperDash.Essentials
|
||||||
var roomKey = content["roomKey"].Value<string>();
|
var roomKey = content["roomKey"].Value<string>();
|
||||||
var touchpanelKey = content.SelectToken("touchpanelKey");
|
var touchpanelKey = content.SelectToken("touchpanelKey");
|
||||||
|
|
||||||
|
TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value<string>(), content.SelectToken("appVersion")?.Value<string>());
|
||||||
|
|
||||||
if (_roomCombiner == null)
|
if (_roomCombiner == null)
|
||||||
{
|
{
|
||||||
var message = new MobileControlMessage
|
var message = new MobileControlMessage
|
||||||
|
|
@ -2252,6 +2297,50 @@ namespace PepperDash.Essentials
|
||||||
SendTouchpanelKey(clientId, touchpanelKey);
|
SendTouchpanelKey(clientId, touchpanelKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the app version reported by a connecting UI client (e.g. the mobile control React app's
|
||||||
|
/// build-time APP_VERSION) and compares it against the configured versions.touchpanelWrapperApp version.
|
||||||
|
/// </summary>
|
||||||
|
private void TrackClientAppVersion(string clientId, string roomKey, string touchpanelKey, string appVersion)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(appVersion))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version;
|
||||||
|
|
||||||
|
var info = new ConnectedClientVersionInfo
|
||||||
|
{
|
||||||
|
ClientId = clientId,
|
||||||
|
RoomKey = roomKey,
|
||||||
|
TouchpanelKey = touchpanelKey,
|
||||||
|
AppVersion = appVersion,
|
||||||
|
ExpectedAppVersion = expectedVersion,
|
||||||
|
LastSeen = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (_connectedClientVersionsLock)
|
||||||
|
{
|
||||||
|
_connectedClientVersions[clientId] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(expectedVersion) && !string.Equals(expectedVersion, appVersion, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
this.LogWarning(
|
||||||
|
"Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}, which does not match configured versions.touchpanelWrapperApp version {expectedVersion}",
|
||||||
|
clientId, touchpanelKey, appVersion, expectedVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.LogVerbose(
|
||||||
|
"Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}",
|
||||||
|
clientId, touchpanelKey, appVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
|
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
|
||||||
{
|
{
|
||||||
if (touchpanelKeyToken == null)
|
if (touchpanelKeyToken == null)
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
||||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -25,11 +25,28 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
||||||
/// and mobile control functionality for Crestron touchpanels.
|
/// and mobile control functionality for Crestron touchpanels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
|
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme, ICommunicationMonitor
|
||||||
{
|
{
|
||||||
private readonly MobileControlTouchpanelProperties localConfig;
|
private readonly MobileControlTouchpanelProperties localConfig;
|
||||||
private IMobileControlRoomMessenger _bridge;
|
private IMobileControlRoomMessenger _bridge;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the CommunicationMonitor tracking the panel's online/offline state
|
||||||
|
/// </summary>
|
||||||
|
public StatusMonitorBase CommunicationMonitor { get; private set; }
|
||||||
|
|
||||||
|
private sealed class NullCommunicationMonitor : StatusMonitorBase
|
||||||
|
{
|
||||||
|
public NullCommunicationMonitor(IKeyed parent) : base(parent, 120000, 300000)
|
||||||
|
{
|
||||||
|
Status = MonitorStatus.InError;
|
||||||
|
Message = "Panel is not initialized";
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Start() { }
|
||||||
|
public override void Stop() { }
|
||||||
|
}
|
||||||
|
|
||||||
private string _appUrl;
|
private string _appUrl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -128,6 +145,11 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
{
|
{
|
||||||
localConfig = config;
|
localConfig = config;
|
||||||
|
|
||||||
|
if (panel != null)
|
||||||
|
{
|
||||||
|
CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000);
|
||||||
|
}
|
||||||
|
|
||||||
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
||||||
|
|
||||||
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
||||||
|
|
@ -366,6 +388,8 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override bool CustomActivate()
|
public override bool CustomActivate()
|
||||||
{
|
{
|
||||||
|
CommunicationMonitor?.Start();
|
||||||
|
|
||||||
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
||||||
|
|
||||||
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
||||||
|
|
@ -393,6 +417,17 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
return base.CustomActivate();
|
return base.CustomActivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the CommunicationMonitor on deactivation.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if deactivation was successful; otherwise, false.</returns>
|
||||||
|
public override bool Deactivate()
|
||||||
|
{
|
||||||
|
CommunicationMonitor?.Stop();
|
||||||
|
|
||||||
|
return base.Deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles device extender signal changes for system reserved signals.
|
/// Handles device extender signal changes for system reserved signals.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -519,15 +554,8 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
return false;
|
return false;
|
||||||
}) ? csIpAddress.ToString() : processorIp;
|
}) ? csIpAddress.ToString() : processorIp;
|
||||||
|
|
||||||
var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
|
// replace the host but preserve whatever scheme (http/https) is already present in the URL
|
||||||
if (match.Success)
|
var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}");
|
||||||
{
|
|
||||||
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);
|
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
|
||||||
|
|
||||||
|
|
@ -740,7 +768,7 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MobileControlTouchpanelControllerFactory()
|
public MobileControlTouchpanelControllerFactory()
|
||||||
{
|
{
|
||||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
|
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" };
|
||||||
MinimumEssentialsFrameworkVersion = "2.0.0";
|
MinimumEssentialsFrameworkVersion = "2.0.0";
|
||||||
|
|
||||||
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
||||||
|
|
@ -765,6 +793,8 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
||||||
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
|
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
|
||||||
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
|
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
|
||||||
|
{"tsw1080", (id, controlSystem, projectName) => new Tsw1080(id, controlSystem)},
|
||||||
|
{"ts1080", (id, controlSystem, projectName) => new Ts1080(id, controlSystem)},
|
||||||
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
|
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,16 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Port { get; private set; }
|
public int Port { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the HTTP scheme to use for generated URLs, based on whether the direct server is configured as secure
|
||||||
|
/// </summary>
|
||||||
|
private string HttpScheme => _parent.Config.DirectServer.Secure ? "https" : "http";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the WebSocket scheme to use for generated URLs, based on whether the direct server is configured as secure
|
||||||
|
/// </summary>
|
||||||
|
private string WsScheme => _parent.Config.DirectServer.Secure ? "wss" : "ws";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user app URL prefix
|
/// Gets the user app URL prefix
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -134,7 +144,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return string.Format("http://{0}:{1}{2}?token=",
|
return string.Format("{0}://{1}:{2}{3}?token=",
|
||||||
|
HttpScheme,
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port,
|
Port,
|
||||||
_userAppBaseHref);
|
_userAppBaseHref);
|
||||||
|
|
@ -273,7 +284,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
_server = new HttpServer(Port, false);
|
_server = new HttpServer(Port, _parent.Config.DirectServer.Secure);
|
||||||
|
|
||||||
_server.OnGet += Server_OnGet;
|
_server.OnGet += Server_OnGet;
|
||||||
|
|
||||||
|
|
@ -291,7 +302,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
ClientCertificateRequired = false,
|
ClientCertificateRequired = false,
|
||||||
CheckCertificateRevocation = false,
|
CheckCertificateRevocation = false,
|
||||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
|
EnabledSslProtocols = SslProtocols.Tls12
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -403,11 +414,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
ip = csIpAddress.ToString();
|
ip = csIpAddress.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
|
var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}";
|
||||||
|
|
||||||
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
||||||
|
|
||||||
touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
|
touchpanel.Touchpanel.SetAppUrl(appUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,7 +498,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
var config = new MobileControlApplicationConfig
|
var config = new MobileControlApplicationConfig
|
||||||
{
|
{
|
||||||
ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
|
ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port),
|
||||||
GatewayAppPath = "",
|
GatewayAppPath = "",
|
||||||
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
||||||
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
||||||
|
|
@ -1098,6 +1109,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.Close();
|
res.Close();
|
||||||
|
|
||||||
|
// remote log collector has no dedicated secure flag; keep it on http regardless of DirectServer.Secure
|
||||||
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
|
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
|
||||||
{
|
{
|
||||||
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||||
|
|
@ -1150,6 +1162,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
var qp = req.QueryString;
|
var qp = req.QueryString;
|
||||||
var token = qp["token"];
|
var token = qp["token"];
|
||||||
|
|
||||||
|
// Each join mints a single-use clientId; the panel webview must never replay a cached
|
||||||
|
// response, or it reconnects forever with an already-consumed id (1008 loop).
|
||||||
|
res.Headers.Add("Cache-Control", "no-store");
|
||||||
|
res.Headers.Add("Pragma", "no-cache");
|
||||||
|
|
||||||
this.LogVerbose("Join Room Request with token: {token}", token);
|
this.LogVerbose("Join Room Request with token: {token}", token);
|
||||||
|
|
||||||
byte[] body;
|
byte[] body;
|
||||||
|
|
@ -1213,8 +1230,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
||||||
|
|
||||||
// Construct WebSocket URL with clientId query parameter
|
// Construct WebSocket URL with clientId query parameter
|
||||||
var wsProtocol = "ws";
|
var wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
||||||
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
|
||||||
|
|
||||||
// Construct the response object
|
// Construct the response object
|
||||||
JoinResponse jRes = new JoinResponse
|
JoinResponse jRes = new JoinResponse
|
||||||
|
|
@ -1226,7 +1242,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
Config = _parent.GetConfigWithPluginVersion(),
|
Config = _parent.GetConfigWithPluginVersion(),
|
||||||
CodeExpires = new DateTime().AddYears(1),
|
CodeExpires = new DateTime().AddYears(1),
|
||||||
UserCode = bridge.UserCode,
|
UserCode = bridge.UserCode,
|
||||||
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
|
UserAppUrl = string.Format("{0}://{1}:{2}/mc/app",
|
||||||
|
HttpScheme,
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port),
|
Port),
|
||||||
WebSocketUrl = wsUrl,
|
WebSocketUrl = wsUrl,
|
||||||
|
|
@ -1251,6 +1268,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.ContentType = "application/json";
|
res.ContentType = "application/json";
|
||||||
|
res.Headers.Add("Cache-Control", "no-store");
|
||||||
|
res.Headers.Add("Pragma", "no-cache");
|
||||||
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
|
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
|
||||||
var message = JsonConvert.SerializeObject(version);
|
var message = JsonConvert.SerializeObject(version);
|
||||||
this.LogVerbose("{message}", message);
|
this.LogVerbose("{message}", message);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.274" />
|
||||||
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue