mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-01-19 23:44:55 +00:00
Compare commits
47 Commits
plugin-min
...
v3.0.0-net
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc900f3f31 | ||
|
|
562f0ba793 | ||
|
|
9c3c924a29 | ||
|
|
9b5af60a46 | ||
|
|
a99b0a1fac | ||
|
|
7591913a9c | ||
|
|
66a6612b65 | ||
|
|
688cf34153 | ||
|
|
0c59237232 | ||
|
|
88eec9a3f1 | ||
|
|
fda4a5a816 | ||
|
|
471d5b701b | ||
|
|
96ac266d24 | ||
|
|
c1809459a6 | ||
|
|
1a9e1087de | ||
|
|
8d55615837 | ||
|
|
19e799f11d | ||
|
|
a3c1c444b7 | ||
|
|
9f70e3c721 | ||
|
|
c9b3205736 | ||
|
|
253b2cddaf | ||
|
|
d96edfa8d0 | ||
|
|
95c1c01396 | ||
|
|
9c94806e4f | ||
|
|
183879f1c4 | ||
|
|
f3159738ce | ||
|
|
2c5cae9f41 | ||
|
|
7178d8e284 | ||
|
|
af98a92f8c | ||
|
|
0a6896910d | ||
|
|
9b1dd099f6 | ||
|
|
3f5269de2f | ||
|
|
60f1adcd35 | ||
|
|
12c8660015 | ||
|
|
ec6aeb17f6 | ||
|
|
e7c3fcbbd9 | ||
|
|
0c7ec82529 | ||
|
|
feb99ecbb6 | ||
|
|
91dc655103 | ||
|
|
bf31fb10eb | ||
|
|
d78b9ea313 | ||
|
|
15172a5509 | ||
|
|
5e21bad596 | ||
|
|
a4de9f2241 | ||
|
|
13cd84b73d | ||
|
|
81a01b7960 | ||
|
|
d9dc70bea2 |
247
.github/workflows/essentials-3-dev-build.yml
vendored
Normal file
247
.github/workflows/essentials-3-dev-build.yml
vendored
Normal file
@@ -0,0 +1,247 @@
|
||||
name: Essentials v3 Development Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- feature-3.0.0/*
|
||||
- hotfix-3.0.0/*
|
||||
- release-3.0.0/*
|
||||
- development-3.0.0
|
||||
|
||||
env:
|
||||
SOLUTION_PATH: .
|
||||
SOLUTION_FILE: PepperDash.Essentials
|
||||
VERSION: 0.0.0-buildtype-buildnumber
|
||||
BUILD_TYPE: Debug
|
||||
RELEASE_BRANCH: main
|
||||
jobs:
|
||||
Build_Project_4-Series:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Detect environment (Act vs GitHub)
|
||||
- name: Detect environment
|
||||
id: detect_env
|
||||
run: |
|
||||
if [ -n "$ACT" ]; then
|
||||
echo "is_local=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_local=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
if [ "${{ steps.detect_env.outputs.is_local }}" == "true" ]; then
|
||||
# For Act - no sudo needed
|
||||
apt-get update
|
||||
apt-get install -y curl wget libicu-dev git unzip
|
||||
else
|
||||
# For GitHub runners - sudo required
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl wget libicu-dev git unzip
|
||||
fi
|
||||
|
||||
- name: Set Version Number
|
||||
id: setVersion
|
||||
shell: bash
|
||||
run: |
|
||||
latestVersion="3.0.0"
|
||||
newVersion=$latestVersion
|
||||
phase=""
|
||||
newVersionString=""
|
||||
|
||||
if [[ $GITHUB_REF =~ ^refs/pull/.* ]]; then
|
||||
phase="beta"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF =~ ^refs/heads/hotfix-3.0.0/.* ]]; then
|
||||
phase="hotfix"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF =~ ^refs/heads/feature-3.0.0/.* ]]; then
|
||||
phase="alpha"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF == "refs/heads/development-3.0.0" ]]; then
|
||||
phase="beta"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF =~ ^refs/heads/release-3.0.0/.* ]]; then
|
||||
version=$(echo $GITHUB_REF | awk -F '/' '{print $NF}' | sed 's/v//')
|
||||
phase="rc"
|
||||
newVersionString="${version}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
else
|
||||
# For local builds or unrecognized branches
|
||||
newVersionString="${newVersion}-local"
|
||||
fi
|
||||
|
||||
echo "version=$newVersionString" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create Build Properties file
|
||||
- name: Create Build Properties
|
||||
run: |
|
||||
cat > Directory.Build.props << EOF
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>${{ steps.setVersion.outputs.version }}</Version>
|
||||
<AssemblyVersion>${{ steps.setVersion.outputs.version }}</AssemblyVersion>
|
||||
<FileVersion>${{ steps.setVersion.outputs.version }}</FileVersion>
|
||||
<InformationalVersion>${{ steps.setVersion.outputs.version }}</InformationalVersion>
|
||||
<PackageVersion>${{ steps.setVersion.outputs.version }}</PackageVersion>
|
||||
<NuGetVersion>${{ steps.setVersion.outputs.version }}</NuGetVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
EOF
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore NuGet Packages
|
||||
run: dotnet restore ${SOLUTION_FILE}.sln
|
||||
|
||||
- name: Build Solution
|
||||
run: dotnet build ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --no-restore
|
||||
|
||||
# Copy the CPZ file to the output directory with version in the filename
|
||||
- name: Copy and Rename CPZ Files
|
||||
run: |
|
||||
mkdir -p ./output/cpz
|
||||
|
||||
# Find the main CPZ file in the build output
|
||||
if [ -f "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" ]; then
|
||||
cp "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" "./output/cpz/PepperDashEssentials.${{ steps.setVersion.outputs.version }}.cpz"
|
||||
echo "Main CPZ file copied and renamed successfully."
|
||||
else
|
||||
echo "Warning: Main CPZ file not found at expected location."
|
||||
find ./src -name "*.cpz" | xargs -I {} cp {} ./output/cpz/
|
||||
fi
|
||||
|
||||
- name: Pack Solution
|
||||
run: dotnet pack ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --output ./output/nuget --no-build
|
||||
|
||||
# List build artifacts (runs in both environments)
|
||||
- name: List Build Artifacts
|
||||
run: |
|
||||
echo "=== Build Artifacts ==="
|
||||
echo "NuGet Packages:"
|
||||
find ./output/nuget -type f | sort
|
||||
echo ""
|
||||
echo "CPZ/CPLZ Files:"
|
||||
find ./output -name "*.cpz" -o -name "*.cplz" | sort
|
||||
echo "======================="
|
||||
|
||||
# Enhanced package inspection for local runs
|
||||
- name: Inspect NuGet Packages
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== NuGet Package Details ==="
|
||||
for pkg in $(find ./output/nuget -name "*.nupkg"); do
|
||||
echo "Package: $(basename "$pkg")"
|
||||
echo "Size: $(du -h "$pkg" | cut -f1)"
|
||||
|
||||
# Extract and show package contents
|
||||
echo "Contents:"
|
||||
unzip -l "$pkg" | tail -n +4 | head -n -2
|
||||
echo "--------------------------"
|
||||
|
||||
# Try to extract and show the nuspec file (contains metadata)
|
||||
echo "Metadata:"
|
||||
unzip -p "$pkg" "*.nuspec" 2>/dev/null | grep -E "(<id>|<version>|<description>|<authors>|<dependencies>)" || echo "Metadata extraction failed"
|
||||
echo "--------------------------"
|
||||
done
|
||||
echo "==========================="
|
||||
|
||||
# Tag creation - GitHub version
|
||||
- name: Create tag for non-rc builds (GitHub)
|
||||
if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'false' }}
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
git tag ${{ steps.setVersion.outputs.version }}
|
||||
git push --tags origin
|
||||
|
||||
# Tag creation - Act mock version
|
||||
- name: Create tag for non-rc builds (Act Mock)
|
||||
if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'true' }}
|
||||
run: |
|
||||
echo "Would create git tag: ${{ steps.setVersion.outputs.version }}"
|
||||
echo "Would push tag to: origin"
|
||||
|
||||
# Release creation - GitHub version
|
||||
- name: Create Release (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: 'output/cpz/*,output/**/*.cplz'
|
||||
generateReleaseNotes: true
|
||||
prerelease: ${{contains('debug', env.BUILD_TYPE)}}
|
||||
tag: ${{ steps.setVersion.outputs.version }}
|
||||
|
||||
# Release creation - Act mock version with enhanced output
|
||||
- name: Create Release (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock Release Creation ==="
|
||||
echo "Would create release with:"
|
||||
echo "- Tag: ${{ steps.setVersion.outputs.version }}"
|
||||
echo "- Prerelease: ${{contains('debug', env.BUILD_TYPE)}}"
|
||||
echo "- Artifacts matching pattern: output/cpz/*,output/**/*.cplz"
|
||||
echo ""
|
||||
echo "Matching artifacts:"
|
||||
find ./output/cpz -type f
|
||||
find ./output -name "*.cplz"
|
||||
|
||||
# Detailed info about release artifacts
|
||||
echo ""
|
||||
echo "Artifact Details:"
|
||||
for artifact in $(find ./output/cpz -type f; find ./output -name "*.cplz"); do
|
||||
echo "File: $(basename "$artifact")"
|
||||
echo "Size: $(du -h "$artifact" | cut -f1)"
|
||||
echo "Created: $(stat -c %y "$artifact")"
|
||||
echo "MD5: $(md5sum "$artifact" | cut -d' ' -f1)"
|
||||
echo "--------------------------"
|
||||
done
|
||||
echo "============================"
|
||||
|
||||
# NuGet setup - GitHub version
|
||||
- name: Setup NuGet (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
run: |
|
||||
dotnet nuget add source https://nuget.pkg.github.com/pepperdash/index.json -n github -u pepperdash -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
|
||||
|
||||
# NuGet setup - Act mock version
|
||||
- name: Setup NuGet (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock NuGet Setup ==="
|
||||
echo "Would add GitHub NuGet source: https://nuget.pkg.github.com/pepperdash/index.json"
|
||||
echo "======================="
|
||||
|
||||
# Publish to NuGet - GitHub version
|
||||
- name: Publish to Nuget (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
run: dotnet nuget push ./output/nuget/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
|
||||
|
||||
# Publish to NuGet - Act mock version
|
||||
- name: Publish to Nuget (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock Publish to NuGet ==="
|
||||
echo "Would publish the following packages to https://api.nuget.org/v3/index.json:"
|
||||
find ./output/nuget -name "*.nupkg" | sort
|
||||
echo "============================="
|
||||
|
||||
# Publish to GitHub NuGet - GitHub version
|
||||
- name: Publish to Github Nuget (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
run: dotnet nuget push ./output/nuget/*.nupkg --source github --api-key ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Publish to GitHub NuGet - Act mock version
|
||||
- name: Publish to Github Nuget (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock Publish to GitHub NuGet ==="
|
||||
echo "Would publish the following packages to the GitHub NuGet registry:"
|
||||
find ./output/nuget -name "*.nupkg" | sort
|
||||
echo "=================================="
|
||||
7
runtimeconfig.json
Normal file
7
runtimeconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"configProperties": {
|
||||
"System.Globalization.Invariant": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,8 @@ namespace PepperDash.Core
|
||||
/// </summary>
|
||||
public interface IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique Key
|
||||
/// <summary>
|
||||
/// Gets the unique key associated with the object.
|
||||
/// </summary>
|
||||
[JsonProperty("key")]
|
||||
string Key { get; }
|
||||
@@ -25,8 +25,8 @@ namespace PepperDash.Core
|
||||
/// </summary>
|
||||
public interface IKeyName : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Isn't it obvious :)
|
||||
/// <summary>
|
||||
/// Gets the name associated with the current object.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
string Name { get; }
|
||||
|
||||
@@ -11,35 +11,35 @@ namespace PepperDash.Core
|
||||
public class Device : IKeyName
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Unique Key
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Unique Key
|
||||
/// </summary>
|
||||
public string Key { get; protected set; }
|
||||
/// <summary>
|
||||
/// Name of the devie
|
||||
/// </summary>
|
||||
public string Name { get; protected set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Name { get; protected set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool Enabled { get; protected set; }
|
||||
|
||||
///// <summary>
|
||||
///// A place to store reference to the original config object, if any. These values should
|
||||
///// NOT be used as properties on the device as they are all publicly-settable values.
|
||||
///// </summary>
|
||||
//public DeviceConfig Config { get; private set; }
|
||||
///// <summary>
|
||||
///// Helper method to check if Config exists
|
||||
///// </summary>
|
||||
//public bool HasConfig { get { return Config != null; } }
|
||||
///// <summary>
|
||||
///// A place to store reference to the original config object, if any. These values should
|
||||
///// NOT be used as properties on the device as they are all publicly-settable values.
|
||||
///// </summary>
|
||||
//public DeviceConfig Config { get; private set; }
|
||||
///// <summary>
|
||||
///// Helper method to check if Config exists
|
||||
///// </summary>
|
||||
//public bool HasConfig { get { return Config != null; } }
|
||||
|
||||
List<Action> _PreActivationActions;
|
||||
List<Action> _PostActivationActions;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public static Device DefaultDevice { get { return _DefaultDevice; } }
|
||||
static Device _DefaultDevice = new Device("Default", "Default");
|
||||
|
||||
@@ -54,27 +54,27 @@ namespace PepperDash.Core
|
||||
Name = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with key and name
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
/// <summary>
|
||||
/// Constructor with key and name
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
public Device(string key, string name) : this(key)
|
||||
{
|
||||
Name = name;
|
||||
|
||||
}
|
||||
|
||||
//public Device(DeviceConfig config)
|
||||
// : this(config.Key, config.Name)
|
||||
//{
|
||||
// Config = config;
|
||||
//}
|
||||
//public Device(DeviceConfig config)
|
||||
// : this(config.Key, config.Name)
|
||||
//{
|
||||
// Config = config;
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pre activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
/// <summary>
|
||||
/// Adds a pre activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
public void AddPreActivationAction(Action act)
|
||||
{
|
||||
if (_PreActivationActions == null)
|
||||
@@ -82,10 +82,10 @@ namespace PepperDash.Core
|
||||
_PreActivationActions.Add(act);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a post activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
/// <summary>
|
||||
/// Adds a post activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
public void AddPostActivationAction(Action act)
|
||||
{
|
||||
if (_PostActivationActions == null)
|
||||
@@ -93,55 +93,58 @@ namespace PepperDash.Core
|
||||
_PostActivationActions.Add(act);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the preactivation actions
|
||||
/// </summary>
|
||||
public void PreActivate()
|
||||
{
|
||||
if (_PreActivationActions != null)
|
||||
_PreActivationActions.ForEach(a => {
|
||||
/// <summary>
|
||||
/// Executes the preactivation actions
|
||||
/// </summary>
|
||||
public void PreActivate()
|
||||
{
|
||||
if (_PreActivationActions != null)
|
||||
_PreActivationActions.ForEach(a =>
|
||||
{
|
||||
try
|
||||
{
|
||||
a.Invoke();
|
||||
} catch (Exception e)
|
||||
{
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets this device ready to be used in the system. Runs any added pre-activation items, and
|
||||
/// all post-activation at end. Classes needing additional logic to
|
||||
/// run should override CustomActivate()
|
||||
/// </summary>
|
||||
public bool Activate()
|
||||
public bool Activate()
|
||||
{
|
||||
//if (_PreActivationActions != null)
|
||||
// _PreActivationActions.ForEach(a => a.Invoke());
|
||||
//if (_PreActivationActions != null)
|
||||
// _PreActivationActions.ForEach(a => a.Invoke());
|
||||
var result = CustomActivate();
|
||||
//if(result && _PostActivationActions != null)
|
||||
// _PostActivationActions.ForEach(a => a.Invoke());
|
||||
return result;
|
||||
//if(result && _PostActivationActions != null)
|
||||
// _PostActivationActions.ForEach(a => a.Invoke());
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the postactivation actions
|
||||
/// </summary>
|
||||
public void PostActivate()
|
||||
{
|
||||
if (_PostActivationActions != null)
|
||||
_PostActivationActions.ForEach(a => {
|
||||
try
|
||||
{
|
||||
a.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
/// <summary>
|
||||
/// Executes the postactivation actions
|
||||
/// </summary>
|
||||
public void PostActivate()
|
||||
{
|
||||
if (_PostActivationActions != null)
|
||||
_PostActivationActions.ForEach(a =>
|
||||
{
|
||||
try
|
||||
{
|
||||
a.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called in between Pre and PostActivationActions when Activate() is called.
|
||||
@@ -158,14 +161,14 @@ namespace PepperDash.Core
|
||||
/// <returns></returns>
|
||||
public virtual bool Deactivate() { return true; }
|
||||
|
||||
/// <summary>
|
||||
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
|
||||
/// </summary>
|
||||
public virtual void Initialize()
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
|
||||
/// </summary>
|
||||
public virtual void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Helper method to check object for bool value false and fire an Action method
|
||||
/// </summary>
|
||||
/// <param name="o">Should be of type bool, others will be ignored</param>
|
||||
@@ -175,5 +178,15 @@ namespace PepperDash.Core
|
||||
if (o is bool && !(bool)o) a();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the object, including its key and name.
|
||||
/// </summary>
|
||||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
||||
/// null or empty, "---" is used in place of the name.</remarks>
|
||||
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Crestron.SimplSharp;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Serilog;
|
||||
using Serilog.Configuration;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Configuration;
|
||||
using WebSocketSharp.Server;
|
||||
using Crestron.SimplSharp;
|
||||
using WebSocketSharp;
|
||||
using System.Security.Authentication;
|
||||
using WebSocketSharp.Net;
|
||||
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Serilog.Formatting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog.Formatting.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Authentication;
|
||||
using WebSocketSharp;
|
||||
using WebSocketSharp.Server;
|
||||
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
public class DebugWebsocketSink : ILogEventSink
|
||||
/// <summary>
|
||||
/// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected
|
||||
/// WebSocket clients.
|
||||
/// </summary>
|
||||
/// <remarks>This class implements the <see cref="ILogEventSink"/> interface and is designed to send
|
||||
/// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally
|
||||
/// and uses a self-signed certificate for SSL/TLS encryption.</remarks>
|
||||
public class DebugWebsocketSink : ILogEventSink, IKeyed
|
||||
{
|
||||
private HttpServer _httpsServer;
|
||||
|
||||
@@ -29,6 +30,9 @@ namespace PepperDash.Core
|
||||
private const string _certificateName = "selfCres";
|
||||
private const string _certificatePassword = "cres12345";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the port number on which the HTTPS server is currently running.
|
||||
/// </summary>
|
||||
public int Port
|
||||
{ get
|
||||
{
|
||||
@@ -38,6 +42,11 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the WebSocket URL for the current server instance.
|
||||
/// </summary>
|
||||
/// <remarks>The URL is dynamically constructed based on the server's current IP address, port,
|
||||
/// and WebSocket path.</remarks>
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
@@ -47,18 +56,31 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the HTTPS server is currently listening for incoming connections.
|
||||
/// </summary>
|
||||
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "DebugWebsocketSink";
|
||||
|
||||
private readonly ITextFormatter _textFormatter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugWebsocketSink"/> class with the specified text formatter.
|
||||
/// </summary>
|
||||
/// <remarks>This constructor initializes the WebSocket sink and ensures that a certificate is
|
||||
/// available for secure communication. If the required certificate does not exist, it will be created
|
||||
/// automatically. Additionally, the sink is configured to stop the server when the program is
|
||||
/// stopping.</remarks>
|
||||
/// <param name="formatProvider">The text formatter used to format log messages. If null, a default JSON formatter is used.</param>
|
||||
public DebugWebsocketSink(ITextFormatter formatProvider)
|
||||
{
|
||||
|
||||
_textFormatter = formatProvider ?? new JsonFormatter();
|
||||
|
||||
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
|
||||
CreateCert(null);
|
||||
CreateCert();
|
||||
|
||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||
{
|
||||
@@ -69,7 +91,7 @@ namespace PepperDash.Core
|
||||
};
|
||||
}
|
||||
|
||||
private void CreateCert(string[] args)
|
||||
private static void CreateCert()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -105,6 +127,13 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a log event to all connected WebSocket clients.
|
||||
/// </summary>
|
||||
/// <remarks>The log event is formatted using the configured text formatter and then broadcasted
|
||||
/// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not
|
||||
/// listening, the method exits without performing any action.</remarks>
|
||||
/// <param name="logEvent">The log event to be formatted and broadcasted. Cannot be null.</param>
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
if (_httpsServer == null || !_httpsServer.IsListening) return;
|
||||
@@ -112,10 +141,16 @@ namespace PepperDash.Core
|
||||
var sw = new StringWriter();
|
||||
_textFormatter.Format(logEvent, sw);
|
||||
|
||||
_httpsServer.WebSocketServices.Broadcast(sw.ToString());
|
||||
|
||||
_httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the WebSocket server on the specified port and configures it with the appropriate certificate.
|
||||
/// </summary>
|
||||
/// <remarks>This method initializes the WebSocket server and binds it to the specified port. It
|
||||
/// also applies the server's certificate for secure communication. Ensure that the port is not already in use
|
||||
/// and that the certificate file is accessible.</remarks>
|
||||
/// <param name="port">The port number on which the WebSocket server will listen. Must be a valid, non-negative port number.</param>
|
||||
public void StartServerAndSetPort(int port)
|
||||
{
|
||||
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
|
||||
@@ -128,24 +163,22 @@ namespace PepperDash.Core
|
||||
{
|
||||
try
|
||||
{
|
||||
_httpsServer = new HttpServer(port, true);
|
||||
|
||||
_httpsServer = new HttpServer(port, true);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(certPath))
|
||||
{
|
||||
Debug.Console(0, "Assigning SSL Configuration");
|
||||
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
|
||||
{
|
||||
ClientCertificateRequired = false,
|
||||
CheckCertificateRevocation = false,
|
||||
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
|
||||
//this is just to test, you might want to actually validate
|
||||
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
|
||||
_httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword);
|
||||
_httpsServer.SslConfiguration.ClientCertificateRequired = false;
|
||||
_httpsServer.SslConfiguration.CheckCertificateRevocation = false;
|
||||
_httpsServer.SslConfiguration.EnabledSslProtocols = SslProtocols.Tls12;
|
||||
//this is just to test, you might want to actually validate
|
||||
_httpsServer.SslConfiguration.ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
|
||||
{
|
||||
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered");
|
||||
return true;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
Debug.Console(0, "Adding Debug Client Service");
|
||||
_httpsServer.AddWebSocketService<DebugClient>(_path);
|
||||
@@ -193,6 +226,11 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the WebSocket server if it is currently running.
|
||||
/// </summary>
|
||||
/// <remarks>This method halts the WebSocket server and releases any associated resources. After
|
||||
/// calling this method, the server will no longer accept or process incoming connections.</remarks>
|
||||
public void StopServer()
|
||||
{
|
||||
Debug.Console(0, "Stopping Websocket Server");
|
||||
@@ -202,8 +240,21 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the logger to write log events to a debug WebSocket sink.
|
||||
/// </summary>
|
||||
/// <remarks>This extension method allows you to direct log events to a WebSocket sink for debugging
|
||||
/// purposes.</remarks>
|
||||
public static class DebugWebsocketSinkExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures a logger to write log events to a debug WebSocket sink.
|
||||
/// </summary>
|
||||
/// <remarks>This method adds a sink that writes log events to a WebSocket for debugging purposes.
|
||||
/// It is typically used during development to stream log events in real-time.</remarks>
|
||||
/// <param name="loggerConfiguration">The logger sink configuration to apply the WebSocket sink to.</param>
|
||||
/// <param name="formatProvider">An optional text formatter to format the log events. If not provided, a default formatter will be used.</param>
|
||||
/// <returns>A <see cref="LoggerConfiguration"/> object that can be used to further configure the logger.</returns>
|
||||
public static LoggerConfiguration DebugWebsocketSink(
|
||||
this LoggerSinkConfiguration loggerConfiguration,
|
||||
ITextFormatter formatProvider = null)
|
||||
@@ -212,10 +263,20 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message
|
||||
/// handling functionality.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="DebugClient"/> class extends <see cref="WebSocketBehavior"/> to handle
|
||||
/// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the
|
||||
/// duration of the connection and logs relevant events for debugging.</remarks>
|
||||
public class DebugClient : WebSocketBehavior
|
||||
{
|
||||
private DateTime _connectionTime;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration of time the WebSocket connection has been active.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectedDuration
|
||||
{
|
||||
get
|
||||
@@ -231,11 +292,17 @@ namespace PepperDash.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugClient"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>This constructor creates a new <see cref="DebugClient"/> instance and logs its
|
||||
/// creation using the <see cref="Debug.Console(int, string)"/> method with a debug level of 0.</remarks>
|
||||
public DebugClient()
|
||||
{
|
||||
Debug.Console(0, "DebugClient Created");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnOpen()
|
||||
{
|
||||
base.OnOpen();
|
||||
@@ -246,6 +313,7 @@ namespace PepperDash.Core
|
||||
_connectionTime = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMessage(MessageEventArgs e)
|
||||
{
|
||||
base.OnMessage(e);
|
||||
@@ -253,6 +321,7 @@ namespace PepperDash.Core
|
||||
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnClose(CloseEventArgs e)
|
||||
{
|
||||
base.OnClose(e);
|
||||
@@ -261,6 +330,7 @@ namespace PepperDash.Core
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
|
||||
{
|
||||
base.OnError(e);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PepperDash.Core</RootNamespace>
|
||||
<AssemblyName>PepperDashCore</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<Deterministic>true</Deterministic>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
@@ -42,15 +42,16 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||
<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" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2024.2.0" />
|
||||
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2025.0.0" />
|
||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6'">
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// This defines a device that has screens with layouts
|
||||
/// Simply decorative
|
||||
/// </summary>
|
||||
public interface IHasScreensWithLayouts
|
||||
{
|
||||
/// <summary>
|
||||
/// A dictionary of screens, keyed by screen ID, that contains information about each screen and its layouts.
|
||||
/// </summary>
|
||||
Dictionary<uint, ScreenInfo> Screens { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Applies a specific layout to a screen based on the provided screen ID and layout index.
|
||||
/// </summary>
|
||||
/// <param name="screenId"></param>
|
||||
/// <param name="layoutIndex"></param>
|
||||
void ApplyLayout(uint screenId, uint layoutIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a screen and its layouts.
|
||||
/// </summary>
|
||||
public class ScreenInfo
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the screen is enabled or not.
|
||||
/// </summary>
|
||||
[JsonProperty("enabled")]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the screen.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the screen.
|
||||
/// </summary>
|
||||
[JsonProperty("screenIndex")]
|
||||
public int ScreenIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary of layout information for the screen, keyed by layout ID.
|
||||
/// </summary>
|
||||
[JsonProperty("layouts")]
|
||||
public Dictionary<uint, LayoutInfo> Layouts { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a layout on a screen.
|
||||
/// </summary>
|
||||
public class LayoutInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the layout.
|
||||
/// </summary>
|
||||
[JsonProperty("layoutName")]
|
||||
public string LayoutName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of the layout.
|
||||
/// </summary>
|
||||
[JsonProperty("layoutIndex")]
|
||||
public int LayoutIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of the layout, which can be "single", "double", "triple", or "quad".
|
||||
/// </summary>
|
||||
[JsonProperty("layoutType")]
|
||||
public string LayoutType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary of window configurations for the layout, keyed by window ID.
|
||||
/// </summary>
|
||||
[JsonProperty("windows")]
|
||||
public Dictionary<uint, WindowConfig> Windows { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration of a window within a layout on a screen.
|
||||
/// </summary>
|
||||
public class WindowConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The display label for the window
|
||||
/// </summary>
|
||||
[JsonProperty("label")]
|
||||
public string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The input for the window
|
||||
/// </summary>
|
||||
[JsonProperty("input")]
|
||||
public string Input { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -248,7 +248,7 @@ namespace PepperDash.Essentials.Core
|
||||
|
||||
foreach (var dev in Devices.Values.OfType<ICommunicationMonitor>())
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}{Environment.NewLine}");
|
||||
CrestronConsole.ConsoleCommandResponse($"{dev}: {dev.CommunicationMonitor.Status}\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,19 +20,20 @@ namespace PepperDash.Essentials.Core
|
||||
public event EventHandler Initialized;
|
||||
|
||||
private bool _isInitialized;
|
||||
public bool IsInitialized {
|
||||
public bool IsInitialized
|
||||
{
|
||||
get { return _isInitialized; }
|
||||
private set
|
||||
{
|
||||
private set
|
||||
{
|
||||
if (_isInitialized == value) return;
|
||||
|
||||
|
||||
_isInitialized = value;
|
||||
|
||||
if (_isInitialized)
|
||||
{
|
||||
Initialized?.Invoke(this, new EventArgs());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected EssentialsDevice(string key)
|
||||
@@ -80,8 +81,9 @@ namespace PepperDash.Essentials.Core
|
||||
/// <summary>
|
||||
/// Override this method to build and create custom Mobile Control Messengers during the Activation phase
|
||||
/// </summary>
|
||||
protected virtual void CreateMobileControlMessengers() {
|
||||
|
||||
protected virtual void CreateMobileControlMessengers()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions for IPAddress to provide additional functionality such as getting broadcast address, network address, and checking if two addresses are in the same subnet.
|
||||
/// </summary>
|
||||
public static class IPAddressExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the broadcast address for a given IP address and subnet mask.
|
||||
/// </summary>
|
||||
/// <param name="address">Address to check</param>
|
||||
/// <param name="subnetMask">Subnet mask in a.b.c.d format</param>
|
||||
/// <returns>Broadcast address</returns>
|
||||
/// <remarks>
|
||||
/// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the broadcast address will be 192.168.1.255
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
byte[] ipAdressBytes = address.GetAddressBytes();
|
||||
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
||||
|
||||
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
||||
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
||||
|
||||
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
||||
for (int i = 0; i < broadcastAddress.Length; i++)
|
||||
{
|
||||
broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
|
||||
}
|
||||
return new IPAddress(broadcastAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the network address for a given IP address and subnet mask.
|
||||
/// </summary>
|
||||
/// <param name="address">Address to check</param>
|
||||
/// <param name="subnetMask">Subnet mask in a.b.c.d</param>
|
||||
/// <returns>Network Address</returns>
|
||||
/// /// <remarks>
|
||||
/// If the input IP address is 192.168.1.100 and the subnet mask is 255.255.255.0, the network address will be 192.168.1.0
|
||||
/// </remarks>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
byte[] ipAdressBytes = address.GetAddressBytes();
|
||||
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
||||
|
||||
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
||||
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
||||
|
||||
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
||||
for (int i = 0; i < broadcastAddress.Length; i++)
|
||||
{
|
||||
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
|
||||
}
|
||||
return new IPAddress(broadcastAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if two IP addresses are in the same subnet.
|
||||
/// </summary>
|
||||
/// <param name="address2">Address to check</param>
|
||||
/// <param name="address">Second address to check</param>
|
||||
/// <param name="subnetMask">Subnet mask to use to compare the 2 IP Address</param>
|
||||
/// <returns>True if addresses are in the same subnet</returns>
|
||||
/// <remarks>
|
||||
/// If the input IP addresses are 192.168.1.100 and 192.168.1.200, and the subnet mask is 255.255.255.0, this will return true.
|
||||
/// If the input IP addresses are 10.1.1.100 and 192.168.1.100, and the subnet mask is 255.255.255.0, this will return false.
|
||||
/// </remarks>
|
||||
public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
IPAddress network1 = address.GetNetworkAddress(subnetMask);
|
||||
IPAddress network2 = address2.GetNetworkAddress(subnetMask);
|
||||
|
||||
return network1.Equals(network2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Globalization;
|
||||
@@ -35,7 +33,8 @@ namespace PepperDash.Essentials.Core
|
||||
public static eCrestronSeries ProcessorSeries { get { return CrestronEnvironment.ProgramCompatibility; } }
|
||||
|
||||
// TODO: consider making this configurable later
|
||||
public static IFormatProvider Culture = CultureInfo.CreateSpecificCulture("en-US");
|
||||
public static IFormatProvider Culture = CultureInfo.InvariantCulture;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// True when the processor type is a DMPS variant
|
||||
@@ -279,6 +278,18 @@ namespace PepperDash.Essentials.Core
|
||||
CrestronConsole.PrintLine("Error starting CrestronDataStoreStatic: {0}", err);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture("en");
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture("en");
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
// If specific culture fails, fall back to invariant
|
||||
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.Reflection;
|
||||
using Crestron.SimplSharp.Scheduler;
|
||||
|
||||
using PepperDash.Core;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Configurations>Debug;Release;Debug 4.7.2</Configurations>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
@@ -24,7 +24,8 @@
|
||||
<DebugType>pdbonly</DebugType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
// using Crestron.SimplSharp.CrestronIO;
|
||||
using System.Reflection;
|
||||
using System.IO;
|
||||
using System.Reflection.PortableExecutable;
|
||||
using System.Reflection.Metadata;
|
||||
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core;
|
||||
@@ -28,6 +31,11 @@ namespace PepperDash.Essentials
|
||||
/// </summary>
|
||||
static List<LoadedAssembly> LoadedPluginFolderAssemblies;
|
||||
|
||||
/// <summary>
|
||||
/// List of plugins that were found to be incompatible with .NET 8
|
||||
/// </summary>
|
||||
public static List<IncompatiblePlugin> IncompatiblePlugins { get; private set; }
|
||||
|
||||
public static LoadedAssembly EssentialsAssembly { get; private set; }
|
||||
|
||||
public static LoadedAssembly PepperDashCoreAssembly { get; private set; }
|
||||
@@ -47,12 +55,28 @@ namespace PepperDash.Essentials
|
||||
// The temp directory where .cplz archives will be unzipped to
|
||||
static string _tempDirectory => _pluginDirectory + Global.DirectorySeparator + "temp";
|
||||
|
||||
// Known incompatible types in .NET 8
|
||||
private static readonly HashSet<string> KnownIncompatibleTypes = new HashSet<string>
|
||||
{
|
||||
"System.Net.ICertificatePolicy",
|
||||
"System.Security.Cryptography.SHA1CryptoServiceProvider",
|
||||
"System.Web.HttpUtility",
|
||||
"System.Configuration.ConfigurationManager",
|
||||
"System.Web.Services.Protocols.SoapHttpClientProtocol",
|
||||
"System.Runtime.Remoting",
|
||||
"System.EnterpriseServices",
|
||||
"System.Runtime.Serialization.Formatters.Binary.BinaryFormatter",
|
||||
"System.Security.SecurityManager",
|
||||
"System.Security.Permissions.FileIOPermission",
|
||||
"System.AppDomain.CreateDomain"
|
||||
};
|
||||
|
||||
static PluginLoader()
|
||||
{
|
||||
LoadedAssemblies = new List<LoadedAssembly>();
|
||||
LoadedPluginFolderAssemblies = new List<LoadedAssembly>();
|
||||
EssentialsPluginAssemblies = new List<LoadedAssembly>();
|
||||
IncompatiblePlugins = new List<IncompatiblePlugin>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,7 +86,7 @@ namespace PepperDash.Essentials
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Getting Assemblies loaded with Essentials");
|
||||
// Get the loaded assembly filenames
|
||||
var appDi = new DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
||||
var appDi = new SystemIO.DirectoryInfo(Global.ApplicationDirectoryPathPrefix);
|
||||
var assemblyFiles = appDi.GetFiles("*.dll");
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} Assemblies", assemblyFiles.Length);
|
||||
@@ -114,7 +138,6 @@ namespace PepperDash.Essentials
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void SetEssentialsAssembly(string name, Assembly assembly)
|
||||
{
|
||||
var loadedAssembly = LoadedAssemblies.FirstOrDefault(la => la.Name.Equals(name));
|
||||
@@ -126,19 +149,108 @@ namespace PepperDash.Essentials
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
|
||||
/// Checks if a plugin is compatible with .NET 8 by examining its metadata
|
||||
/// </summary>
|
||||
/// <param name="fileName"></param>
|
||||
static LoadedAssembly LoadAssembly(string filePath)
|
||||
/// <param name="filePath">Path to the plugin assembly</param>
|
||||
/// <returns>Tuple with compatibility result, reason if incompatible, and referenced assemblies</returns>
|
||||
public static (bool IsCompatible, string Reason, List<string> References) IsPluginCompatibleWithNet8(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load {0}", filePath);
|
||||
List<string> referencedAssemblies = new List<string>();
|
||||
|
||||
using (SystemIO.FileStream fs = new SystemIO.FileStream(filePath, SystemIO.FileMode.Open,
|
||||
SystemIO.FileAccess.Read, SystemIO.FileShare.ReadWrite))
|
||||
using (PEReader peReader = new PEReader(fs))
|
||||
{
|
||||
if (!peReader.HasMetadata)
|
||||
return (false, "Not a valid .NET assembly", referencedAssemblies);
|
||||
|
||||
MetadataReader metadataReader = peReader.GetMetadataReader();
|
||||
|
||||
// Collect assembly references
|
||||
foreach (var assemblyRefHandle in metadataReader.AssemblyReferences)
|
||||
{
|
||||
var assemblyRef = metadataReader.GetAssemblyReference(assemblyRefHandle);
|
||||
string assemblyName = metadataReader.GetString(assemblyRef.Name);
|
||||
referencedAssemblies.Add(assemblyName);
|
||||
}
|
||||
|
||||
// Check for references to known incompatible types
|
||||
foreach (var typeRefHandle in metadataReader.TypeReferences)
|
||||
{
|
||||
var typeRef = metadataReader.GetTypeReference(typeRefHandle);
|
||||
string typeNamespace = metadataReader.GetString(typeRef.Namespace);
|
||||
string typeName = metadataReader.GetString(typeRef.Name);
|
||||
|
||||
string fullTypeName = $"{typeNamespace}.{typeName}";
|
||||
if (KnownIncompatibleTypes.Contains(fullTypeName))
|
||||
{
|
||||
return (false, $"Uses incompatible type: {fullTypeName}", referencedAssemblies);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for explicit .NET 8 compatibility attribute
|
||||
bool hasNet8Attribute = false;
|
||||
foreach (var customAttributeHandle in metadataReader.GetAssemblyDefinition().GetCustomAttributes())
|
||||
{
|
||||
var customAttribute = metadataReader.GetCustomAttribute(customAttributeHandle);
|
||||
var ctorHandle = customAttribute.Constructor;
|
||||
|
||||
if (ctorHandle.Kind == HandleKind.MemberReference)
|
||||
{
|
||||
var memberRef = metadataReader.GetMemberReference((MemberReferenceHandle)ctorHandle);
|
||||
var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)memberRef.Parent);
|
||||
|
||||
string typeName = metadataReader.GetString(typeRef.Name);
|
||||
if (typeName == "Net8CompatibleAttribute" || typeName == "TargetFrameworkAttribute")
|
||||
{
|
||||
hasNet8Attribute = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNet8Attribute)
|
||||
{
|
||||
return (true, null, referencedAssemblies);
|
||||
}
|
||||
|
||||
// If we can't determine incompatibility, assume it's compatible
|
||||
return (true, null, referencedAssemblies);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, $"Error analyzing assembly: {ex.Message}", new List<string>());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads an assembly via Reflection and adds it to the list of loaded assemblies
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the assembly file</param>
|
||||
/// <param name="requestedBy">Name of the plugin requesting this assembly (null for direct loads)</param>
|
||||
static LoadedAssembly LoadAssembly(string filePath, string requestedBy = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check .NET 8 compatibility before loading
|
||||
var (isCompatible, reason, references) = IsPluginCompatibleWithNet8(filePath);
|
||||
if (!isCompatible)
|
||||
{
|
||||
string fileName = CrestronIO.Path.GetFileName(filePath);
|
||||
Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason);
|
||||
|
||||
var incompatiblePlugin = new IncompatiblePlugin(fileName, reason, requestedBy);
|
||||
IncompatiblePlugins.Add(incompatiblePlugin);
|
||||
return null;
|
||||
}
|
||||
|
||||
var assembly = Assembly.LoadFrom(filePath);
|
||||
if (assembly != null)
|
||||
{
|
||||
var assyVersion = GetAssemblyVersion(assembly);
|
||||
|
||||
var loadedAssembly = new LoadedAssembly(assembly.GetName().Name, assyVersion, assembly);
|
||||
LoadedAssemblies.Add(loadedAssembly);
|
||||
Debug.LogMessage(LogEventLevel.Information, "Loaded assembly '{0}', version {1}", loadedAssembly.Name, loadedAssembly.Version);
|
||||
@@ -148,14 +260,47 @@ namespace PepperDash.Essentials
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Unable to load assembly: '{0}'", filePath);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch(Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
catch(SystemIO.FileLoadException ex) when (ex.Message.Contains("Assembly with same name is already loaded"))
|
||||
{
|
||||
// Get the assembly name from the file path
|
||||
string assemblyName = CrestronIO.Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
// Try to find the already loaded assembly
|
||||
var existingAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name.Equals(assemblyName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingAssembly != null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Assembly '{0}' is already loaded, using existing instance", assemblyName);
|
||||
var assyVersion = GetAssemblyVersion(existingAssembly);
|
||||
var loadedAssembly = new LoadedAssembly(existingAssembly.GetName().Name, assyVersion, existingAssembly);
|
||||
LoadedAssemblies.Add(loadedAssembly);
|
||||
return loadedAssembly;
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Warning, "Assembly with same name already loaded but couldn't find it: {0}", filePath);
|
||||
return null;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
string fileName = CrestronIO.Path.GetFileName(filePath);
|
||||
|
||||
// Check if this might be a .NET Framework compatibility issue
|
||||
if (ex.Message.Contains("Could not load type") ||
|
||||
ex.Message.Contains("Unable to load one or more of the requested types"))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Error, "Error loading assembly {0}: Likely .NET 8 compatibility issue: {1}",
|
||||
fileName, ex.Message);
|
||||
IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, ex.Message, requestedBy));
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(ex, "Error loading assembly from {path}", null, filePath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -217,12 +362,25 @@ namespace PepperDash.Essentials
|
||||
CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
|
||||
}
|
||||
|
||||
//CrestronConsole.ConsoleCommandResponse("Loaded Assemblies:" + CrestronEnvironment.NewLine);
|
||||
//foreach (var assembly in LoadedAssemblies)
|
||||
//{
|
||||
// CrestronConsole.ConsoleCommandResponse("{0} Version: {1}" + CrestronEnvironment.NewLine, assembly.Name, assembly.Version);
|
||||
//}
|
||||
if (IncompatiblePlugins.Count > 0)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Incompatible Plugins:" + CrestronEnvironment.NewLine);
|
||||
foreach (var plugin in IncompatiblePlugins)
|
||||
{
|
||||
if (plugin.TriggeredBy != "Direct load")
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("{0}: {1} (Required by: {2})" + CrestronEnvironment.NewLine,
|
||||
plugin.Name, plugin.Reason, plugin.TriggeredBy);
|
||||
}
|
||||
else
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("{0}: {1}" + CrestronEnvironment.NewLine,
|
||||
plugin.Name, plugin.Reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves any .dll assemblies not already loaded from the plugins folder to loadedPlugins folder
|
||||
/// </summary>
|
||||
@@ -230,14 +388,14 @@ namespace PepperDash.Essentials
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Looking for .dll assemblies from plugins folder...");
|
||||
|
||||
var pluginDi = new DirectoryInfo(_pluginDirectory);
|
||||
var pluginDi = new SystemIO.DirectoryInfo(_pluginDirectory);
|
||||
var pluginFiles = pluginDi.GetFiles("*.dll");
|
||||
|
||||
if (pluginFiles.Length > 0)
|
||||
{
|
||||
if (!Directory.Exists(_loadedPluginsDirectoryPath))
|
||||
if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
||||
SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,14 +412,14 @@ namespace PepperDash.Essentials
|
||||
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + pluginFile.Name;
|
||||
|
||||
// Check if there is a previous file in the loadedPlugins directory and delete
|
||||
if (File.Exists(filePath))
|
||||
if (SystemIO.File.Exists(filePath))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
|
||||
File.Delete(filePath);
|
||||
SystemIO.File.Delete(filePath);
|
||||
}
|
||||
|
||||
// Move the file
|
||||
File.Move(pluginFile.FullName, filePath);
|
||||
SystemIO.File.Move(pluginFile.FullName, filePath);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", pluginFile.FullName, filePath);
|
||||
}
|
||||
else
|
||||
@@ -284,9 +442,9 @@ namespace PepperDash.Essentials
|
||||
/// </summary>
|
||||
static void UnzipAndMoveCplzArchives()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from user folder...");
|
||||
//var di = new DirectoryInfo(_pluginDirectory);
|
||||
//var zFiles = di.GetFiles("*.cplz");
|
||||
Debug.LogMessage(LogEventLevel.Information, "Looking for .cplz archives from plugins folder...");
|
||||
var di = new SystemIO.DirectoryInfo(_pluginDirectory);
|
||||
var zFiles = di.GetFiles("*.cplz");
|
||||
|
||||
//// Find cplz files at the root of the user folder. Makes development/testing easier for VC-4, and helps with mistakes by end users
|
||||
|
||||
@@ -300,16 +458,16 @@ namespace PepperDash.Essentials
|
||||
|
||||
if (cplzFiles.Length > 0)
|
||||
{
|
||||
if (!Directory.Exists(_loadedPluginsDirectoryPath))
|
||||
if (!SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
|
||||
{
|
||||
Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
||||
SystemIO.Directory.CreateDirectory(_loadedPluginsDirectoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var zfi in cplzFiles)
|
||||
{
|
||||
Directory.CreateDirectory(_tempDirectory);
|
||||
var tempDi = new DirectoryInfo(_tempDirectory);
|
||||
SystemIO.Directory.CreateDirectory(_tempDirectory);
|
||||
var tempDi = new SystemIO.DirectoryInfo(_tempDirectory);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Found cplz: {0}. Unzipping into temp plugins directory", zfi.FullName);
|
||||
var result = CrestronZIP.Unzip(zfi.FullName, tempDi.FullName);
|
||||
@@ -327,14 +485,14 @@ namespace PepperDash.Essentials
|
||||
filePath = _loadedPluginsDirectoryPath + Global.DirectorySeparator + tempFile.Name;
|
||||
|
||||
// Check if there is a previous file in the loadedPlugins directory and delete
|
||||
if (File.Exists(filePath))
|
||||
if (SystemIO.File.Exists(filePath))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Found existing file in loadedPlugins: {0} Deleting and moving new file to replace it", filePath);
|
||||
File.Delete(filePath);
|
||||
SystemIO.File.Delete(filePath);
|
||||
}
|
||||
|
||||
// Move the file
|
||||
File.Move(tempFile.FullName, filePath);
|
||||
SystemIO.File.Move(tempFile.FullName, filePath);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Moved {0} to {1}", tempFile.FullName, filePath);
|
||||
}
|
||||
else
|
||||
@@ -350,7 +508,7 @@ namespace PepperDash.Essentials
|
||||
}
|
||||
|
||||
// Delete the .cplz and the temp directory
|
||||
Directory.Delete(_tempDirectory, true);
|
||||
SystemIO.Directory.Delete(_tempDirectory, true);
|
||||
zfi.Delete();
|
||||
}
|
||||
|
||||
@@ -363,16 +521,40 @@ namespace PepperDash.Essentials
|
||||
static void LoadPluginAssemblies()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Loading assemblies from loadedPlugins folder...");
|
||||
var pluginDi = new DirectoryInfo(_loadedPluginsDirectoryPath);
|
||||
var pluginDi = new CrestronIO.DirectoryInfo(_loadedPluginsDirectoryPath);
|
||||
var pluginFiles = pluginDi.GetFiles("*.dll");
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Found {0} plugin assemblies to load", pluginFiles.Length);
|
||||
|
||||
// First, check compatibility of all assemblies before loading any
|
||||
var assemblyCompatibility = new Dictionary<string, (bool IsCompatible, string Reason, List<string> References)>();
|
||||
|
||||
foreach (var pluginFile in pluginFiles)
|
||||
{
|
||||
var loadedAssembly = LoadAssembly(pluginFile.FullName);
|
||||
|
||||
LoadedPluginFolderAssemblies.Add(loadedAssembly);
|
||||
string fileName = pluginFile.Name;
|
||||
assemblyCompatibility[fileName] = IsPluginCompatibleWithNet8(pluginFile.FullName);
|
||||
}
|
||||
|
||||
// Now load compatible assemblies and track incompatible ones
|
||||
foreach (var pluginFile in pluginFiles)
|
||||
{
|
||||
string fileName = pluginFile.Name;
|
||||
var (isCompatible, reason, references) = assemblyCompatibility[fileName];
|
||||
|
||||
if (!isCompatible)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Warning, "Assembly '{0}' is not compatible with .NET 8: {1}", fileName, reason);
|
||||
IncompatiblePlugins.Add(new IncompatiblePlugin(fileName, reason, null));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to load the assembly
|
||||
var loadedAssembly = LoadAssembly(pluginFile.FullName, null);
|
||||
|
||||
if (loadedAssembly != null)
|
||||
{
|
||||
LoadedPluginFolderAssemblies.Add(loadedAssembly);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "All Plugins Loaded.");
|
||||
@@ -384,8 +566,13 @@ namespace PepperDash.Essentials
|
||||
static void LoadCustomPluginTypes()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Loading Custom Plugin Types...");
|
||||
|
||||
foreach (var loadedAssembly in LoadedPluginFolderAssemblies)
|
||||
{
|
||||
// Skip if assembly is null (can happen if we had loading issues)
|
||||
if (loadedAssembly == null || loadedAssembly.Assembly == null)
|
||||
continue;
|
||||
|
||||
// iteratate this assembly's classes, looking for "LoadPlugin()" methods
|
||||
try
|
||||
{
|
||||
@@ -396,11 +583,49 @@ namespace PepperDash.Essentials
|
||||
types = assy.GetTypes();
|
||||
Debug.LogMessage(LogEventLevel.Debug, $"Got types for assembly {assy.GetName().Name}");
|
||||
}
|
||||
catch (ReflectionTypeLoadException e)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
|
||||
loadedAssembly.Name, e.Message);
|
||||
|
||||
// Check if any of the loader exceptions are due to missing assemblies
|
||||
foreach (var loaderEx in e.LoaderExceptions)
|
||||
{
|
||||
if (loaderEx is SystemIO.FileNotFoundException fileNotFoundEx)
|
||||
{
|
||||
string missingAssembly = fileNotFoundEx.FileName;
|
||||
if (!string.IsNullOrEmpty(missingAssembly))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Warning, "Assembly {0} requires missing dependency: {1}",
|
||||
loadedAssembly.Name, missingAssembly);
|
||||
|
||||
// Add to incompatible plugins with dependency information
|
||||
IncompatiblePlugins.Add(new IncompatiblePlugin(
|
||||
CrestronIO.Path.GetFileName(missingAssembly),
|
||||
$"Missing dependency required by {loadedAssembly.Name}",
|
||||
loadedAssembly.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
|
||||
continue;
|
||||
}
|
||||
catch (TypeLoadException e)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Error, "Unable to get types for assembly {0}: {1}",
|
||||
loadedAssembly.Name, e.Message);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, e.StackTrace);
|
||||
|
||||
// Add to incompatible plugins if this is likely a .NET 8 compatibility issue
|
||||
if (e.Message.Contains("Could not load type") ||
|
||||
e.Message.Contains("Unable to load one or more of the requested types"))
|
||||
{
|
||||
IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name,
|
||||
$"Type loading error: {e.Message}",
|
||||
null));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -425,22 +650,66 @@ namespace PepperDash.Essentials
|
||||
loadedAssembly.Name, e.Message, type.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Error Loading assembly {0}: {1}",
|
||||
loadedAssembly.Name, e.Message);
|
||||
loadedAssembly.Name, e.Message);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "{0}", e.StackTrace);
|
||||
|
||||
// Add to incompatible plugins if this is likely a .NET 8 compatibility issue
|
||||
if (e.Message.Contains("Could not load type") ||
|
||||
e.Message.Contains("Unable to load one or more of the requested types"))
|
||||
{
|
||||
IncompatiblePlugins.Add(new IncompatiblePlugin(loadedAssembly.Name,
|
||||
$"Assembly loading error: {e.Message}",
|
||||
null));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Update incompatible plugins with dependency information
|
||||
var pluginDependencies = new Dictionary<string, List<string>>();
|
||||
// Populate pluginDependencies with relevant data
|
||||
// Example: pluginDependencies["PluginA"] = new List<string> { "Dependency1", "Dependency2" };
|
||||
UpdateIncompatiblePluginDependencies(pluginDependencies);
|
||||
|
||||
// plugin dll will be loaded. Any classes in plugin should have a static constructor
|
||||
// that registers that class with the Core.DeviceFactory
|
||||
Debug.LogMessage(LogEventLevel.Information, "Done Loading Custom Plugin Types.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates incompatible plugins with information about which plugins depend on them
|
||||
/// </summary>
|
||||
private static void UpdateIncompatiblePluginDependencies(Dictionary<string, List<string>> pluginDependencies)
|
||||
{
|
||||
// For each incompatible plugin
|
||||
foreach (var incompatiblePlugin in IncompatiblePlugins)
|
||||
{
|
||||
// If it already has a requestedBy, skip it
|
||||
if (incompatiblePlugin.TriggeredBy != "Direct load")
|
||||
continue;
|
||||
|
||||
// Find plugins that depend on this incompatible plugin
|
||||
foreach (var plugin in pluginDependencies)
|
||||
{
|
||||
string pluginName = plugin.Key;
|
||||
List<string> dependencies = plugin.Value;
|
||||
|
||||
// If this plugin depends on the incompatible plugin
|
||||
if (dependencies.Contains(incompatiblePlugin.Name) ||
|
||||
dependencies.Any(d => d.StartsWith(incompatiblePlugin.Name + ",")))
|
||||
{
|
||||
incompatiblePlugin.UpdateTriggeringPlugin(pluginName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Loads a
|
||||
/// </summary>
|
||||
@@ -517,7 +786,6 @@ namespace PepperDash.Essentials
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Loading legacy plugin: {0}", loadedAssembly.Name);
|
||||
loadPlugin.Invoke(null, null);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -527,7 +795,7 @@ namespace PepperDash.Essentials
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Attempting to Load Plugins from {_pluginDirectory}", _pluginDirectory);
|
||||
|
||||
if (Directory.Exists(_pluginDirectory))
|
||||
if (SystemIO.Directory.Exists(_pluginDirectory))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Plugins directory found, checking for plugins");
|
||||
|
||||
@@ -537,7 +805,7 @@ namespace PepperDash.Essentials
|
||||
// Deal with any .cplz files
|
||||
UnzipAndMoveCplzArchives();
|
||||
|
||||
if (Directory.Exists(_loadedPluginsDirectoryPath))
|
||||
if (SystemIO.Directory.Exists(_loadedPluginsDirectoryPath))
|
||||
{
|
||||
// Load the assemblies from the loadedPlugins folder into the AppDomain
|
||||
LoadPluginAssemblies();
|
||||
@@ -545,9 +813,27 @@ namespace PepperDash.Essentials
|
||||
// Load the types from any custom plugin assemblies
|
||||
LoadCustomPluginTypes();
|
||||
}
|
||||
|
||||
// Report on incompatible plugins
|
||||
if (IncompatiblePlugins.Count > 0)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Warning, "Found {0} incompatible plugins:", IncompatiblePlugins.Count);
|
||||
foreach (var plugin in IncompatiblePlugins)
|
||||
{
|
||||
if (plugin.TriggeredBy != "Direct load")
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1} (Required by: {2})",
|
||||
plugin.Name, plugin.Reason, plugin.TriggeredBy);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Warning, " - {0}: {1}",
|
||||
plugin.Name, plugin.Reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -574,4 +860,52 @@ namespace PepperDash.Essentials
|
||||
Assembly = assembly;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a plugin that was found to be incompatible with .NET 8
|
||||
/// </summary>
|
||||
public class IncompatiblePlugin
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; private set; }
|
||||
|
||||
[JsonProperty("reason")]
|
||||
public string Reason { get; private set; }
|
||||
|
||||
[JsonProperty("triggeredBy")]
|
||||
public string TriggeredBy { get; private set; }
|
||||
|
||||
public IncompatiblePlugin(string name, string reason, string triggeredBy = null)
|
||||
{
|
||||
Name = name;
|
||||
Reason = reason;
|
||||
TriggeredBy = triggeredBy ?? "Direct load";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the plugin that triggered this incompatibility
|
||||
/// </summary>
|
||||
/// <param name="triggeringPlugin">Name of the plugin that requires this incompatible plugin</param>
|
||||
public void UpdateTriggeringPlugin(string triggeringPlugin)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(triggeringPlugin))
|
||||
{
|
||||
TriggeredBy = triggeringPlugin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute to explicitly mark a plugin as .NET 8 compatible
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Assembly)]
|
||||
public class Net8CompatibleAttribute : Attribute
|
||||
{
|
||||
public bool IsCompatible { get; }
|
||||
|
||||
public Net8CompatibleAttribute(bool isCompatible = true)
|
||||
{
|
||||
IsCompatible = isCompatible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,13 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a device that manages room combinations by controlling partitions and scenarios.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="EssentialsRoomCombiner"/> allows for dynamic configuration of room
|
||||
/// combinations based on partition states and predefined scenarios. It supports both automatic and manual modes
|
||||
/// for managing room combinations. In automatic mode, the device determines the current room combination scenario
|
||||
/// based on partition sensor states. In manual mode, scenarios can be set explicitly by the user.</remarks>
|
||||
public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner
|
||||
{
|
||||
private EssentialsRoomCombinerPropertiesConfig _propertiesConfig;
|
||||
@@ -18,6 +25,9 @@ namespace PepperDash.Essentials.Core
|
||||
|
||||
private List<IEssentialsRoom> _rooms;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of rooms represented as key-name pairs.
|
||||
/// </summary>
|
||||
public List<IKeyName> Rooms
|
||||
{
|
||||
get
|
||||
@@ -28,6 +38,12 @@ namespace PepperDash.Essentials.Core
|
||||
|
||||
private bool _isInAutoMode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the system is operating in automatic mode.
|
||||
/// </summary>
|
||||
/// <remarks>Changing this property triggers an update event via
|
||||
/// <c>IsInAutoModeFeedback.FireUpdate()</c>. Ensure that any event listeners are properly configured to handle
|
||||
/// this update.</remarks>
|
||||
public bool IsInAutoMode
|
||||
{
|
||||
get
|
||||
@@ -46,12 +62,36 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether automatic mode is disabled.
|
||||
/// </summary>
|
||||
public bool DisableAutoMode
|
||||
{
|
||||
get
|
||||
{
|
||||
return _propertiesConfig.DisableAutoMode;
|
||||
}
|
||||
}
|
||||
|
||||
private CTimer _scenarioChangeDebounceTimer;
|
||||
|
||||
private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s
|
||||
|
||||
private Mutex _scenarioChange = new Mutex();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EssentialsRoomCombiner"/> class, which manages room combination
|
||||
/// scenarios and partition states.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="EssentialsRoomCombiner"/> class is designed to handle dynamic room
|
||||
/// combination scenarios based on partition states. It supports both automatic and manual modes for managing
|
||||
/// room combinations. By default, the instance starts in automatic mode unless the <paramref name="props"/>
|
||||
/// specifies otherwise. After activation, the room combiner initializes partition state providers and sets up
|
||||
/// the initial room configuration. Additionally, it subscribes to the <see
|
||||
/// cref="DeviceManager.AllDevicesInitialized"/> event to ensure proper initialization of dependent devices
|
||||
/// before determining or setting the room combination scenario.</remarks>
|
||||
/// <param name="key">The unique identifier for the room combiner instance.</param>
|
||||
/// <param name="props">The configuration properties for the room combiner, including default settings and debounce times.</param>
|
||||
public EssentialsRoomCombiner(string key, EssentialsRoomCombinerPropertiesConfig props)
|
||||
: base(key)
|
||||
{
|
||||
@@ -246,8 +286,16 @@ namespace PepperDash.Essentials.Core
|
||||
|
||||
#region IEssentialsRoomCombiner Members
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the room combination scenario changes.
|
||||
/// </summary>
|
||||
/// <remarks>This event is triggered whenever the configuration or state of the room combination
|
||||
/// changes. Subscribers can use this event to update their logic or UI based on the new scenario.</remarks>
|
||||
public event EventHandler<EventArgs> RoomCombinationScenarioChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current room combination scenario.
|
||||
/// </summary>
|
||||
public IRoomCombinationScenario CurrentScenario
|
||||
{
|
||||
get
|
||||
@@ -256,10 +304,25 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the feedback indicating whether the system is currently in auto mode.
|
||||
/// </summary>
|
||||
public BoolFeedback IsInAutoModeFeedback { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables auto mode for the room combiner and its partitions, allowing automatic room combination scenarios to
|
||||
/// be determined.
|
||||
/// </summary>
|
||||
/// <remarks>Auto mode allows the room combiner to automatically adjust its configuration based on
|
||||
/// the state of its partitions. If auto mode is disabled in the configuration, this method logs a warning and
|
||||
/// does not enable auto mode.</remarks>
|
||||
public void SetAutoMode()
|
||||
{
|
||||
if(_propertiesConfig.DisableAutoMode)
|
||||
{
|
||||
this.LogWarning("Auto mode is disabled for this room combiner. Cannot set to auto mode.");
|
||||
return;
|
||||
}
|
||||
IsInAutoMode = true;
|
||||
|
||||
foreach (var partition in Partitions)
|
||||
@@ -270,6 +333,12 @@ namespace PepperDash.Essentials.Core
|
||||
DetermineRoomCombinationScenario();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the system to manual mode, disabling automatic operations.
|
||||
/// </summary>
|
||||
/// <remarks>This method sets the system to manual mode by updating the mode state and propagates
|
||||
/// the change to all partitions. Once in manual mode, automatic operations are disabled for the system and its
|
||||
/// partitions.</remarks>
|
||||
public void SetManualMode()
|
||||
{
|
||||
IsInAutoMode = false;
|
||||
@@ -280,6 +349,11 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the current mode between automatic and manual.
|
||||
/// </summary>
|
||||
/// <remarks>If the current mode is automatic, this method switches to manual mode. If the
|
||||
/// current mode is manual, it switches to automatic mode.</remarks>
|
||||
public void ToggleMode()
|
||||
{
|
||||
if (IsInAutoMode)
|
||||
@@ -292,10 +366,22 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of room combination scenarios.
|
||||
/// </summary>
|
||||
public List<IRoomCombinationScenario> RoomCombinationScenarios { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of partition controllers managed by this instance.
|
||||
/// </summary>
|
||||
public List<IPartitionController> Partitions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the state of the partition identified by the specified partition key.
|
||||
/// </summary>
|
||||
/// <remarks>If no partition with the specified key exists, the method performs no
|
||||
/// action.</remarks>
|
||||
/// <param name="partitionKey">The key of the partition whose state is to be toggled. This value cannot be null or empty.</param>
|
||||
public void TogglePartitionState(string partitionKey)
|
||||
{
|
||||
var partition = Partitions.FirstOrDefault((p) => p.Key.Equals(partitionKey));
|
||||
@@ -306,6 +392,17 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the room combination scenario based on the specified scenario key.
|
||||
/// </summary>
|
||||
/// <remarks>This method manually adjusts the partition states according to the specified
|
||||
/// scenario. If the application is in auto mode, the operation will not proceed, and a log message will be
|
||||
/// generated indicating that the mode must be set to manual first. If the specified scenario key does not
|
||||
/// match any existing scenario, a debug log message will be generated. For each partition state in the
|
||||
/// scenario, the corresponding partition will be updated to either "Present" or "Not Present" based on the
|
||||
/// scenario's configuration. If a partition key in the scenario cannot be found, a debug log message will be
|
||||
/// generated.</remarks>
|
||||
/// <param name="scenarioKey">The key identifying the room combination scenario to apply. This must match the key of an existing scenario.</param>
|
||||
public void SetRoomCombinationScenario(string scenarioKey)
|
||||
{
|
||||
if (IsInAutoMode)
|
||||
@@ -354,13 +451,32 @@ namespace PepperDash.Essentials.Core
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a factory for creating instances of <see cref="EssentialsRoomCombiner"/> devices.
|
||||
/// </summary>
|
||||
/// <remarks>This factory is responsible for constructing <see cref="EssentialsRoomCombiner"/> devices
|
||||
/// based on the provided configuration. It supports the type name "essentialsroomcombiner" for device
|
||||
/// creation.</remarks>
|
||||
public class EssentialsRoomCombinerFactory : EssentialsDeviceFactory<EssentialsRoomCombiner>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EssentialsRoomCombinerFactory"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>This factory is used to create instances of room combiners with the specified type
|
||||
/// names. By default, the factory includes the type name "essentialsroomcombiner".</remarks>
|
||||
public EssentialsRoomCombinerFactory()
|
||||
{
|
||||
TypeNames = new List<string> { "essentialsroomcombiner" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and initializes a new instance of the <see cref="EssentialsRoomCombiner"/> device.
|
||||
/// </summary>
|
||||
/// <remarks>This method uses the provided device configuration to extract the properties and
|
||||
/// create an <see cref="EssentialsRoomCombiner"/> device. Ensure that the configuration contains valid
|
||||
/// properties for the device to be created successfully.</remarks>
|
||||
/// <param name="dc">The device configuration containing the key and properties required to build the device.</param>
|
||||
/// <returns>A new instance of <see cref="EssentialsRoomCombiner"/> initialized with the specified configuration.</returns>
|
||||
public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new EssentialsRoomCombiner Device");
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using PepperDash.Core;
|
||||
|
||||
@@ -17,6 +11,14 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public class EssentialsRoomCombinerPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the system operates in automatic mode.
|
||||
/// <remarks>Some systems don't have partitions sensors, and show shouldn't allow auto mode to be turned on. When this is true in the configuration,
|
||||
/// auto mode won't be allowed to be turned on.</remarks>
|
||||
/// </summary>
|
||||
[JsonProperty("disableAutoMode")]
|
||||
public bool DisableAutoMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of partitions that device the rooms
|
||||
/// </summary>
|
||||
@@ -47,6 +49,9 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonProperty("defaultScenarioKey")]
|
||||
public string defaultScenarioKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the debounce time, in seconds, for scenario changes.
|
||||
/// </summary>
|
||||
[JsonProperty("scenarioChangeDebounceTimeSeconds")]
|
||||
public int ScenarioChangeDebounceTimeSeconds { get; set; }
|
||||
}
|
||||
@@ -56,9 +61,15 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public class PartitionConfig : IKeyName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the unique key associated with the object.
|
||||
/// </summary>
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name associated with the object.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
@@ -80,12 +91,21 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public class RoomCombinationScenarioConfig : IKeyName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key associated with the object.
|
||||
/// </summary>
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name associated with the object.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of partition states.
|
||||
/// </summary>
|
||||
[JsonProperty("partitionStates")]
|
||||
public List<PartitionState> PartitionStates { get; set; }
|
||||
|
||||
@@ -95,9 +115,15 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonProperty("uiMap")]
|
||||
public Dictionary<string, string> UiMap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of actions to be performed during device activation.
|
||||
/// </summary>
|
||||
[JsonProperty("activationActions")]
|
||||
public List<DeviceActionWrapper> ActivationActions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of actions to be performed when a device is deactivated.
|
||||
/// </summary>
|
||||
[JsonProperty("deactivationActions")]
|
||||
public List<DeviceActionWrapper> DeactivationActions { get; set; }
|
||||
}
|
||||
@@ -107,9 +133,15 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public class PartitionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the partition key used to group and organize data within a storage system.
|
||||
/// </summary>
|
||||
[JsonProperty("partitionKey")]
|
||||
public string PartitionKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a partition is currently present.
|
||||
/// </summary>
|
||||
[JsonProperty("partitionSensedState")]
|
||||
public bool PartitionPresent { get; set; }
|
||||
}
|
||||
|
||||
@@ -28,9 +28,20 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonIgnore]
|
||||
BoolFeedback IsInAutoModeFeedback {get;}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the automatic mode is disabled.
|
||||
/// </summary>
|
||||
[JsonProperty("disableAutoMode")]
|
||||
bool DisableAutoMode { get; }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the system is operating in automatic mode.
|
||||
/// </summary>
|
||||
[JsonProperty("isInAutoMode")]
|
||||
bool IsInAutoMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of rooms associated with the current object.
|
||||
/// </summary>
|
||||
[JsonProperty("rooms")]
|
||||
List<IKeyName> Rooms { get; }
|
||||
|
||||
@@ -74,6 +85,13 @@ namespace PepperDash.Essentials.Core
|
||||
void SetRoomCombinationScenario(string scenarioKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a scenario for combining rooms, including activation, deactivation, and associated state.
|
||||
/// </summary>
|
||||
/// <remarks>This interface defines the behavior for managing room combination scenarios, including
|
||||
/// activation and deactivation, tracking the active state, and managing related partition states and UI mappings.
|
||||
/// Implementations of this interface are expected to handle the logic for room combinations based on the provided
|
||||
/// partition states and UI mappings.</remarks>
|
||||
public interface IRoomCombinationScenario : IKeyName
|
||||
{
|
||||
/// <summary>
|
||||
@@ -82,6 +100,9 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonIgnore]
|
||||
BoolFeedback IsActiveFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the entity is active.
|
||||
/// </summary>
|
||||
[JsonProperty("isActive")]
|
||||
bool IsActive { get; }
|
||||
|
||||
|
||||
@@ -18,8 +18,15 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores pending route requests, keyed by the destination device key.
|
||||
/// Used primarily to handle routing requests while a device is cooling down.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, RouteRequest> RouteRequests = new Dictionary<string, RouteRequest>();
|
||||
|
||||
/// <summary>
|
||||
/// A queue to process route requests and releases sequentially.
|
||||
/// </summary>
|
||||
private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue");
|
||||
|
||||
/// <summary>
|
||||
@@ -38,16 +45,49 @@ namespace PepperDash.Essentials.Core
|
||||
|
||||
ReleaseAndMakeRoute(destination, source, signalType, inputPort, outputPort);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will release the existing route to the destination, if a route is found. This does not CLEAR the route, only stop counting usage time on any output ports that have a usage tracker set
|
||||
/// </summary>
|
||||
/// <param name="destination">destination to clear</param>
|
||||
public static void ReleaseRoute(this IRoutingInputs destination)
|
||||
{
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty));
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will release the existing route to the destination, if a route is found. This does not CLEAR the route, only stop counting usage time on any output ports that have a usage tracker set
|
||||
/// </summary>
|
||||
/// <param name="destination">destination to clear</param>
|
||||
/// <param name="inputPortKey">Input to use to find existing route</param>
|
||||
public static void ReleaseRoute(this IRoutingInputs destination, string inputPortKey)
|
||||
{
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey));
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the route on the destination. This will remove any routes that are currently in use
|
||||
/// </summary>
|
||||
/// <param name="destination">Destination</param>
|
||||
public static void ClearRoute(this IRoutingInputs destination)
|
||||
{
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, string.Empty, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the route on the destination. This will remove any routes that are currently in use
|
||||
/// </summary>
|
||||
/// <param name="destination">destination</param>
|
||||
/// <param name="inputPortKey">input to use to find existing route</param>
|
||||
public static void ClearRoute(this IRoutingInputs destination, string inputPortKey)
|
||||
{
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, inputPortKey, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the route request for the destination. This will remove any routes that are currently in use
|
||||
/// </summary>
|
||||
/// <param name="destinationKey">destination device key</param>
|
||||
public static void RemoveRouteRequestForDestination(string destinationKey)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey);
|
||||
@@ -130,6 +170,15 @@ namespace PepperDash.Essentials.Core
|
||||
return (audioRouteDescriptor, videoRouteDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to handle the logic for releasing an existing route and making a new one.
|
||||
/// Handles devices with cooling states by queueing the request.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination device.</param>
|
||||
/// <param name="source">The source device.</param>
|
||||
/// <param name="signalType">The type of signal to route.</param>
|
||||
/// <param name="destinationPort">The specific destination input port (optional).</param>
|
||||
/// <param name="sourcePort">The specific source output port (optional).</param>
|
||||
private static void ReleaseAndMakeRoute(IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort = null, RoutingOutputPort sourcePort = null)
|
||||
{
|
||||
if (destination == null) throw new ArgumentNullException(nameof(destination));
|
||||
@@ -184,11 +233,16 @@ namespace PepperDash.Essentials.Core
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
|
||||
}
|
||||
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty));
|
||||
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty, false));
|
||||
|
||||
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
|
||||
/// Finds the route path, adds it to the collection, and executes the switches.
|
||||
/// </summary>
|
||||
/// <param name="request">The route request details.</param>
|
||||
private static void RunRouteRequest(RouteRequest request)
|
||||
{
|
||||
try
|
||||
@@ -216,14 +270,15 @@ namespace PepperDash.Essentials.Core
|
||||
{
|
||||
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will release the existing route on the destination, if it is found in
|
||||
/// RouteDescriptorCollection.DefaultCollection
|
||||
/// Will release the existing route on the destination, if it is found in RouteDescriptorCollection.DefaultCollection
|
||||
/// </summary>
|
||||
/// <param name="destination"></param>
|
||||
private static void ReleaseRouteInternal(IRoutingInputs destination, string inputPortKey)
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="inputPortKey"> The input port key to use to find the route. If empty, will use the first available input port</param>
|
||||
/// <param name="clearRoute"> If true, will clear the route on the destination. This will remove any routes that are currently in use</param>
|
||||
private static void ReleaseRouteInternal(IRoutingInputs destination, string inputPortKey, bool clearRoute)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -242,7 +297,7 @@ namespace PepperDash.Essentials.Core
|
||||
if (current != null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
|
||||
current.ReleaseRoutes();
|
||||
current.ReleaseRoutes(clearRoute);
|
||||
}
|
||||
} catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an IRoutingOutputs devices as being a source - the start of the chain
|
||||
/// Marker interface to identify a device that acts as the origin of a signal path (<see cref="IRoutingOutputs"/>).
|
||||
/// </summary>
|
||||
public interface IRoutingSource : IRoutingOutputs
|
||||
{
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a routing device (<see cref="IRouting"/>) that supports explicitly clearing a route on an output.
|
||||
/// </summary>
|
||||
public interface IRoutingWithClear : IRouting
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -3,14 +3,25 @@ using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for handling route change events on devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||
/// </summary>
|
||||
/// <param name="midpoint">The routing device where the change occurred.</param>
|
||||
/// <param name="newRoute">A descriptor of the new route that was established.</param>
|
||||
public delegate void RouteChangedEventHandler(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute);
|
||||
/// <summary>
|
||||
/// Defines an IRouting with a feedback event
|
||||
/// Defines a routing device (<see cref="IRouting"/>) that provides feedback about its current routes.
|
||||
/// </summary>
|
||||
public interface IRoutingWithFeedback : IRouting
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a list describing the currently active routes on this device.
|
||||
/// </summary>
|
||||
List<RouteSwitchDescriptor> CurrentRoutes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event triggered when a route changes on this device.
|
||||
/// </summary>
|
||||
event RouteChangedEventHandler RouteChanged;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a routing device (typically a transmitter or source) that provides numeric feedback for its current route.
|
||||
/// Extends <see cref="IRoutingNumeric"/>.
|
||||
/// </summary>
|
||||
public interface ITxRouting : IRoutingNumeric
|
||||
{
|
||||
/// <summary>
|
||||
/// Feedback indicating the currently routed video source by its numeric identifier.
|
||||
/// </summary>
|
||||
IntFeedback VideoSourceNumericFeedback { get; }
|
||||
/// <summary>
|
||||
/// Feedback indicating the currently routed audio source by its numeric identifier.
|
||||
/// </summary>
|
||||
IntFeedback AudioSourceNumericFeedback { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharpPro;
|
||||
|
||||
@@ -9,35 +10,63 @@ using Serilog.Events;
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an collection of individual route steps between Source and Destination
|
||||
/// Represents a collection of individual route steps between a Source and a Destination device for a specific signal type.
|
||||
/// </summary>
|
||||
public class RouteDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// The destination device (sink or midpoint) for the route.
|
||||
/// </summary>
|
||||
public IRoutingInputs Destination { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The specific input port on the destination device used for this route. Can be null if not specified or applicable.
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source device for the route.
|
||||
/// </summary>
|
||||
public IRoutingOutputs Source { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of signal being routed (e.g., Audio, Video). This descriptor represents a single signal type.
|
||||
/// </summary>
|
||||
public eRoutingSignalType SignalType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of individual switching steps required to establish the route.
|
||||
/// </summary>
|
||||
public List<RouteSwitchDescriptor> Routes { get; private set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteDescriptor"/> class for a route without a specific destination input port.
|
||||
/// </summary>
|
||||
/// <param name="source">The source device.</param>
|
||||
/// <param name="destination">The destination device.</param>
|
||||
/// <param name="signalType">The type of signal being routed.</param>
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) : this(source, destination, null, signalType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteDescriptor"/> class for a route with a specific destination input port.
|
||||
/// </summary>
|
||||
/// <param name="source">The source device.</param>
|
||||
/// <param name="destination">The destination device.</param>
|
||||
/// <param name="inputPort">The destination input port (optional).</param>
|
||||
/// <param name="signalType">The signal type for this route.</param>
|
||||
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType)
|
||||
{
|
||||
Destination = destination;
|
||||
InputPort = inputPort;
|
||||
Source = source;
|
||||
SignalType = signalType;
|
||||
InputPort = inputPort;
|
||||
Routes = new List<RouteSwitchDescriptor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes all routes described in this collection. Typically called via
|
||||
/// extension method IRoutingInputs.ReleaseAndMakeRoute()
|
||||
/// Executes all the switching steps defined in the <see cref="Routes"/> list.
|
||||
/// </summary>
|
||||
public void ExecuteRoutes()
|
||||
{
|
||||
@@ -63,15 +92,27 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases all routes in this collection. Typically called via
|
||||
/// extension method IRoutingInputs.ReleaseAndMakeRoute()
|
||||
/// Releases the usage tracking for the route and optionally clears the route on the switching devices.
|
||||
/// </summary>
|
||||
public void ReleaseRoutes()
|
||||
/// <param name="clearRoute">If true, attempts to clear the route on the switching devices (e.g., set input to null/0).</param>
|
||||
public void ReleaseRoutes(bool clearRoute = false)
|
||||
{
|
||||
foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting))
|
||||
{
|
||||
if (route.SwitchingDevice is IRouting switchingDevice)
|
||||
{
|
||||
if(clearRoute)
|
||||
{
|
||||
try
|
||||
{
|
||||
switchingDevice.ExecuteSwitch(null, route.OutputPort.Selector, SignalType);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError("Error executing switch: {exception}", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
if (route.OutputPort == null)
|
||||
{
|
||||
continue;
|
||||
@@ -90,6 +131,10 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the route.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
||||
|
||||
@@ -4,15 +4,42 @@ using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a request to establish a route between a source and a destination device.
|
||||
/// </summary>
|
||||
public class RouteRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The specific input port on the destination device to use for the route. Can be null if the port should be automatically determined or is not applicable.
|
||||
/// </summary>
|
||||
public RoutingInputPort DestinationPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The specific output port on the source device to use for the route. Can be null if the port should be automatically determined or is not applicable.
|
||||
/// </summary>
|
||||
public RoutingOutputPort SourcePort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The destination device (sink or midpoint) for the route.
|
||||
/// </summary>
|
||||
public IRoutingInputs Destination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The source device for the route.
|
||||
/// </summary>
|
||||
public IRoutingOutputs Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of signal being routed (e.g., Audio, Video, AudioVideo).
|
||||
/// </summary>
|
||||
public eRoutingSignalType SignalType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles the route request after a device's cooldown period has finished.
|
||||
/// This method is typically subscribed to the IsCoolingDownFeedback.OutputChange event.
|
||||
/// </summary>
|
||||
/// <param name="sender">The object that triggered the event (usually the cooling device).</param>
|
||||
/// <param name="args">Event arguments indicating the cooldown state change.</param>
|
||||
public void HandleCooldown(object sender, FeedbackEventArgs args)
|
||||
{
|
||||
try
|
||||
@@ -39,6 +66,10 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the route request.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the source and destination of the route request.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Route {Source?.Key ?? "No Source Device"}:{SourcePort?.Key ?? "auto"} to {Destination?.Key ?? "No Destination Device"}:{DestinationPort?.Key ?? "auto"}";
|
||||
|
||||
@@ -5,17 +5,34 @@ using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an item in the route request queue.
|
||||
/// </summary>
|
||||
public class RouteRequestQueueItem : IQueueMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to perform for the route request.
|
||||
/// </summary>
|
||||
private readonly Action<RouteRequest> action;
|
||||
/// <summary>
|
||||
/// The route request data.
|
||||
/// </summary>
|
||||
private readonly RouteRequest routeRequest;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteRequestQueueItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="routeAction">The action to perform.</param>
|
||||
/// <param name="request">The route request data.</param>
|
||||
public RouteRequestQueueItem(Action<RouteRequest> routeAction, RouteRequest request)
|
||||
{
|
||||
action = routeAction;
|
||||
routeRequest = request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches the route request action.
|
||||
/// </summary>
|
||||
public void Dispatch()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Dispatching route request {routeRequest}", null, routeRequest);
|
||||
@@ -23,23 +40,50 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an item in the queue for releasing a route.
|
||||
/// </summary>
|
||||
public class ReleaseRouteQueueItem : IQueueMessage
|
||||
{
|
||||
private readonly Action<IRoutingInputs, string> action;
|
||||
/// <summary>
|
||||
/// The action to perform for releasing the route.
|
||||
/// </summary>
|
||||
private readonly Action<IRoutingInputs, string, bool> action;
|
||||
/// <summary>
|
||||
/// The destination device whose route is being released.
|
||||
/// </summary>
|
||||
private readonly IRoutingInputs destination;
|
||||
/// <summary>
|
||||
/// The specific input port key on the destination to release, or null/empty for any/default.
|
||||
/// </summary>
|
||||
private readonly string inputPortKey;
|
||||
/// <summary>
|
||||
/// Indicates whether to clear the route (send null) or just release the usage tracking.
|
||||
/// </summary>
|
||||
private readonly bool clearRoute;
|
||||
|
||||
public ReleaseRouteQueueItem(Action<IRoutingInputs, string> action, IRoutingInputs destination, string inputPortKey)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ReleaseRouteQueueItem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to perform.</param>
|
||||
/// <param name="destination">The destination device.</param>
|
||||
/// <param name="inputPortKey">The input port key.</param>
|
||||
/// <param name="clearRoute">True to clear the route, false to just release.</param>
|
||||
public ReleaseRouteQueueItem(Action<IRoutingInputs, string, bool> action, IRoutingInputs destination, string inputPortKey, bool clearRoute)
|
||||
{
|
||||
this.action = action;
|
||||
this.destination = destination;
|
||||
this.inputPortKey = inputPortKey;
|
||||
this.clearRoute = clearRoute;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches the release route action.
|
||||
/// </summary>
|
||||
public void Dispatch()
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Dispatching release route request for {destination}:{inputPortKey}", null, destination?.Key ?? "no destination", string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||
action(destination, inputPortKey);
|
||||
action(destination, inputPortKey, clearRoute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,47 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an individual link for a route
|
||||
/// Represents a single switching step within a larger route, detailing the switching device, input port, and optionally the output port.
|
||||
/// </summary>
|
||||
public class RouteSwitchDescriptor
|
||||
{
|
||||
/// <summary>
|
||||
/// The device performing the switch (derived from the InputPort's parent).
|
||||
/// </summary>
|
||||
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
||||
/// <summary>
|
||||
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
||||
/// </summary>
|
||||
public RoutingOutputPort OutputPort { get; set; }
|
||||
/// <summary>
|
||||
/// The input port being switched to.
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
|
||||
/// </summary>
|
||||
/// <param name="inputPort">The input port being switched to.</param>
|
||||
public RouteSwitchDescriptor(RoutingInputPort inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">The output port being switched from.</param>
|
||||
/// <param name="inputPort">The input port being switched to.</param>
|
||||
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
OutputPort = outputPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the route switch descriptor.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the switch operation.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
if (SwitchingDevice is IRouting)
|
||||
|
||||
@@ -5,8 +5,17 @@ using System.Linq;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages routing feedback by subscribing to route changes on midpoint and sink devices,
|
||||
/// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices.
|
||||
/// </summary>
|
||||
public class RoutingFeedbackManager:EssentialsDevice
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key for this manager device.</param>
|
||||
/// <param name="name">The name of this manager device.</param>
|
||||
public RoutingFeedbackManager(string key, string name): base(key, name)
|
||||
{
|
||||
AddPreActivationAction(SubscribeForMidpointFeedback);
|
||||
@@ -14,6 +23,9 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||
/// </summary>
|
||||
private void SubscribeForMidpointFeedback()
|
||||
{
|
||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||
@@ -24,6 +36,9 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the InputChanged event on all devices implementing <see cref="IRoutingSinkWithSwitchingWithInputPort"/>.
|
||||
/// </summary>
|
||||
private void SubscribeForSinkFeedback()
|
||||
{
|
||||
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
@@ -34,6 +49,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the RouteChanged event from a midpoint device.
|
||||
/// Triggers an update for all sink devices.
|
||||
/// </summary>
|
||||
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
||||
/// <param name="newRoute">The descriptor of the new route.</param>
|
||||
private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute)
|
||||
{
|
||||
try
|
||||
@@ -51,6 +72,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the InputChanged event from a sink device.
|
||||
/// Triggers an update for the specific sink device.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sink device that reported an input change.</param>
|
||||
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
||||
private void HandleSinkUpdate(IRoutingSinkWithSwitching sender, RoutingInputPort currentInputPort)
|
||||
{
|
||||
try
|
||||
@@ -63,6 +90,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device
|
||||
/// based on its currently selected input port by tracing the route back through tie lines.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination sink device to update.</param>
|
||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||
private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort)
|
||||
{
|
||||
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key);
|
||||
@@ -199,6 +232,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively traces a route back from a given tie line to find the root source tie line.
|
||||
/// It navigates through midpoint devices (<see cref="IRoutingWithFeedback"/>) by checking their current routes.
|
||||
/// </summary>
|
||||
/// <param name="tieLine">The starting tie line (typically connected to a sink or midpoint).</param>
|
||||
/// <returns>The <see cref="TieLine"/> connected to the original source device, or null if the source cannot be determined.</returns>
|
||||
private TieLine GetRootTieLine(TieLine tieLine)
|
||||
{
|
||||
TieLine nextTieLine = null;
|
||||
|
||||
@@ -5,7 +5,7 @@ using System;
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic RoutingInput with no statuses.
|
||||
/// Represents a basic routing input port on a device.
|
||||
/// </summary>
|
||||
public class RoutingInputPort : RoutingPort
|
||||
{
|
||||
@@ -41,6 +41,10 @@ namespace PepperDash.Essentials.Core
|
||||
ParentDevice = parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the input port.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "ParentDeviceKey|PortKey|SignalType|ConnectionType".</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ParentDevice.Key}|{Key}|{Type}|{ConnectionType}";
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A RoutingInputPort for devices like DM-TX and DM input cards.
|
||||
/// Will provide video statistics on connected signals
|
||||
/// Represents a routing input port that provides video status feedback (e.g., sync, resolution).
|
||||
/// Suitable for devices like DM transmitters or DM input cards.
|
||||
/// </summary>
|
||||
public class RoutingInputPortWithVideoStatuses : RoutingInputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Video statuses attached to this port
|
||||
/// Provides feedback outputs for video statuses associated with this port.
|
||||
/// </summary>
|
||||
public VideoStatusOutputs VideoStatus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// Initializes a new instance of the <see cref="RoutingInputPortWithVideoStatuses"/> class.
|
||||
/// </summary>
|
||||
/// <param name="selector">An object used to refer to this port in the IRouting device's ExecuteSwitch method.
|
||||
/// May be string, number, whatever</param>
|
||||
/// <param name="parent">The IRoutingInputs object this lives on</param>
|
||||
/// <param name="funcs">A VideoStatusFuncsWrapper used to assign the callback funcs that will get
|
||||
/// the values for the various stats</param>
|
||||
/// <param name="key">The unique key for this port.</param>
|
||||
/// <param name="type">The signal type supported by this port.</param>
|
||||
/// <param name="connType">The physical connection type of this port.</param>
|
||||
/// <param name="selector">An object used to refer to this port in the parent device's ExecuteSwitch method.</param>
|
||||
/// <param name="parent">The <see cref="IRoutingInputs"/> device this port belongs to.</param>
|
||||
/// <param name="funcs">A <see cref="VideoStatusFuncsWrapper"/> containing delegates to retrieve video status values.</param>
|
||||
public RoutingInputPortWithVideoStatuses(string key,
|
||||
eRoutingSignalType type, eRoutingPortConnectionType connType, object selector,
|
||||
IRoutingInputs parent, VideoStatusFuncsWrapper funcs) :
|
||||
|
||||
@@ -3,32 +3,72 @@
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides event arguments for routing changes, potentially including numeric or port object references.
|
||||
/// </summary>
|
||||
public class RoutingNumericEventArgs : EventArgs
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The numeric representation of the output, if applicable.
|
||||
/// </summary>
|
||||
public uint? Output { get; set; }
|
||||
/// <summary>
|
||||
/// The numeric representation of the input, if applicable.
|
||||
/// </summary>
|
||||
public uint? Input { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of signal involved in the routing change.
|
||||
/// </summary>
|
||||
public eRoutingSignalType SigType { get; set; }
|
||||
/// <summary>
|
||||
/// The input port involved in the routing change, if applicable.
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; set; }
|
||||
/// <summary>
|
||||
/// The output port involved in the routing change, if applicable.
|
||||
/// </summary>
|
||||
public RoutingOutputPort OutputPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingNumericEventArgs"/> class using numeric identifiers.
|
||||
/// </summary>
|
||||
/// <param name="output">The numeric output identifier.</param>
|
||||
/// <param name="input">The numeric input identifier.</param>
|
||||
/// <param name="sigType">The signal type.</param>
|
||||
public RoutingNumericEventArgs(uint output, uint input, eRoutingSignalType sigType) : this(output, input, null, null, sigType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingNumericEventArgs"/> class using port objects.
|
||||
/// </summary>
|
||||
/// <param name="outputPort">The output port object.</param>
|
||||
/// <param name="inputPort">The input port object.</param>
|
||||
/// <param name="sigType">The signal type.</param>
|
||||
public RoutingNumericEventArgs(RoutingOutputPort outputPort, RoutingInputPort inputPort,
|
||||
eRoutingSignalType sigType)
|
||||
: this(null, null, outputPort, inputPort, sigType)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingNumericEventArgs"/> class with default values.
|
||||
/// </summary>
|
||||
public RoutingNumericEventArgs()
|
||||
: this(null, null, null, null, 0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingNumericEventArgs"/> class with potentially mixed identifiers.
|
||||
/// </summary>
|
||||
/// <param name="output">The numeric output identifier (optional).</param>
|
||||
/// <param name="input">The numeric input identifier (optional).</param>
|
||||
/// <param name="outputPort">The output port object (optional).</param>
|
||||
/// <param name="inputPort">The input port object (optional).</param>
|
||||
/// <param name="sigType">The signal type.</param>
|
||||
public RoutingNumericEventArgs(uint? output, uint? input, RoutingOutputPort outputPort,
|
||||
RoutingInputPort inputPort, eRoutingSignalType sigType)
|
||||
{
|
||||
|
||||
@@ -4,29 +4,46 @@ using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a basic routing output port on a device.
|
||||
/// </summary>
|
||||
public class RoutingOutputPort : RoutingPort
|
||||
{
|
||||
/// <summary>
|
||||
/// The IRoutingOutputs object this port lives on
|
||||
/// The IRoutingOutputs object this port lives on.
|
||||
/// </summary>
|
||||
///
|
||||
[JsonIgnore]
|
||||
public IRoutingOutputs ParentDevice { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tracks which destinations are currently using this output port.
|
||||
/// </summary>
|
||||
public InUseTracking InUseTracker { get; private set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingOutputPort"/> class.
|
||||
/// </summary>
|
||||
/// <param name="selector">An object used to refer to this port in the IRouting device's ExecuteSwitch method.
|
||||
/// May be string, number, whatever</param>
|
||||
/// <param name="parent">The IRoutingOutputs object this port lives on</param>
|
||||
/// <param name="key">The unique key for this port.</param>
|
||||
/// <param name="type">The signal type supported by this port.</param>
|
||||
/// <param name="connType">The physical connection type of this port.</param>
|
||||
/// <param name="selector">An object used to refer to this port in the parent device's ExecuteSwitch method.</param>
|
||||
/// <param name="parent">The <see cref="IRoutingOutputs"/> device this port belongs to.</param>
|
||||
public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType,
|
||||
object selector, IRoutingOutputs parent)
|
||||
: this(key, type, connType, selector, parent, false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingOutputPort"/> class, potentially marking it as internal.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key for this port.</param>
|
||||
/// <param name="type">The signal type supported by this port.</param>
|
||||
/// <param name="connType">The physical connection type of this port.</param>
|
||||
/// <param name="selector">An object used to refer to this port in the parent device's ExecuteSwitch method.</param>
|
||||
/// <param name="parent">The <see cref="IRoutingOutputs"/> device this port belongs to.</param>
|
||||
/// <param name="isInternal">True if this port represents an internal connection within a device (e.g., card to backplane).</param>
|
||||
public RoutingOutputPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType,
|
||||
object selector, IRoutingOutputs parent, bool isInternal)
|
||||
: base(key, type, connType, selector, isInternal)
|
||||
@@ -35,6 +52,10 @@ namespace PepperDash.Essentials.Core
|
||||
InUseTracker = new InUseTracking();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the output port.
|
||||
/// </summary>
|
||||
/// <returns>A string in the format "ParentDeviceKey|PortKey|SignalType|ConnectionType".</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{ParentDevice.Key}|{Key}|{Type}|{ConnectionType}";
|
||||
|
||||
@@ -4,18 +4,47 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for RoutingInput and Output ports
|
||||
/// Base class for <see cref="RoutingInputPort"/> and <see cref="RoutingOutputPort"/>.
|
||||
/// </summary>
|
||||
public abstract class RoutingPort : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique key identifying this port within its parent device.
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
/// <summary>
|
||||
/// The type of signal this port handles (e.g., Audio, Video, AudioVideo).
|
||||
/// </summary>
|
||||
public eRoutingSignalType Type { get; private set; }
|
||||
/// <summary>
|
||||
/// The physical connection type of this port (e.g., Hdmi, Rca, Dm).
|
||||
/// </summary>
|
||||
public eRoutingPortConnectionType ConnectionType { get; private set; }
|
||||
/// <summary>
|
||||
/// An object (often a number or string) used by the parent routing device to select this port during switching.
|
||||
/// </summary>
|
||||
public readonly object Selector;
|
||||
/// <summary>
|
||||
/// Indicates if this port represents an internal connection within a device (e.g., card to backplane).
|
||||
/// </summary>
|
||||
public bool IsInternal { get; private set; }
|
||||
/// <summary>
|
||||
/// An object used to match feedback values to this port, if applicable.
|
||||
/// </summary>
|
||||
public object FeedbackMatchObject { get; set; }
|
||||
/// <summary>
|
||||
/// A reference to the underlying hardware port object (e.g., SimplSharpPro Port), if applicable.
|
||||
/// </summary>
|
||||
public object Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingPort"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key for this port.</param>
|
||||
/// <param name="type">The signal type supported by this port.</param>
|
||||
/// <param name="connType">The physical connection type of this port.</param>
|
||||
/// <param name="selector">The selector object for switching.</param>
|
||||
/// <param name="isInternal">True if this port is internal.</param>
|
||||
public RoutingPort(string key, eRoutingSignalType type, eRoutingPortConnectionType connType, object selector, bool isInternal)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
@@ -7,7 +7,8 @@ using Crestron.SimplSharp;
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// These should correspond directly with the portNames var in the config tool.
|
||||
/// Defines constant string values for common routing port keys.
|
||||
/// These should correspond directly with the portNames var in the config tool.
|
||||
/// </summary>
|
||||
public class RoutingPortNames
|
||||
{
|
||||
|
||||
@@ -4,14 +4,23 @@ using System.Collections.Generic;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a connection (tie line) between a <see cref="RoutingOutputPort"/> and a <see cref="RoutingInputPort"/>.
|
||||
/// </summary>
|
||||
public class TieLine
|
||||
{
|
||||
/// <summary>
|
||||
/// The source output port of the tie line.
|
||||
/// </summary>
|
||||
public RoutingOutputPort SourcePort { get; private set; }
|
||||
/// <summary>
|
||||
/// The destination input port of the tie line.
|
||||
/// </summary>
|
||||
public RoutingInputPort DestinationPort { get; private set; }
|
||||
//public int InUseCount { get { return DestinationUsingThis.Count; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of this tie line. Will either be the type of the desination port
|
||||
/// Gets the type of this tie line. Will either be the type of the destination port
|
||||
/// or the type of OverrideType when it is set.
|
||||
/// </summary>
|
||||
public eRoutingSignalType Type
|
||||
@@ -35,20 +44,27 @@ namespace PepperDash.Essentials.Core
|
||||
//List<IRoutingInputs> DestinationUsingThis = new List<IRoutingInputs>();
|
||||
|
||||
/// <summary>
|
||||
/// For tie lines that represent internal links, like from cards to the matrix in a DM.
|
||||
/// This property is true if SourcePort and DestinationPort IsInternal
|
||||
/// property are both true
|
||||
/// Gets a value indicating whether this tie line represents an internal connection within a device (both source and destination ports are internal).
|
||||
/// </summary>
|
||||
public bool IsInternal { get { return SourcePort.IsInternal && DestinationPort.IsInternal; } }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the signal types of the source and destination ports differ.
|
||||
/// </summary>
|
||||
public bool TypeMismatch { get { return SourcePort.Type != DestinationPort.Type; } }
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the connection types of the source and destination ports differ.
|
||||
/// </summary>
|
||||
public bool ConnectionTypeMismatch { get { return SourcePort.ConnectionType != DestinationPort.ConnectionType; } }
|
||||
/// <summary>
|
||||
/// A descriptive note about any type mismatch, if applicable.
|
||||
/// </summary>
|
||||
public string TypeMismatchNote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Initializes a new instance of the <see cref="TieLine"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sourcePort"></param>
|
||||
/// <param name="destinationPort"></param>
|
||||
/// <param name="sourcePort">The source output port.</param>
|
||||
/// <param name="destinationPort">The destination input port.</param>
|
||||
public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort)
|
||||
{
|
||||
if (sourcePort == null || destinationPort == null)
|
||||
@@ -58,9 +74,11 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tie line with an overriding Type. See help for OverrideType property for info
|
||||
/// Creates a tie line with an overriding Type. See help for OverrideType property for info.
|
||||
/// </summary>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides DestinationPort.Type</param>
|
||||
/// <param name="sourcePort">The source output port.</param>
|
||||
/// <param name="destinationPort">The destination input port.</param>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations.</param>
|
||||
public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType? overrideType) :
|
||||
this(sourcePort, destinationPort)
|
||||
{
|
||||
@@ -68,9 +86,11 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a tie line with an overriding Type. See help for OverrideType property for info
|
||||
/// Creates a tie line with an overriding Type. See help for OverrideType property for info.
|
||||
/// </summary>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides DestinationPort.Type</param>
|
||||
/// <param name="sourcePort">The source output port.</param>
|
||||
/// <param name="destinationPort">The destination input port.</param>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations.</param>
|
||||
public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType overrideType) :
|
||||
this(sourcePort, destinationPort)
|
||||
{
|
||||
@@ -78,18 +98,25 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will link up video status from supporting inputs to connected outputs
|
||||
/// Will link up video status from supporting inputs to connected outputs.
|
||||
/// </summary>
|
||||
public void Activate()
|
||||
{
|
||||
// Now does nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the tie line.
|
||||
/// </summary>
|
||||
public void Deactivate()
|
||||
{
|
||||
// Now does nothing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the tie line.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the source, destination, and type of the tie line.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Tie line: {0}:{1} --> {2}:{3} {4}", SourcePort.ParentDevice.Key, SourcePort.Key,
|
||||
@@ -99,8 +126,14 @@ namespace PepperDash.Essentials.Core
|
||||
|
||||
//********************************************************************************
|
||||
|
||||
/// <summary>
|
||||
/// Represents a collection of <see cref="TieLine"/> objects.
|
||||
/// </summary>
|
||||
public class TieLineCollection : List<TieLine>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default singleton instance of the <see cref="TieLineCollection"/>.
|
||||
/// </summary>
|
||||
public static TieLineCollection Default
|
||||
{
|
||||
get
|
||||
@@ -111,6 +144,9 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backing field for the singleton instance.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
private static TieLineCollection _Default;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
@@ -15,15 +13,44 @@ using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the configuration data for a single tie line between two routing ports.
|
||||
/// </summary>
|
||||
public class TieLineConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The key of the source device.
|
||||
/// </summary>
|
||||
public string SourceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the source card (if applicable, e.g., in a modular chassis).
|
||||
/// </summary>
|
||||
public string SourceCard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the source output port.
|
||||
/// </summary>
|
||||
public string SourcePort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the destination device.
|
||||
/// </summary>
|
||||
public string DestinationKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the destination card (if applicable).
|
||||
/// </summary>
|
||||
public string DestinationCard { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the destination input port.
|
||||
/// </summary>
|
||||
public string DestinationPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional override for the signal type of the tie line. If set, this overrides the destination port's type for routing calculations.
|
||||
/// </summary>
|
||||
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public eRoutingSignalType? OverrideType { get; set; }
|
||||
@@ -73,11 +100,19 @@ namespace PepperDash.Essentials.Core.Config
|
||||
return new TieLine(sourceOutputPort, destinationInputPort, OverrideType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logs an error message related to creating this tie line configuration.
|
||||
/// </summary>
|
||||
/// <param name="msg">The specific error message.</param>
|
||||
void LogError(string msg)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Error, "WARNING: Cannot create tie line: {message}:\r {tieLineConfig}",null, msg, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the tie line configuration.
|
||||
/// </summary>
|
||||
/// <returns>A string describing the source and destination of the configured tie line.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}.{1}.{2} --> {3}.{4}.{5}", SourceKey, SourceCard, SourcePort,
|
||||
|
||||
37
src/PepperDash.Essentials.Core/SomeWebSocketClass.cs
Normal file
37
src/PepperDash.Essentials.Core/SomeWebSocketClass.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class SomeWebSocketClass
|
||||
{
|
||||
private ClientWebSocket _webSocket;
|
||||
|
||||
public SomeWebSocketClass()
|
||||
{
|
||||
_webSocket = new ClientWebSocket();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(Uri uri)
|
||||
{
|
||||
await _webSocket.ConnectAsync(uri, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendAsync(string message)
|
||||
{
|
||||
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
|
||||
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveAsync()
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
}
|
||||
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,14 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using PepperDash.Essentials.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
private readonly DebugWebsocketSink _sink = new DebugWebsocketSink();
|
||||
|
||||
public DebugSessionRequestHandler()
|
||||
: base(true)
|
||||
{
|
||||
@@ -43,21 +46,21 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
|
||||
var port = 0;
|
||||
|
||||
if (!Debug.WebsocketSink.IsRunning)
|
||||
if (!_sink.IsRunning)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Starting WS Server");
|
||||
// Generate a random port within a specified range
|
||||
port = new Random().Next(65435, 65535);
|
||||
// Start the WS Server
|
||||
Debug.WebsocketSink.StartServerAndSetPort(port);
|
||||
_sink.StartServerAndSetPort(port);
|
||||
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
|
||||
}
|
||||
|
||||
var url = Debug.WebsocketSink.Url;
|
||||
var url = _sink.Url;
|
||||
|
||||
object data = new
|
||||
{
|
||||
url = Debug.WebsocketSink.Url
|
||||
url = _sink.Url
|
||||
};
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
|
||||
@@ -84,7 +87,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
/// <param name="context"></param>
|
||||
protected override void HandlePost(HttpCwsContext context)
|
||||
{
|
||||
Debug.WebsocketSink.StopServer();
|
||||
_sink.StopServer();
|
||||
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.StatusDescription = "OK";
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
public class DebugWebsocketSink
|
||||
{
|
||||
private bool _isRunning;
|
||||
private string _url;
|
||||
|
||||
public bool IsRunning => _isRunning;
|
||||
public string Url => _url;
|
||||
|
||||
public void StartServerAndSetPort(int port)
|
||||
{
|
||||
try
|
||||
{
|
||||
_url = $"ws://localhost:{port}";
|
||||
_isRunning = true;
|
||||
// Implement actual server startup logic here
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_isRunning = false;
|
||||
throw new Exception($"Failed to start debug websocket server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void StopServer()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Implement actual server shutdown logic here
|
||||
_isRunning = false;
|
||||
_url = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to stop debug websocket server: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
|
||||
SendCameraPresetNamesToApi(presetsCamera, joinMap, trilist);
|
||||
|
||||
for (int i = 0; i < joinMap.NumberOfPresets.JoinSpan; i++)
|
||||
for (int i = 0; i < joinMap.PresetRecallStart.JoinSpan; i++)
|
||||
{
|
||||
int tempNum = i;
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.Codec.Cisco
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the available tracking modes for a Cisco codec's Presenter Track feature.
|
||||
/// </summary>
|
||||
public enum ePresenterTrackMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Presenter Track is turned off.
|
||||
/// </summary>
|
||||
Off,
|
||||
/// <summary>
|
||||
/// Presenter Track follows the speaker's movements.
|
||||
/// </summary>
|
||||
Follow,
|
||||
/// <summary>
|
||||
/// Presenter Track is set to background mode, where it tracks the speaker but does not actively follow.
|
||||
/// </summary>
|
||||
Background,
|
||||
/// <summary>
|
||||
/// Presenter Track is set to persistent mode, where it maintains a fixed position or focus on the speaker.
|
||||
/// </summary>
|
||||
Persistent
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Describes the Presenter Track controls for a Cisco codec.
|
||||
/// </summary>
|
||||
public interface IPresenterTrack : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
bool PresenterTrackAvailability { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback indicating whether Presenter Track is available.
|
||||
/// </summary>
|
||||
BoolFeedback PresenterTrackAvailableFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback indicating the current status of Presenter Track is off
|
||||
/// </summary>
|
||||
BoolFeedback PresenterTrackStatusOffFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback indicating the current status of Presenter Track is follow
|
||||
/// </summary>
|
||||
BoolFeedback PresenterTrackStatusFollowFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback indicating the current status of Presenter Track is background
|
||||
/// </summary>
|
||||
BoolFeedback PresenterTrackStatusBackgroundFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback indicating the current status of Presenter Track is persistent
|
||||
/// </summary>
|
||||
BoolFeedback PresenterTrackStatusPersistentFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the current status of Presenter Track.
|
||||
/// </summary>
|
||||
bool PresenterTrackStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Turns off Presenter Track.
|
||||
/// </summary>
|
||||
void PresenterTrackOff();
|
||||
|
||||
/// <summary>
|
||||
/// Turns on Presenter Track in follow mode.
|
||||
/// </summary>
|
||||
void PresenterTrackFollow();
|
||||
|
||||
/// <summary>
|
||||
/// Turns on Presenter Track in background mode.
|
||||
/// </summary>
|
||||
void PresenterTrackBackground();
|
||||
|
||||
/// <summary>
|
||||
/// Turns on Presenter Track in persistent mode.
|
||||
/// </summary>
|
||||
void PresenterTrackPersistent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.Codec.Cisco
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the available tracking modes for a Cisco codec
|
||||
/// </summary>
|
||||
public interface ISpeakerTrack : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether Speaker Track is available on the codec.
|
||||
/// </summary>
|
||||
bool SpeakerTrackAvailability { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
BoolFeedback SpeakerTrackAvailableFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback indicating the current status of Speaker Track is off
|
||||
/// </summary>
|
||||
bool SpeakerTrackStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Turns Speaker Track off
|
||||
/// </summary>
|
||||
void SpeakerTrackOff();
|
||||
/// <summary>
|
||||
/// Turns Speaker Track on
|
||||
/// </summary>
|
||||
void SpeakerTrackOn();
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,13 @@ namespace PepperDash.Essentials.Devices.Common.DSP
|
||||
|
||||
public Dictionary<string, DspControlPoint> SwitcherControlPoints { get; private set; }
|
||||
|
||||
public DspBase(string key, string name) :
|
||||
base(key, name)
|
||||
{
|
||||
public DspBase(string key, string name) :
|
||||
base(key, name)
|
||||
{
|
||||
|
||||
LevelControlPoints = new Dictionary<string, IBasicVolumeWithFeedback>();
|
||||
DialerControlPoints = new Dictionary<string, DspControlPoint>();
|
||||
SwitcherControlPoints = new Dictionary<string, DspControlPoint>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.Generic
|
||||
{
|
||||
public class GenericSink : EssentialsDevice, IRoutingSink
|
||||
public class GenericSink : EssentialsDevice, IRoutingSinkWithInputPort
|
||||
{
|
||||
public GenericSink(string key, string name) : base(key, name)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Configurations>Debug;Release;Debug 4.7.2</Configurations>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<AssemblyName>Essentials Devices Common</AssemblyName>
|
||||
@@ -28,6 +28,7 @@
|
||||
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -8,9 +8,10 @@ using System.Linq;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.SoftCodec
|
||||
{
|
||||
public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingOutputs, IRoutingSinkWithSwitching
|
||||
public class GenericSoftCodec : EssentialsDevice, IRoutingSource, IRoutingSinkWithSwitchingWithInputPort
|
||||
{
|
||||
private RoutingInputPort _currentInputPort;
|
||||
|
||||
public RoutingInputPort CurrentInputPort {
|
||||
get => _currentInputPort;
|
||||
set
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class SomeOtherWebSocketClass
|
||||
{
|
||||
private ClientWebSocket _webSocket;
|
||||
|
||||
public SomeOtherWebSocketClass()
|
||||
{
|
||||
_webSocket = new ClientWebSocket();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(Uri uri)
|
||||
{
|
||||
await _webSocket.ConnectAsync(uri, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendAsync(string message)
|
||||
{
|
||||
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
|
||||
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveAsync()
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
}
|
||||
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -12,20 +12,45 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
/// </summary>
|
||||
public interface IHasCodecRoomPresets
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is raised when the list of room presets has changed.
|
||||
/// </summary>
|
||||
event EventHandler<EventArgs> CodecRoomPresetsListHasChanged;
|
||||
|
||||
/// <summary>
|
||||
/// List of near end presets that can be recalled.
|
||||
/// </summary>
|
||||
List<CodecRoomPreset> NearEndPresets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// List of far end presets that can be recalled.
|
||||
/// </summary>
|
||||
List<CodecRoomPreset> FarEndRoomPresets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Selects a near end preset by its ID.
|
||||
/// </summary>
|
||||
/// <param name="preset"></param>
|
||||
void CodecRoomPresetSelect(int preset);
|
||||
|
||||
void CodecRoomPresetStore(int preset, string description);
|
||||
/// <summary>
|
||||
/// Stores a near end preset with the given ID and description.
|
||||
/// </summary>
|
||||
/// <param name="preset"></param>
|
||||
/// <param name="description"></param>
|
||||
void CodecRoomPresetStore(int preset, string description);
|
||||
|
||||
/// <summary>
|
||||
/// Selects a far end preset by its ID. This is typically used to recall a preset that has been defined on the far end codec.
|
||||
/// </summary>
|
||||
/// <param name="preset"></param>
|
||||
void SelectFarEndPreset(int preset);
|
||||
}
|
||||
|
||||
public static class RoomPresets
|
||||
/// <summary>
|
||||
/// Static class for converting non-generic RoomPresets to generic CameraPresets.
|
||||
/// </summary>
|
||||
public static class RoomPresets
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts non-generic RoomPresets to generic CameraPresets
|
||||
@@ -47,6 +72,13 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
|
||||
/// </summary>
|
||||
public class CodecRoomPreset : PresetBase
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="description"></param>
|
||||
/// <param name="def"></param>
|
||||
/// <param name="isDef"></param>
|
||||
public CodecRoomPreset(int id, string description, bool def, bool isDef)
|
||||
: base(id, description, def, isDef)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using Crestron.SimplSharp.Reflection;
|
||||
using System.Reflection;
|
||||
using Crestron.SimplSharpPro.DeviceSupport;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core;
|
||||
|
||||
@@ -2,27 +2,69 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core.DeviceInfo;
|
||||
using System.Timers;
|
||||
|
||||
namespace PepperDash.Essentials.AppServer.Messengers
|
||||
{
|
||||
/// <summary>
|
||||
/// Facilitates communication of device information by providing mechanisms for status updates and device
|
||||
/// information reporting.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="DeviceInfoMessenger"/> class integrates with an <see
|
||||
/// cref="IDeviceInfoProvider"/> to manage device-specific information. It uses a debounce timer to limit the
|
||||
/// frequency of updates, ensuring efficient communication. The timer is initialized with a 1-second interval and
|
||||
/// is disabled by default. This class also subscribes to device information change events and provides actions for
|
||||
/// reporting full device status and triggering updates.</remarks>
|
||||
public class DeviceInfoMessenger : MessengerBase
|
||||
{
|
||||
private readonly IDeviceInfoProvider _deviceInfoProvider;
|
||||
|
||||
private readonly Timer debounceTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeviceInfoMessenger"/> class, which facilitates communication
|
||||
/// of device information.
|
||||
/// </summary>
|
||||
/// <remarks>The messenger uses a debounce timer to limit the frequency of certain operations. The
|
||||
/// timer is initialized with a 1-second interval and is disabled by default.</remarks>
|
||||
/// <param name="key">A unique identifier for the messenger instance.</param>
|
||||
/// <param name="messagePath">The path used for sending and receiving messages.</param>
|
||||
/// <param name="device">An implementation of <see cref="IDeviceInfoProvider"/> that provides device-specific information.</param>
|
||||
public DeviceInfoMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device)
|
||||
{
|
||||
_deviceInfoProvider = device;
|
||||
}
|
||||
|
||||
debounceTimer = new Timer(1000)
|
||||
{
|
||||
Enabled = false,
|
||||
AutoReset = false
|
||||
};
|
||||
|
||||
debounceTimer.Elapsed += DebounceTimer_Elapsed;
|
||||
}
|
||||
|
||||
private void DebounceTimer_Elapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
PostStatusMessage(JToken.FromObject(new
|
||||
{
|
||||
deviceInfo = _deviceInfoProvider.DeviceInfo
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers actions and event handlers for device information updates and status reporting.
|
||||
/// </summary>
|
||||
/// <remarks>This method sets up actions for handling device status updates and reporting full
|
||||
/// device status. It also subscribes to the <see cref="IDeviceInfoProvider.DeviceInfoChanged"/> event to
|
||||
/// trigger debounced updates when the device information changes.</remarks>
|
||||
protected override void RegisterActions()
|
||||
{
|
||||
base.RegisterActions();
|
||||
|
||||
_deviceInfoProvider.DeviceInfoChanged += (o, a) =>
|
||||
{
|
||||
PostStatusMessage(JToken.FromObject(new
|
||||
{
|
||||
deviceInfo = a.DeviceInfo
|
||||
}));
|
||||
debounceTimer.Stop();
|
||||
debounceTimer.Start();
|
||||
};
|
||||
|
||||
AddAction("/fullStatus", (id, context) => PostStatusMessage(new DeviceInfoStateMessage
|
||||
@@ -34,6 +76,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message containing the state information of a device, including detailed device information.
|
||||
/// </summary>
|
||||
/// <remarks>This class is used to encapsulate the state of a device along with its associated
|
||||
/// information. It extends <see cref="DeviceStateMessageBase"/> to provide additional details about the
|
||||
/// device.</remarks>
|
||||
public class DeviceInfoStateMessage : DeviceStateMessageBase
|
||||
{
|
||||
[JsonProperty("deviceInfo")]
|
||||
|
||||
@@ -8,16 +8,42 @@ using System.Collections.Generic;
|
||||
|
||||
namespace PepperDash.Essentials.AppServer.Messengers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides messaging functionality for managing room combination scenarios and partition states in an <see
|
||||
/// cref="IEssentialsRoomCombiner"/> instance. Enables external systems to interact with the room combiner via
|
||||
/// predefined actions and status updates.
|
||||
/// </summary>
|
||||
/// <remarks>This class facilitates communication with an <see cref="IEssentialsRoomCombiner"/> by
|
||||
/// exposing actions for toggling modes, managing partitions, and setting room combination scenarios. It also
|
||||
/// listens for feedback changes and broadcasts status updates to connected systems. Typical usage involves
|
||||
/// registering actions for external commands and handling feedback events to synchronize state changes.</remarks>
|
||||
public class IEssentialsRoomCombinerMessenger : MessengerBase
|
||||
{
|
||||
private readonly IEssentialsRoomCombiner _roomCombiner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IEssentialsRoomCombinerMessenger"/> class, which facilitates
|
||||
/// messaging for an <see cref="IEssentialsRoomCombiner"/> instance.
|
||||
/// </summary>
|
||||
/// <remarks>This class is designed to enable communication and interaction with an <see
|
||||
/// cref="IEssentialsRoomCombiner"/> through the specified messaging path. Ensure that the <paramref
|
||||
/// name="roomCombiner"/> parameter is not null when creating an instance.</remarks>
|
||||
/// <param name="key">The unique key identifying this messenger instance.</param>
|
||||
/// <param name="messagePath">The path used for messaging operations.</param>
|
||||
/// <param name="roomCombiner">The <see cref="IEssentialsRoomCombiner"/> instance associated with this messenger.</param>
|
||||
public IEssentialsRoomCombinerMessenger(string key, string messagePath, IEssentialsRoomCombiner roomCombiner)
|
||||
: base(key, messagePath, roomCombiner as IKeyName)
|
||||
{
|
||||
_roomCombiner = roomCombiner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers actions and event handlers for managing room combination scenarios and partition states.
|
||||
/// </summary>
|
||||
/// <remarks>This method sets up various actions that can be triggered via specific endpoints,
|
||||
/// such as toggling modes, setting room combination scenarios, and managing partition states. It also
|
||||
/// subscribes to feedback events to update the status when changes occur in room combination scenarios or
|
||||
/// partition states.</remarks>
|
||||
protected override void RegisterActions()
|
||||
{
|
||||
AddAction("/fullStatus", (id, content) => SendFullStatus());
|
||||
@@ -107,6 +133,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
|
||||
var message = new IEssentialsRoomCombinerStateMessage
|
||||
{
|
||||
DisableAutoMode = _roomCombiner.DisableAutoMode,
|
||||
IsInAutoMode = _roomCombiner.IsInAutoMode,
|
||||
CurrentScenario = _roomCombiner.CurrentScenario,
|
||||
Rooms = rooms,
|
||||
@@ -132,20 +159,48 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the state message for a room combiner system, providing information about the current configuration,
|
||||
/// operational mode, and associated rooms, partitions, and scenarios.
|
||||
/// </summary>
|
||||
/// <remarks>This class is used to encapsulate the state of a room combiner system, including its current
|
||||
/// mode of operation, active room combination scenario, and the list of rooms and partitions involved. It is
|
||||
/// typically serialized and transmitted to communicate the state of the system.</remarks>
|
||||
public class IEssentialsRoomCombinerStateMessage : DeviceStateMessageBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether automatic mode is disabled.
|
||||
/// </summary>
|
||||
[JsonProperty("disableAutoMode", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool DisableAutoMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the system is operating in automatic mode.
|
||||
/// </summary>
|
||||
[JsonProperty("isInAutoMode", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool IsInAutoMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current room combination scenario.
|
||||
/// </summary>
|
||||
[JsonProperty("currentScenario", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public IRoomCombinationScenario CurrentScenario { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of rooms associated with the entity.
|
||||
/// </summary>
|
||||
[JsonProperty("rooms", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<IKeyName> Rooms { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of room combination scenarios.
|
||||
/// </summary>
|
||||
[JsonProperty("roomCombinationScenarios", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<IRoomCombinationScenario> RoomCombinationScenarios { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of partition controllers.
|
||||
/// </summary>
|
||||
[JsonProperty("partitions", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<IPartitionController> Partitions { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PepperDash.Essentials.AppServer</RootNamespace>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<AssemblyTitle>mobile-control-messengers</AssemblyTitle>
|
||||
<AssemblyName>mobile-control-messengers</AssemblyName>
|
||||
<Product>mobile-control-messengers</Product>
|
||||
@@ -32,7 +32,8 @@
|
||||
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PepperDash.Essentials</RootNamespace>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<Deterministic>false</Deterministic>
|
||||
<AssemblyTitle>epi-essentials-mobile-control</AssemblyTitle>
|
||||
@@ -37,7 +37,8 @@
|
||||
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -491,7 +491,7 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
{
|
||||
public MobileControlTouchpanelControllerFactory()
|
||||
{
|
||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel" };
|
||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
|
||||
MinimumEssentialsFrameworkVersion = "2.0.0";
|
||||
}
|
||||
|
||||
@@ -555,7 +555,10 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
return new Tsw1070(id, Global.ControlSystem);
|
||||
else if (type == "ts1070")
|
||||
return new Ts1070(id, Global.ControlSystem);
|
||||
else
|
||||
else if (type == "dge1000")
|
||||
return new Dge1000(id, Global.ControlSystem);
|
||||
else
|
||||
|
||||
{
|
||||
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "WARNING: Cannot create TSW controller with type '{0}'", type);
|
||||
return null;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,145 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.AppServer.Messengers;
|
||||
using PepperDash.Essentials.RoomBridges;
|
||||
using Serilog.Events;
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using WebSocketSharp;
|
||||
using WebSocketSharp.Server;
|
||||
using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
|
||||
|
||||
|
||||
namespace PepperDash.Essentials.WebSocketServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the behaviour to associate with a UiClient for WebSocket communication
|
||||
/// </summary>
|
||||
public class UiClient : WebSocketBehavior
|
||||
{
|
||||
public MobileControlSystemController Controller { get; set; }
|
||||
|
||||
public string RoomKey { get; set; }
|
||||
|
||||
private string _clientId;
|
||||
|
||||
private DateTime _connectionTime;
|
||||
|
||||
public TimeSpan ConnectedDuration
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Context.WebSocket.IsAlive)
|
||||
{
|
||||
return DateTime.Now - _connectionTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TimeSpan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public UiClient()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void OnOpen()
|
||||
{
|
||||
base.OnOpen();
|
||||
|
||||
var url = Context.WebSocket.Url;
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "New WebSocket Connection from: {0}", null, url);
|
||||
|
||||
var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)");
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
_connectionTime = DateTime.Now;
|
||||
return;
|
||||
}
|
||||
|
||||
var clientId = match.Groups[1].Value;
|
||||
_clientId = clientId;
|
||||
|
||||
if (Controller == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null");
|
||||
_connectionTime = DateTime.Now;
|
||||
}
|
||||
|
||||
var clientJoinedMessage = new MobileControlMessage
|
||||
{
|
||||
Type = "/system/clientJoined",
|
||||
Content = JToken.FromObject(new
|
||||
{
|
||||
clientId,
|
||||
roomKey = RoomKey,
|
||||
})
|
||||
};
|
||||
|
||||
Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage));
|
||||
|
||||
var bridge = Controller.GetRoomBridge(RoomKey);
|
||||
|
||||
if (bridge == null) return;
|
||||
|
||||
SendUserCodeToClient(bridge, clientId);
|
||||
|
||||
bridge.UserCodeChanged -= Bridge_UserCodeChanged;
|
||||
bridge.UserCodeChanged += Bridge_UserCodeChanged;
|
||||
|
||||
// TODO: Future: Check token to see if there's already an open session using that token and reject/close the session
|
||||
}
|
||||
|
||||
private void Bridge_UserCodeChanged(object sender, EventArgs e)
|
||||
{
|
||||
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
|
||||
}
|
||||
|
||||
private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId)
|
||||
{
|
||||
var content = new
|
||||
{
|
||||
userCode = bridge.UserCode,
|
||||
qrUrl = bridge.QrCodeUrl,
|
||||
};
|
||||
|
||||
var message = new MobileControlMessage
|
||||
{
|
||||
Type = "/system/userCodeChanged",
|
||||
ClientId = clientId,
|
||||
Content = JToken.FromObject(content)
|
||||
};
|
||||
|
||||
Controller.SendMessageObjectToDirectClient(message);
|
||||
}
|
||||
|
||||
protected override void OnMessage(MessageEventArgs e)
|
||||
{
|
||||
base.OnMessage(e);
|
||||
|
||||
if (e.IsText && e.Data.Length > 0 && Controller != null)
|
||||
{
|
||||
// Forward the message to the controller to be put on the receive queue
|
||||
Controller.HandleClientMessage(e.Data);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClose(CloseEventArgs e)
|
||||
{
|
||||
base.OnClose(e);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Closing: {0} reason: {1}", null, e.Code, e.Reason);
|
||||
}
|
||||
|
||||
protected override void OnError(ErrorEventArgs e)
|
||||
{
|
||||
base.OnError(e);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Error: {exception} message: {message}", e.Exception, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using System.Reflection;
|
||||
using Crestron.SimplSharpPro;
|
||||
@@ -28,7 +27,9 @@ namespace PepperDash.Essentials
|
||||
|
||||
public ControlSystem()
|
||||
: base()
|
||||
|
||||
{
|
||||
|
||||
Thread.MaxNumberOfUserThreads = 400;
|
||||
Global.ControlSystem = this;
|
||||
DeviceManager.Initialize(this);
|
||||
@@ -36,7 +37,7 @@ namespace PepperDash.Essentials
|
||||
SystemMonitor.ProgramInitialization.ProgramInitializationUnderUserControl = true;
|
||||
|
||||
Debug.SetErrorLogMinimumDebugLevel(CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? LogEventLevel.Warning : LogEventLevel.Verbose);
|
||||
|
||||
|
||||
// AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
|
||||
}
|
||||
|
||||
@@ -96,6 +97,9 @@ namespace PepperDash.Essentials
|
||||
|
||||
DeterminePlatform();
|
||||
|
||||
// Print .NET runtime version
|
||||
Debug.LogMessage(LogEventLevel.Information, "Running on .NET runtime version: {0}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
|
||||
|
||||
if (Debug.DoNotLoadConfigOnNextBoot)
|
||||
{
|
||||
CrestronConsole.AddNewConsoleCommand(s => CrestronInvoke.BeginInvoke((o) => GoWithLoad()), "go", "Loads configuration file",
|
||||
@@ -144,7 +148,7 @@ namespace PepperDash.Essentials
|
||||
CrestronConsole.AddNewConsoleCommand(DeviceManager.GetRoutingPorts,
|
||||
"getroutingports", "Reports all routing ports, if any. Requires a device key", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API"));
|
||||
//DeviceManager.AddDevice(new EssentialsWebApi("essentialsWebApi", "Essentials Web API"));
|
||||
|
||||
if (!Debug.DoNotLoadConfigOnNextBoot)
|
||||
{
|
||||
@@ -173,7 +177,7 @@ namespace PepperDash.Essentials
|
||||
|
||||
var dirSeparator = Global.DirectorySeparator;
|
||||
|
||||
string directoryPrefix;
|
||||
string directoryPrefix;
|
||||
|
||||
directoryPrefix = Directory.GetApplicationRootDirectory();
|
||||
|
||||
@@ -181,24 +185,10 @@ namespace PepperDash.Essentials
|
||||
|
||||
if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Server) // Handles 3-series running Windows CE OS
|
||||
{
|
||||
string userFolder;
|
||||
string nvramFolder;
|
||||
bool is4series = false;
|
||||
string userFolder = "user";
|
||||
string nvramFolder = "nvram";
|
||||
|
||||
if (eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4)) // Handle 4-series
|
||||
{
|
||||
is4series = true;
|
||||
// Set path to user/
|
||||
userFolder = "user";
|
||||
nvramFolder = "nvram";
|
||||
}
|
||||
else
|
||||
{
|
||||
userFolder = "User";
|
||||
nvramFolder = "Nvram";
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series");
|
||||
Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{version:l} on {processorSeries:l} Appliance", Global.AssemblyVersion, "4-series");
|
||||
//Debug.LogMessage(LogEventLevel.Information, "Starting Essentials v{0} on {1} Appliance", Global.AssemblyVersion, is4series ? "4-series" : "3-series");
|
||||
|
||||
// Check if User/ProgramX exists
|
||||
@@ -361,6 +351,7 @@ namespace PepperDash.Essentials
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
void Load()
|
||||
{
|
||||
LoadDevices();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.Fusion
|
||||
{
|
||||
public class EssentialsHuddleSpaceFusionSystemControllerBase
|
||||
{
|
||||
private ClientWebSocket _webSocket;
|
||||
|
||||
public EssentialsHuddleSpaceFusionSystemControllerBase()
|
||||
{
|
||||
_webSocket = new ClientWebSocket();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(Uri uri)
|
||||
{
|
||||
await _webSocket.ConnectAsync(uri, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendAsync(string message)
|
||||
{
|
||||
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
|
||||
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task<string> ReceiveAsync()
|
||||
{
|
||||
var buffer = new byte[1024];
|
||||
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
return System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
}
|
||||
|
||||
public async Task CloseAsync()
|
||||
{
|
||||
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,17 @@
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PepperDash.Essentials</RootNamespace>
|
||||
<AssemblyName>PepperDashEssentials</AssemblyName>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<OutputPath>$(ProjectDir)bin\$(Configuration)\</OutputPath>
|
||||
<Title>PepperDash Essentials</Title>
|
||||
<PackageId>PepperDashEssentials</PackageId>
|
||||
<InformationalVersion>$(Version)</InformationalVersion>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<ProjectType>Program</ProjectType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
@@ -47,7 +50,8 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user