Compare commits

...

14 Commits

Author SHA1 Message Date
Neil Dorin
7e3c62b303 fix: Update actions/checkout and actions/setup-dotnet versions for improved compatibility 2026-04-08 15:54:15 -06:00
Neil Dorin
884b2c2e6e feat: Enhance documentation for MicrophonePrivacyController and its factory with detailed summaries and parameter descriptions 2026-04-08 15:49:38 -06:00
Neil Dorin
daf9b4bda0 refactor: Change CustomActivate and Initialize methods to protected access in multiple classes for better inheritance control 2026-04-08 15:47:58 -06:00
Neil Dorin
e818c9ca03 fix: Downgrade setup-dotnet action version to v5 for compatibility 2026-04-08 15:24:13 -06:00
Neil Dorin
5dd6d18fcc feat: Update CI workflow and project files for improved testing and documentation generation 2026-04-08 15:23:49 -06:00
Neil Dorin
37961b9eac feat: Enhance device testing and environment handling with new interfaces and fakes 2026-04-08 13:21:15 -06:00
Neil Dorin
24df4e7a03 feat: Add unit tests and fakes for Crestron environment and data store
- Introduced `FakeCrestronEnvironment` and `FakeEthernetHelper` for testing purposes.
- Implemented `InMemoryCrestronDataStore` to facilitate unit tests for data storage.
- Created `DebugServiceTests` to validate the behavior of debug-related services.
- Added project files for unit tests targeting .NET 9.0 with necessary dependencies.
- Developed production adapters (`CrestronConsoleAdapter`, `CrestronDataStoreAdapter`, `CrestronEnvironmentAdapter`, `CrestronEthernetAdapter`) to interface with the Crestron SDK.
- Updated `Debug` class to utilize injected service abstractions instead of direct SDK calls.
- Enhanced `ControlSystem` initialization to register service adapters before usage.
2026-04-08 12:41:15 -06:00
Neil Dorin
bfb9838743 refactor: Rename logging level switch variables for clarity and add fallback handler to WebApiServer to allow for serving debug app 2026-04-08 11:51:10 -06:00
Neil Dorin
c98b48ff87 refactor: Clean up and streamline Debug and WebApiServer classes for improved readability and performance 2026-04-07 15:53:00 -06:00
Neil Dorin
5c26438a1c refactor: Remove DebugWebsocketSink.cs to streamline the codebase 2026-03-31 19:29:58 -06:00
Neil Dorin
7bec96e68b refactor: Remove obsolete interfaces and classes to streamline the codebase 2026-03-30 12:11:21 -06:00
Neil Dorin
7076eafc21 Refator: Refactor timer implementation across multiple classes to use System.Timers.Timer instead of CTimer for improved consistency and performance.
- Updated RelayControlledShade to utilize Timer for output pulsing.
- Refactored MockVC to replace CTimer with Timer for call status simulation.
- Modified VideoCodecBase to enhance documentation and improve feedback handling.
- Removed obsolete IHasCamerasMessenger and updated related classes to use IHasCamerasWithControls.
- Adjusted PressAndHoldHandler to implement Timer for button hold actions.
- Enhanced logging throughout MobileControl and RoomBridges for better debugging and information tracking.
- Cleaned up unnecessary comments and improved exception handling in various classes.
2026-03-30 11:44:15 -06:00
Neil Dorin
b4d53dbe0e refactor: routing interfaces and implementations to support current sources
- Updated ICurrentSources interface to use IRoutingSource instead of SourceListItem.
- Introduced CurrentSourcesChangedEventArgs for detailed event notifications.
- Modified IRoutingOutputs and IRoutingSource interfaces to include JSON properties for serialization.
- Enhanced IRoutingSink and related interfaces to implement ICurrentSources for better source management.
- Refactored RoutingFeedbackManager to utilize new current source handling.
- Updated GenericAudioOut, GenericSink, and BlueJeansPc classes to implement new current source logic.
- Adjusted MobileControl messengers to accommodate changes in current source handling.
- Removed deprecated destination handling in MobileControlEssentialsRoomBridge.
2026-03-26 13:49:23 -06:00
Neil Dorin
43a9661e08 Refactor: Removing unused classes that reference Crestron HTTP classes
- Removed the HttpLogoServer class and its related functionality from ControlSystem.
- Updated ControlSystem to eliminate references to the logo server, including initialization and device checks.
- Cleaned up unused variables and methods related to logo server handling.
2026-03-23 11:20:44 -06:00
135 changed files with 4067 additions and 5398 deletions

View File

@@ -6,9 +6,13 @@ on:
- '**'
jobs:
runTests:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-run-tests.yml@main
secrets: inherit
getVersion:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main
secrets: inherit
needs: runTests
build-4Series:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main
secrets: inherit

View File

@@ -1,247 +0,0 @@
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 "=================================="

View File

@@ -26,9 +26,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Dotnet Setup
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 8.x

View File

@@ -36,49 +36,151 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core.Tests", "src\PepperDash.Core.Tests\PepperDash.Core.Tests.csproj", "{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
Debug 4.7.2|x64 = Debug 4.7.2|x64
Debug 4.7.2|x86 = Debug 4.7.2|x86
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.ActiveCfg = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.Build.0 = Debug|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.Build.0 = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.ActiveCfg = Release|Any CPU
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.ActiveCfg = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.Build.0 = Debug|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.Build.0 = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.ActiveCfg = Release|Any CPU
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.Build.0 = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.Build.0 = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.ActiveCfg = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.Build.0 = Debug|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.Build.0 = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.ActiveCfg = Release|Any CPU
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.Build.0 = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.ActiveCfg = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.Build.0 = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.Build.0 = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.ActiveCfg = Debug|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.ActiveCfg = Release|Any CPU
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.ActiveCfg = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.ActiveCfg = Debug|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -90,6 +192,8 @@ Global
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}

View File

@@ -32,7 +32,7 @@ If the `CustomActivate()` method is long, consider breaking it up into many smal
Note: It is best-practice in Essentials to not write arbitrarily-timed startup sequences to ensure that a "system" or room is functional. Rather, we encourage the developer to use various properties and conditions on devices to aggregate together "room is ready" statuses that can trigger further action. This ensures that all devices can be up and alive, allowing them to be debugged within a program that may otherwise be misbehaving - as well as not making users and expensive developers wait for code to start up!
```cs
public override bool CustomActivate()
protected override bool CustomActivate()
{
Debug.Console(0, this, "Final activation. Setting up actions and feedbacks");
SetupFunctions();
@@ -52,7 +52,7 @@ We can see in the example below that during the `CustomActivate()` phase, we def
### **Example**
```cs
public override bool CustomActivate()
protected override bool CustomActivate()
{
foreach (var i in Config.Inputs)
{
@@ -115,7 +115,7 @@ The main task that should be undertaken in the `Initialize()` method for any 3rd
### Example (from `PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.CiscoSparkCodec`)
```cs
public override void Initialize()
protected override void Initialize()
{
var socket = Communication as ISocketStatus;
if (socket != null)

View File

@@ -0,0 +1,36 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Allows pre-registration of Crestron service implementations before the <c>Debug</c>
/// static class initialises. Call <see cref="Register"/> from the composition root
/// (e.g. ControlSystem constructor) <em>before</em> any code touches <c>Debug.*</c>.
/// Test projects should call it with no-op / in-memory implementations so that the
/// <c>Debug</c> static constructor never tries to reach the real Crestron SDK.
/// </summary>
public static class DebugServiceRegistration
{
/// <summary>Gets the registered environment abstraction, or <c>null</c> if not registered.</summary>
public static ICrestronEnvironment? Environment { get; private set; }
/// <summary>Gets the registered console abstraction, or <c>null</c> if not registered.</summary>
public static ICrestronConsole? Console { get; private set; }
/// <summary>Gets the registered data-store abstraction, or <c>null</c> if not registered.</summary>
public static ICrestronDataStore? DataStore { get; private set; }
/// <summary>
/// Registers the service implementations that <c>Debug</c> will use when its
/// static constructor runs. Any parameter may be <c>null</c> to leave the
/// corresponding service unregistered (the <c>Debug</c> class will skip that
/// capability gracefully).
/// </summary>
public static void Register(
ICrestronEnvironment? environment,
ICrestronConsole? console,
ICrestronDataStore? dataStore)
{
Environment = environment;
Console = console;
DataStore = dataStore;
}
}

View File

@@ -0,0 +1,100 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Mirrors Crestron's <c>eDevicePlatform</c> without requiring the Crestron SDK.
/// </summary>
public enum DevicePlatform
{
/// <summary>Hardware appliance (e.g. CP4, MC4).</summary>
Appliance,
/// <summary>Crestron Virtual Control / server runtime.</summary>
Server,
}
/// <summary>
/// Mirrors Crestron's <c>eRuntimeEnvironment</c>.
/// </summary>
public enum RuntimeEnvironment
{
/// <summary>SimplSharpPro program slot (hardware 4-series).</summary>
SimplSharpPro,
/// <summary>SimplSharp (older 3-series or server environments).</summary>
SimplSharp,
/// <summary>Any other environment — check for completeness.</summary>
Other,
}
/// <summary>
/// Mirrors Crestron's <c>ConsoleAccessLevelEnum</c>.
/// </summary>
public enum ConsoleAccessLevel
{
AccessAdministrator = 0,
AccessOperator = 1,
AccessProgrammer = 2,
}
/// <summary>
/// Mirrors Crestron's <c>eProgramStatusEventType</c>.
/// </summary>
public enum ProgramStatusEventType
{
Starting,
Stopping,
Paused,
Resumed,
}
/// <summary>
/// Mirrors the event type used by Crestron's <c>EthernetEventArgs</c>.
/// </summary>
public enum EthernetEventType
{
LinkDown = 0,
LinkUp = 1,
}
/// <summary>
/// Event args for Crestron ethernet link events.
/// </summary>
public class PepperDashEthernetEventArgs : EventArgs
{
public EthernetEventType EthernetEventType { get; }
public short EthernetAdapter { get; }
public PepperDashEthernetEventArgs(EthernetEventType eventType, short adapter)
{
EthernetEventType = eventType;
EthernetAdapter = adapter;
}
}
/// <summary>
/// Mirrors the set of <c>CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET</c> values
/// used across this codebase — does not aim to be exhaustive.
/// </summary>
public enum EthernetParameterType
{
GetCurrentIpAddress,
GetHostname,
GetDomainName,
GetLinkStatus,
GetCurrentDhcpState,
GetCurrentIpMask,
GetCurrentRouter,
GetMacAddress,
GetDnsServer,
}
/// <summary>
/// Mirrors Crestron's <c>SocketStatus</c> without requiring the Crestron SDK.
/// </summary>
public enum PepperDashSocketStatus
{
SocketNotConnected = 0,
SocketConnected = 2,
SocketConnectionInProgress = 6,
SocketConnectFailed = 11,
SocketDisconnecting = 12,
SocketBrokenRemotely = 7,
}

View File

@@ -0,0 +1,32 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronConsole</c> to allow unit testing
/// without the Crestron SDK.
/// </summary>
public interface ICrestronConsole
{
/// <summary>Prints a line to the Crestron console/telnet output.</summary>
void PrintLine(string message);
/// <summary>Prints text (without newline) to the Crestron console/telnet output.</summary>
void Print(string message);
/// <summary>
/// Sends a response string to the console for the currently-executing console command.
/// </summary>
void ConsoleCommandResponse(string message);
/// <summary>
/// Registers a new command with the Crestron console.
/// </summary>
/// <param name="callback">Handler invoked when the command is typed.</param>
/// <param name="command">Command name (no spaces).</param>
/// <param name="helpText">Help text shown by the Crestron console.</param>
/// <param name="accessLevel">Minimum access level required to run the command.</param>
void AddNewConsoleCommand(
Action<string> callback,
string command,
string helpText,
ConsoleAccessLevel accessLevel);
}

View File

@@ -0,0 +1,31 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronDataStore.CrestronDataStoreStatic</c>
/// to allow unit testing without the Crestron SDK.
/// </summary>
public interface ICrestronDataStore
{
/// <summary>Initialises the data store. Must be called once before any other operation.</summary>
void InitStore();
/// <summary>Reads an integer value from the local (program-slot) store.</summary>
/// <returns><c>true</c> if the value was found and read successfully.</returns>
bool TryGetLocalInt(string key, out int value);
/// <summary>Writes an integer value to the local (program-slot) store.</summary>
/// <returns><c>true</c> on success.</returns>
bool SetLocalInt(string key, int value);
/// <summary>Writes an unsigned integer value to the local (program-slot) store.</summary>
/// <returns><c>true</c> on success.</returns>
bool SetLocalUint(string key, uint value);
/// <summary>Reads a boolean value from the local (program-slot) store.</summary>
/// <returns><c>true</c> if the value was found and read successfully.</returns>
bool TryGetLocalBool(string key, out bool value);
/// <summary>Writes a boolean value to the local (program-slot) store.</summary>
/// <returns><c>true</c> on success.</returns>
bool SetLocalBool(string key, bool value);
}

View File

@@ -0,0 +1,52 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronEnvironment</c> to allow unit testing
/// without the Crestron SDK.
/// </summary>
public interface ICrestronEnvironment
{
/// <summary>Gets the platform the program is executing on.</summary>
DevicePlatform DevicePlatform { get; }
/// <summary>Gets the current runtime environment.</summary>
RuntimeEnvironment RuntimeEnvironment { get; }
/// <summary>Gets the platform-appropriate newline string.</summary>
string NewLine { get; }
/// <summary>Gets the application number (program slot).</summary>
uint ApplicationNumber { get; }
/// <summary>Gets the room ID (used in Crestron Virtual Control / server environments).</summary>
uint RoomId { get; }
/// <summary>Raised when program status changes (starting, stopping, etc.).</summary>
event EventHandler<ProgramStatusEventArgs> ProgramStatusChanged;
/// <summary>Raised when the ethernet link changes state.</summary>
event EventHandler<PepperDashEthernetEventArgs> EthernetEventReceived;
/// <summary>Gets the application root directory path.</summary>
string GetApplicationRootDirectory();
/// <summary>
/// Returns <c>true</c> when running on real Crestron hardware.
/// Returns <c>false</c> in test / dev environments so that SDK-specific
/// sinks and enrichers can be safely skipped.
/// </summary>
bool IsHardwareRuntime { get; }
}
/// <summary>
/// Event args for <see cref="ICrestronEnvironment.ProgramStatusChanged"/>.
/// </summary>
public class ProgramStatusEventArgs : EventArgs
{
public ProgramStatusEventType EventType { get; }
public ProgramStatusEventArgs(ProgramStatusEventType eventType)
{
EventType = eventType;
}
}

View File

@@ -0,0 +1,16 @@
namespace PepperDash.Core.Abstractions;
/// <summary>
/// Abstracts <c>Crestron.SimplSharp.CrestronEthernetHelper</c> to allow unit testing
/// without the Crestron SDK.
/// </summary>
public interface IEthernetHelper
{
/// <summary>
/// Returns a network parameter string for the specified adapter.
/// </summary>
/// <param name="parameter">The parameter to retrieve.</param>
/// <param name="ethernetAdapterId">Ethernet adapter index (0 = LAN A).</param>
/// <returns>String value of the requested parameter.</returns>
string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId);
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>PepperDash.Core.Abstractions</RootNamespace>
<AssemblyName>PepperDash.Core.Abstractions</AssemblyName>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Deterministic>true</Deterministic>
<NeutralLanguage>en</NeutralLanguage>
<Title>PepperDash Core Abstractions</Title>
<Company>PepperDash Technologies</Company>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/PepperDash/PepperDashCore</RepositoryUrl>
<NullableContextOptions>enable</NullableContextOptions>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<InformationalVersion>$(Version)</InformationalVersion>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<!-- No Crestron SDK reference — this project must remain hardware-agnostic so test projects can reference it -->
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,323 @@
using FluentAssertions;
using PepperDash.Core;
using Xunit;
namespace PepperDash.Core.Tests.Devices;
/// <summary>
/// Tests for <see cref="Device"/> — the base class for all PepperDash devices.
/// These run without Crestron hardware; Debug is initialized with fakes via TestInitializer.
/// </summary>
public class DeviceTests
{
// -----------------------------------------------------------------------
// Construction
// -----------------------------------------------------------------------
[Fact]
public void Constructor_SingleArg_SetsKey()
{
var device = new Device("my-device");
device.Key.Should().Be("my-device");
}
[Fact]
public void Constructor_SingleArg_SetsNameToEmpty()
{
var device = new Device("my-device");
device.Name.Should().BeEmpty();
}
[Fact]
public void Constructor_TwoArg_SetsKeyAndName()
{
var device = new Device("my-device", "My Device");
device.Key.Should().Be("my-device");
device.Name.Should().Be("My Device");
}
[Fact]
public void Constructor_KeyWithDot_StillSetsKey()
{
// The dot triggers a debug log warning but must not prevent construction.
var device = new Device("parent.child");
device.Key.Should().Be("parent.child");
}
// -----------------------------------------------------------------------
// ToString
// -----------------------------------------------------------------------
[Fact]
public void ToString_WithName_FormatsKeyDashName()
{
var device = new Device("cam-01", "Front Camera");
device.ToString().Should().Be("cam-01 - Front Camera");
}
[Fact]
public void ToString_WithoutName_UsesDashPlaceholder()
{
var device = new Device("cam-01");
device.ToString().Should().Be("cam-01 - ---");
}
// -----------------------------------------------------------------------
// DefaultDevice
// -----------------------------------------------------------------------
[Fact]
public void DefaultDevice_IsNotNull()
{
Device.DefaultDevice.Should().NotBeNull();
}
[Fact]
public void DefaultDevice_HasKeyDefault()
{
Device.DefaultDevice.Key.Should().Be("Default");
}
// -----------------------------------------------------------------------
// CustomActivate / Activate / Deactivate / Initialize
// -----------------------------------------------------------------------
[Fact]
public void CustomActivate_DefaultReturnTrue()
{
var device = new TestDevice("d1");
device.CallCustomActivate().Should().BeTrue();
}
[Fact]
public void Deactivate_DefaultReturnsTrue()
{
var device = new Device("d1");
device.Deactivate().Should().BeTrue();
}
[Fact]
public void Activate_CallsCustomActivate_AndReturnsItsResult()
{
var stub = new ActivateTrackingDevice("d1", result: false);
stub.Activate().Should().BeFalse();
stub.CustomActivateCalled.Should().BeTrue();
}
[Fact]
public void Activate_TrueWhenCustomActivateReturnsTrue()
{
var stub = new ActivateTrackingDevice("d1", result: true);
stub.Activate().Should().BeTrue();
}
[Fact]
public void Initialize_DoesNotThrow()
{
var device = new TestDevice("d1");
var act = () => device.CallInitialize();
act.Should().NotThrow();
}
// -----------------------------------------------------------------------
// PreActivate
// -----------------------------------------------------------------------
[Fact]
public void PreActivate_NoActions_DoesNotThrow()
{
var device = new TestDevice("d1");
var act = () => device.PreActivate();
act.Should().NotThrow();
}
[Fact]
public void PreActivate_RunsRegisteredActionsInOrder()
{
var device = new TestDevice("d1");
var order = new List<int>();
device.AddPreActivationAction(() => order.Add(1));
device.AddPreActivationAction(() => order.Add(2));
device.AddPreActivationAction(() => order.Add(3));
device.PreActivate();
order.Should().Equal(1, 2, 3);
}
[Fact]
public void PreActivate_ContinuesAfterFaultingAction()
{
var device = new TestDevice("d1");
var reached = false;
device.AddPreActivationAction(() => throw new InvalidOperationException("boom"));
device.AddPreActivationAction(() => reached = true);
var act = () => device.PreActivate();
act.Should().NotThrow("exceptions in individual actions must be caught internally");
reached.Should().BeTrue("actions after a faulting action must still run");
}
// -----------------------------------------------------------------------
// PostActivate
// -----------------------------------------------------------------------
[Fact]
public void PostActivate_NoActions_DoesNotThrow()
{
var device = new TestDevice("d1");
var act = () => device.PostActivate();
act.Should().NotThrow();
}
[Fact]
public void PostActivate_RunsRegisteredActionsInOrder()
{
var device = new TestDevice("d1");
var order = new List<int>();
device.AddPostActivationAction(() => order.Add(1));
device.AddPostActivationAction(() => order.Add(2));
device.PostActivate();
order.Should().Equal(1, 2);
}
[Fact]
public void PostActivate_ContinuesAfterFaultingAction()
{
var device = new TestDevice("d1");
var reached = false;
device.AddPostActivationAction(() => throw new Exception("boom"));
device.AddPostActivationAction(() => reached = true);
var act = () => device.PostActivate();
act.Should().NotThrow();
reached.Should().BeTrue();
}
// -----------------------------------------------------------------------
// Pre and Post actions are independent lists
// -----------------------------------------------------------------------
[Fact]
public void PreActivationActions_DoNotRunOnPostActivate()
{
var device = new TestDevice("d1");
var preRan = false;
device.AddPreActivationAction(() => preRan = true);
device.PostActivate();
preRan.Should().BeFalse();
}
[Fact]
public void PostActivationActions_DoNotRunOnPreActivate()
{
var device = new TestDevice("d1");
var postRan = false;
device.AddPostActivationAction(() => postRan = true);
device.PreActivate();
postRan.Should().BeFalse();
}
// -----------------------------------------------------------------------
// OnFalse
// -----------------------------------------------------------------------
[Fact]
public void OnFalse_FiresAction_WhenBoolIsFalse()
{
var device = new Device("d1");
var fired = false;
device.OnFalse(false, () => fired = true);
fired.Should().BeTrue();
}
[Fact]
public void OnFalse_DoesNotFireAction_WhenBoolIsTrue()
{
var device = new Device("d1");
var fired = false;
device.OnFalse(true, () => fired = true);
fired.Should().BeFalse();
}
[Fact]
public void OnFalse_DoesNotFireAction_ForNonBoolType()
{
var device = new Device("d1");
var fired = false;
device.OnFalse("not a bool", () => fired = true);
device.OnFalse(0, () => fired = true);
device.OnFalse(null!, () => fired = true);
fired.Should().BeFalse();
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
/// <summary>
/// Exposes protected Device members so test methods can call them directly.
/// </summary>
private class TestDevice : Device
{
public TestDevice(string key) : base(key) { }
public TestDevice(string key, string name) : base(key, name) { }
public void AddPreActivationAction(Action act) => base.AddPreActivationAction(act);
public void AddPostActivationAction(Action act) => base.AddPostActivationAction(act);
public bool CallCustomActivate() => base.CustomActivate();
public void CallInitialize() => base.Initialize();
}
/// <summary>
/// Records whether CustomActivate was invoked and returns a configured result.
/// Used to verify Activate() correctly delegates to CustomActivate().
/// </summary>
private sealed class ActivateTrackingDevice : Device
{
private readonly bool _result;
public bool CustomActivateCalled { get; private set; }
public ActivateTrackingDevice(string key, bool result = true) : base(key)
{
_result = result;
}
protected override bool CustomActivate()
{
CustomActivateCalled = true;
return _result;
}
}
}

View File

@@ -0,0 +1,38 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// No-op ICrestronConsole that captures output for test assertions.
/// </summary>
public class CapturingCrestronConsole : ICrestronConsole
{
public List<string> Lines { get; } = new();
public List<string> CommandResponses { get; } = new();
public List<(string Command, string HelpText)> RegisteredCommands { get; } = new();
public void PrintLine(string message) => Lines.Add(message);
public void Print(string message) => Lines.Add(message);
public void ConsoleCommandResponse(string message) => CommandResponses.Add(message);
public void AddNewConsoleCommand(
Action<string> callback,
string command,
string helpText,
ConsoleAccessLevel accessLevel)
{
RegisteredCommands.Add((command, helpText));
}
}
/// <summary>
/// Minimal no-op ICrestronConsole that discards all output. Useful when you only
/// care about the system under test and not what it logs.
/// </summary>
public class NoOpCrestronConsole : ICrestronConsole
{
public void PrintLine(string message) { }
public void Print(string message) { }
public void ConsoleCommandResponse(string message) { }
public void AddNewConsoleCommand(Action<string> _, string __, string ___, ConsoleAccessLevel ____) { }
}

View File

@@ -0,0 +1,49 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// Configurable ICrestronEnvironment for unit tests.
/// Defaults: Appliance / SimplSharpPro / ApplicationNumber=1.
/// </summary>
public class FakeCrestronEnvironment : ICrestronEnvironment
{
public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance;
public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro;
public string NewLine { get; set; } = "\r\n";
public uint ApplicationNumber { get; set; } = 1;
public uint RoomId { get; set; } = 0;
public event EventHandler<ProgramStatusEventArgs>? ProgramStatusChanged;
public event EventHandler<PepperDashEthernetEventArgs>? EthernetEventReceived;
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
/// <inheritdoc/>
public bool IsHardwareRuntime => false;
/// <summary>Simulates a program status event for tests.</summary>
public void RaiseProgramStatus(ProgramStatusEventType type) =>
ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type));
/// <summary>Simulates an ethernet event for tests.</summary>
public void RaiseEthernetEvent(EthernetEventType type, short adapter = 0) =>
EthernetEventReceived?.Invoke(this, new PepperDashEthernetEventArgs(type, adapter));
}
/// <summary>
/// No-op IEthernetHelper that returns configurable values.
/// </summary>
public class FakeEthernetHelper : IEthernetHelper
{
private readonly Dictionary<EthernetParameterType, string> _values = new();
public FakeEthernetHelper Seed(EthernetParameterType param, string value)
{
_values[param] = value;
return this;
}
public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) =>
_values.TryGetValue(parameter, out var v) ? v : string.Empty;
}

View File

@@ -0,0 +1,59 @@
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Tests.Fakes;
/// <summary>
/// In-memory ICrestronDataStore backed by a dictionary.
/// Use in unit tests to verify that keys are read from and written to the store correctly.
/// </summary>
public class InMemoryCrestronDataStore : ICrestronDataStore
{
private readonly Dictionary<string, object> _store = new();
public bool Initialized { get; private set; }
public void InitStore() => Initialized = true;
public bool TryGetLocalInt(string key, out int value)
{
if (_store.TryGetValue(key, out var raw) && raw is int i)
{
value = i;
return true;
}
value = 0;
return false;
}
public bool SetLocalInt(string key, int value)
{
_store[key] = value;
return true;
}
public bool SetLocalUint(string key, uint value)
{
_store[key] = (int)value;
return true;
}
public bool TryGetLocalBool(string key, out bool value)
{
if (_store.TryGetValue(key, out var raw) && raw is bool b)
{
value = b;
return true;
}
value = false;
return false;
}
public bool SetLocalBool(string key, bool value)
{
_store[key] = value;
return true;
}
/// <summary>Seeds a key for testing read paths.</summary>
public void Seed(string key, object value) => _store[key] = value;
}

View File

@@ -0,0 +1,171 @@
using FluentAssertions;
using PepperDash.Core.Abstractions;
using PepperDash.Core.Tests.Fakes;
using Xunit;
namespace PepperDash.Core.Tests.Logging;
/// <summary>
/// Tests for Debug-related service interfaces and implementations.
/// These tests verify the behaviour of the abstractions in isolation (no Crestron SDK required).
/// </summary>
public class DebugServiceTests
{
// -----------------------------------------------------------------------
// ICrestronDataStore — InMemoryCrestronDataStore
// -----------------------------------------------------------------------
[Fact]
public void DataStore_InitStore_SetsInitializedFlag()
{
var store = new InMemoryCrestronDataStore();
store.Initialized.Should().BeFalse("not yet initialized");
store.InitStore();
store.Initialized.Should().BeTrue();
}
[Fact]
public void DataStore_SetAndGetLocalInt_RoundTrips()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalInt("MyKey", 42).Should().BeTrue();
store.TryGetLocalInt("MyKey", out var value).Should().BeTrue();
value.Should().Be(42);
}
[Fact]
public void DataStore_TryGetLocalInt_ReturnsFalse_WhenKeyAbsent()
{
var store = new InMemoryCrestronDataStore();
store.TryGetLocalInt("Missing", out var value).Should().BeFalse();
value.Should().Be(0);
}
[Fact]
public void DataStore_SetAndGetLocalBool_RoundTrips()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalBool("FlagKey", true).Should().BeTrue();
store.TryGetLocalBool("FlagKey", out var value).Should().BeTrue();
value.Should().BeTrue();
}
[Fact]
public void DataStore_TryGetLocalBool_ReturnsFalse_WhenKeyAbsent()
{
var store = new InMemoryCrestronDataStore();
store.TryGetLocalBool("Missing", out var value).Should().BeFalse();
value.Should().BeFalse();
}
[Fact]
public void DataStore_SetLocalUint_CanBeReadBackAsInt()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalUint("UintKey", 3u).Should().BeTrue();
store.TryGetLocalInt("UintKey", out var value).Should().BeTrue();
value.Should().Be(3);
}
[Fact]
public void DataStore_Seed_AllowsTestSetupOfReadPaths()
{
var store = new InMemoryCrestronDataStore();
store.Seed("MyLevel", 2);
store.TryGetLocalInt("MyLevel", out var level).Should().BeTrue();
level.Should().Be(2);
}
// -----------------------------------------------------------------------
// DebugServiceRegistration
// -----------------------------------------------------------------------
[Fact]
public void ServiceRegistration_Register_StoresAllThreeServices()
{
var env = new FakeCrestronEnvironment();
var console = new NoOpCrestronConsole();
var store = new InMemoryCrestronDataStore();
DebugServiceRegistration.Register(env, console, store);
DebugServiceRegistration.Environment.Should().BeSameAs(env);
DebugServiceRegistration.Console.Should().BeSameAs(console);
DebugServiceRegistration.DataStore.Should().BeSameAs(store);
}
[Fact]
public void ServiceRegistration_Register_AcceptsNullsWithoutThrowing()
{
var act = () => DebugServiceRegistration.Register(null, null, null);
act.Should().NotThrow();
}
// -----------------------------------------------------------------------
// ICrestronEnvironment — FakeCrestronEnvironment
// -----------------------------------------------------------------------
[Fact]
public void FakeEnvironment_DefaultsToAppliance()
{
var env = new FakeCrestronEnvironment();
env.DevicePlatform.Should().Be(DevicePlatform.Appliance);
}
[Fact]
public void FakeEnvironment_RaiseProgramStatus_FiresEvent()
{
var env = new FakeCrestronEnvironment();
ProgramStatusEventType? received = null;
env.ProgramStatusChanged += (_, e) => received = e.EventType;
env.RaiseProgramStatus(ProgramStatusEventType.Stopping);
received.Should().Be(ProgramStatusEventType.Stopping);
}
[Fact]
public void FakeEnvironment_RaiseEthernetEvent_FiresEvent()
{
var env = new FakeCrestronEnvironment();
EthernetEventType? received = null;
env.EthernetEventReceived += (_, e) => received = e.EthernetEventType;
env.RaiseEthernetEvent(EthernetEventType.LinkUp, adapter: 0);
received.Should().Be(EthernetEventType.LinkUp);
}
// -----------------------------------------------------------------------
// ICrestronConsole — CapturingCrestronConsole
// -----------------------------------------------------------------------
[Fact]
public void CapturingConsole_PrintLine_CapturesMessage()
{
var console = new CapturingCrestronConsole();
console.PrintLine("hello world");
console.Lines.Should().ContainSingle().Which.Should().Be("hello world");
}
[Fact]
public void CapturingConsole_AddNewConsoleCommand_RecordsCommandName()
{
var console = new CapturingCrestronConsole();
console.AddNewConsoleCommand(_ => { }, "appdebug", "Sets debug level", ConsoleAccessLevel.AccessOperator);
console.RegisteredCommands.Should().ContainSingle()
.Which.Command.Should().Be("appdebug");
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
</ItemGroup>
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
<!-- PepperDash.Core is referenced so we can test Device and other concrete types.
DebugServiceRegistration.Register() is called via a [ModuleInitializer] before any
test runs, so Debug's static constructor uses fakes and never touches the Crestron SDK. -->
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
using System.Runtime.CompilerServices;
using PepperDash.Core.Abstractions;
using PepperDash.Core.Tests.Fakes;
namespace PepperDash.Core.Tests;
/// <summary>
/// Runs once before any type in this assembly is accessed.
/// Registers fake Crestron service implementations with <see cref="DebugServiceRegistration"/>
/// so that the <c>Debug</c> static constructor uses them instead of the real Crestron SDK.
/// This must remain a module initializer (not a test fixture) because the static constructor
/// fires the first time <em>any</em> type in PepperDash.Core is referenced — before xUnit
/// has a chance to run fixture setup code.
/// </summary>
internal static class TestInitializer
{
[ModuleInitializer]
internal static void Initialize()
{
DebugServiceRegistration.Register(
new FakeCrestronEnvironment
{
DevicePlatform = DevicePlatform.Server, // avoids any appliance-only code paths
RuntimeEnvironment = RuntimeEnvironment.Other, // skips console command registration
},
new NoOpCrestronConsole(),
new InMemoryCrestronDataStore());
}
}

View File

@@ -0,0 +1,35 @@
using System;
using Crestron.SimplSharp;
using PdCore = PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates ICrestronConsole calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronConsoleAdapter : PdCore.ICrestronConsole
{
public void PrintLine(string message) => CrestronConsole.PrintLine(message);
public void Print(string message) => CrestronConsole.Print(message);
public void ConsoleCommandResponse(string message) =>
CrestronConsole.ConsoleCommandResponse(message);
public void AddNewConsoleCommand(
Action<string> callback,
string command,
string helpText,
PdCore.ConsoleAccessLevel accessLevel)
{
var crestronLevel = accessLevel switch
{
PdCore.ConsoleAccessLevel.AccessAdministrator => ConsoleAccessLevelEnum.AccessAdministrator,
PdCore.ConsoleAccessLevel.AccessProgrammer => ConsoleAccessLevelEnum.AccessProgrammer,
_ => ConsoleAccessLevelEnum.AccessOperator,
};
// Wrap Action<string> in a lambda — Crestron's delegate is not a standard Action<string>.
CrestronConsole.AddNewConsoleCommand(s => callback(s), command, helpText, crestronLevel);
}
}

View File

@@ -0,0 +1,42 @@
using Crestron.SimplSharp.CrestronDataStore;
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates ICrestronDataStore calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronDataStoreAdapter : ICrestronDataStore
{
public void InitStore() => CrestronDataStoreStatic.InitCrestronDataStore();
public bool TryGetLocalInt(string key, out int value)
{
var err = CrestronDataStoreStatic.GetLocalIntValue(key, out value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool SetLocalInt(string key, int value)
{
var err = CrestronDataStoreStatic.SetLocalIntValue(key, value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool SetLocalUint(string key, uint value)
{
var err = CrestronDataStoreStatic.SetLocalUintValue(key, value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool TryGetLocalBool(string key, out bool value)
{
var err = CrestronDataStoreStatic.GetLocalBoolValue(key, out value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
public bool SetLocalBool(string key, bool value)
{
var err = CrestronDataStoreStatic.SetLocalBoolValue(key, value);
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
}
}

View File

@@ -0,0 +1,71 @@
using System;
using Crestron.SimplSharp;
using PdCore = PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates ICrestronEnvironment calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment
{
// Subscribe once in constructor and re-raise as our event types.
private event EventHandler<PdCore.ProgramStatusEventArgs>? _programStatusChanged;
private event EventHandler<PdCore.PepperDashEthernetEventArgs>? _ethernetEventReceived;
public CrestronEnvironmentAdapter()
{
CrestronEnvironment.ProgramStatusEventHandler += type =>
_programStatusChanged?.Invoke(this, new PdCore.ProgramStatusEventArgs(MapProgramStatus(type)));
CrestronEnvironment.EthernetEventHandler += args =>
_ethernetEventReceived?.Invoke(this, new PdCore.PepperDashEthernetEventArgs(
args.EthernetEventType == eEthernetEventType.LinkDown
? PdCore.EthernetEventType.LinkDown
: PdCore.EthernetEventType.LinkUp,
(short)args.EthernetAdapter));
}
public PdCore.DevicePlatform DevicePlatform =>
CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
? PdCore.DevicePlatform.Appliance
: PdCore.DevicePlatform.Server;
public PdCore.RuntimeEnvironment RuntimeEnvironment =>
CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro
? PdCore.RuntimeEnvironment.SimplSharpPro
: PdCore.RuntimeEnvironment.Other;
public string NewLine => CrestronEnvironment.NewLine;
public uint ApplicationNumber => InitialParametersClass.ApplicationNumber;
public uint RoomId => uint.TryParse(InitialParametersClass.RoomId, out var r) ? r : 0;
public event EventHandler<PdCore.ProgramStatusEventArgs> ProgramStatusChanged
{
add => _programStatusChanged += value;
remove => _programStatusChanged -= value;
}
public event EventHandler<PdCore.PepperDashEthernetEventArgs> EthernetEventReceived
{
add => _ethernetEventReceived += value;
remove => _ethernetEventReceived -= value;
}
public string GetApplicationRootDirectory() =>
Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory();
/// <inheritdoc/>
public bool IsHardwareRuntime => true;
private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) =>
type switch
{
eProgramStatusEventType.Stopping => PdCore.ProgramStatusEventType.Stopping,
eProgramStatusEventType.Paused => PdCore.ProgramStatusEventType.Paused,
eProgramStatusEventType.Resumed => PdCore.ProgramStatusEventType.Resumed,
_ => PdCore.ProgramStatusEventType.Starting,
};
}

View File

@@ -0,0 +1,39 @@
using System;
using Crestron.SimplSharp;
using PepperDash.Core.Abstractions;
namespace PepperDash.Core.Adapters;
/// <summary>
/// Production adapter — delegates IEthernetHelper calls to the real Crestron SDK.
/// </summary>
public sealed class CrestronEthernetAdapter : IEthernetHelper
{
public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId)
{
var crestronParam = parameter switch
{
EthernetParameterType.GetCurrentIpAddress =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS,
EthernetParameterType.GetHostname =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME,
EthernetParameterType.GetDomainName =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME,
EthernetParameterType.GetLinkStatus =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS,
EthernetParameterType.GetCurrentDhcpState =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE,
EthernetParameterType.GetCurrentIpMask =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK,
EthernetParameterType.GetCurrentRouter =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER,
EthernetParameterType.GetMacAddress =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS,
EthernetParameterType.GetDnsServer =>
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER,
_ => throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null),
};
return CrestronEthernetHelper.GetEthernetParameter(crestronParam, ethernetAdapterId);
}
}

View File

@@ -3,8 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -183,7 +182,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
}
// private Timer for auto reconnect
private CTimer RetryTimer;
private Timer RetryTimer;
#endregion
@@ -266,12 +265,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
Timer HeartbeatSendTimer;
Timer HeartbeatAckTimer;
// Used to force disconnection on a dead connect attempt
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
Timer ConnectFailTimer;
Timer WaitForSharedKey;
private int ConnectionCount;
bool ProgramIsStopping;
@@ -493,7 +492,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
ConnectFailTimer = new Timer(30000) { AutoReset = false };
ConnectFailTimer.Elapsed += (s, e) =>
{
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
@@ -507,7 +507,8 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
};
ConnectFailTimer.Start();
this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
_client.ConnectToServerAsync(o =>
@@ -528,16 +529,16 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
WaitForSharedKey = new Timer(15000) { AutoReset = false };
WaitForSharedKey.Elapsed += (s, e) =>
{
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
};
WaitForSharedKey.Start();
}
else
{
@@ -637,7 +638,9 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
}
if (AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime);
RetryTimer = new Timer(rndTime) { AutoReset = false };
RetryTimer.Elapsed += (s, e) => Connect();
RetryTimer.Start();
}
}
@@ -698,11 +701,11 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
if (Monitor.TryEnter(_dequeueLock))
Task.Run(() => DequeueEvent());
if (System.Threading.Monitor.TryEnter(_dequeueLock))
System.Threading.Tasks.Task.Run(() => DequeueEvent());
}
}
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
}
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
{
client.DisconnectFromServer();
}
@@ -732,7 +735,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
this.LogError(e, "DequeueEvent error: {0}", e.Message);
}
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
Monitor.Exit(_dequeueLock);
System.Threading.Monitor.Exit(_dequeueLock);
}
void HeartbeatStart()
@@ -743,11 +746,15 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
HeartbeatSendTimer.Start();
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
}
}
@@ -791,11 +798,15 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
HeartbeatAckTimer.Stop();
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
HeartbeatAckTimer.Start();
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
}
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;

View File

@@ -17,6 +17,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -223,7 +224,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
System.Timers.Timer RetryTimer;
/// <summary>
@@ -255,13 +256,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
System.Timers.Timer HeartbeatSendTimer;
System.Timers.Timer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
System.Timers.Timer ConnectFailTimer;
System.Timers.Timer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
@@ -457,7 +458,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
ConnectFailTimer = new System.Timers.Timer(30000) { AutoReset = false };
ConnectFailTimer.Elapsed += (s, e) =>
{
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
@@ -471,7 +473,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
};
ConnectFailTimer.Start();
this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o =>
@@ -492,16 +495,16 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
WaitForSharedKey = new System.Timers.Timer(15000) { AutoReset = false };
WaitForSharedKey.Elapsed += (s, e) =>
{
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
};
WaitForSharedKey.Start();
}
else
{
@@ -596,7 +599,9 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
}
if (AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime);
RetryTimer = new System.Timers.Timer(rndTime) { AutoReset = false };
RetryTimer.Elapsed += (s, e) => Connect();
RetryTimer.Start();
}
}
@@ -702,11 +707,15 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
HeartbeatSendTimer = new System.Timers.Timer(HeartbeatInterval) { AutoReset = true };
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
HeartbeatSendTimer.Start();
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
}
}
@@ -750,11 +759,15 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
HeartbeatAckTimer.Stop();
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
HeartbeatAckTimer.Start();
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
}
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;

View File

@@ -2,8 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -92,7 +91,7 @@ public class GenericSecureTcpIpServer : Device
/// <summary>
/// Timer to operate the bandaid monitor client in a loop.
/// </summary>
CTimer MonitorClientTimer;
Timer MonitorClientTimer;
/// <summary>
///
@@ -259,7 +258,7 @@ public class GenericSecureTcpIpServer : Device
public string HeartbeatStringToMatch { get; set; }
//private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
//flags to show the secure server is waiting for client at index to send the shared key
List<uint> WaitingForSharedKey = new List<uint>();
@@ -592,11 +591,17 @@ public class GenericSecureTcpIpServer : Device
if (noDelimiter.Contains(HeartbeatStringToMatch))
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat
@@ -607,11 +612,17 @@ public class GenericSecureTcpIpServer : Device
else
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
}
@@ -665,7 +676,6 @@ public class GenericSecureTcpIpServer : Device
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
var discoResult = SecureServer.Disconnect(clientIndex);
//Debug.Console(1, this, "{0}", discoResult);
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
@@ -694,8 +704,6 @@ public class GenericSecureTcpIpServer : Device
try
{
// Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
@@ -725,7 +733,7 @@ public class GenericSecureTcpIpServer : Device
//Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state
//after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag
//is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event.
Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
System.Threading.Tasks.Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
}
#endregion
@@ -770,7 +778,10 @@ public class GenericSecureTcpIpServer : Device
{
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
}
@@ -860,8 +871,8 @@ public class GenericSecureTcpIpServer : Device
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
if (Monitor.TryEnter(_dequeueLock))
Task.Run(() => DequeueEvent());
if (System.Threading.Monitor.TryEnter(_dequeueLock))
System.Threading.Tasks.Task.Run(() => DequeueEvent());
}
}
else
@@ -894,7 +905,7 @@ public class GenericSecureTcpIpServer : Device
this.LogError(e, "DequeueEvent error");
}
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
Monitor.Exit(_dequeueLock);
System.Threading.Monitor.Exit(_dequeueLock);
}
#endregion
@@ -991,7 +1002,9 @@ public class GenericSecureTcpIpServer : Device
{
return;
}
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
MonitorClientTimer = new Timer(60000) { AutoReset = false };
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
MonitorClientTimer.Start();
}
/// <summary>

View File

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Org.BouncyCastle.Utilities;
using PepperDash.Core.Logging;
using Renci.SshNet;
using Renci.SshNet.Common;
@@ -134,9 +131,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
ShellStream TheStream;
CTimer ReconnectTimer;
Timer ReconnectTimer;
private SemaphoreSlim connectLock = new SemaphoreSlim(1);
private System.Threading.SemaphoreSlim connectLock = new System.Threading.SemaphoreSlim(1);
private bool DisconnectLogged = false;
@@ -155,13 +152,14 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
Password = password;
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
ReconnectTimer.Elapsed += (s, e) =>
{
if (ConnectEnabled)
{
Connect();
}
}, System.Threading.Timeout.Infinite);
};
}
/// <summary>
@@ -173,13 +171,14 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
ReconnectTimer.Elapsed += (s, e) =>
{
if (ConnectEnabled)
{
Connect();
}
}, System.Threading.Timeout.Infinite);
};
}
/// <summary>
@@ -265,7 +264,6 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
catch (SshConnectionException e)
{
var ie = e.InnerException; // The details are inside!!
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
if (ie is SocketException)
{
@@ -289,7 +287,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
}
}
catch (SshOperationTimeoutException ex)
@@ -301,19 +301,22 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
}
}
catch (Exception e)
{
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
this.LogException(e, "Unhandled exception on connect");
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
}
}
}
@@ -434,7 +437,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
/// </summary>
void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{
Task.Run(() =>
System.Threading.Tasks.Task.Run(() =>
{
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
this.LogError("Disconnected by remote");
@@ -452,7 +455,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
if (AutoReconnect && ConnectEnabled)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
ReconnectTimer.Stop();
ReconnectTimer.Interval = AutoReconnectIntervalMs;
ReconnectTimer.Start();
}
});
}
@@ -497,7 +502,8 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
ReconnectTimer.Stop();
ReconnectTimer.Start();
}
catch (Exception ex)
{
@@ -531,7 +537,8 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
ReconnectTimer.Stop();
ReconnectTimer.Start();
}
catch (Exception ex)
{

View File

@@ -131,12 +131,6 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Ushort representation of client status
/// </summary>
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary>
/// Connection failure reason
/// </summary>

View File

@@ -11,10 +11,9 @@ PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -210,7 +209,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
Timer RetryTimer;
/// <summary>
@@ -237,13 +236,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
/// </summary>
public int HeartbeatInterval = 50000;
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
Timer HeartbeatSendTimer;
Timer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
Timer ConnectFailTimer;
Timer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
@@ -303,7 +302,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
this.LogInformation("Program stopping. Closing Client connection");
ProgramIsStopping = true;
Disconnect();
}
@@ -316,17 +315,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
this.LogDebug("Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
this.LogInformation("Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
this.LogInformation("Already trying to connect. Ignoring.");
return;
}
try
@@ -339,17 +338,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
this.LogWarning("DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
this.LogWarning("DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
this.LogWarning("DynamicTcpClient: No Shared Key set");
return;
}
@@ -370,9 +369,10 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
ConnectFailTimer = new Timer(30000) { AutoReset = false };
ConnectFailTimer.Elapsed += (s, e) =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
@@ -384,12 +384,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
};
ConnectFailTimer.Start();
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
this.LogDebug("Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
this.LogDebug("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
@@ -399,22 +400,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
WaitForSharedKey = new Timer(15000) { AutoReset = false };
WaitForSharedKey.Elapsed += (s, e) =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
};
WaitForSharedKey.Start();
}
else
{
@@ -428,14 +429,15 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
this.LogException(ex, "Client connection exception: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
@@ -472,7 +474,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
if (Client != null)
{
//SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose();
Client = null;
@@ -494,20 +496,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
this.LogVerbose("Cleaning up remotely closed/failed connection.");
Cleanup();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
RetryTimer = new CTimer(o => Connect(), rndTime);
RetryTimer = new Timer(rndTime) { AutoReset = false };
RetryTimer.Elapsed += (s, e) => Connect();
RetryTimer.Start();
}
}
@@ -526,18 +530,18 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
this.LogVerbose("Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
this.LogVerbose("Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
@@ -553,7 +557,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
this.LogException(ex, "Error receiving data: {1}. Error: {0}", ex.Message, str);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
}
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
@@ -564,15 +569,19 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
if (HeartbeatEnabled)
{
Debug.Console(2, this, "Starting Heartbeat");
this.LogVerbose("Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
HeartbeatSendTimer.Start();
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
}
}
@@ -582,13 +591,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
this.LogVerbose("Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
this.LogVerbose("Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
@@ -597,7 +606,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
this.LogVerbose("Sending Heartbeat");
}
@@ -616,13 +625,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
HeartbeatAckTimer.Stop();
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
HeartbeatAckTimer.Start();
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
HeartbeatAckTimer.Start();
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
@@ -630,7 +643,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
}
return received;
}
@@ -644,7 +658,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
@@ -652,7 +666,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
this.LogException(ex, "Heartbeat timeout Error on Client: {0}, {1}", Key, ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
}
}
@@ -685,14 +700,15 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
this.LogException(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
}
}
}
@@ -711,7 +727,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
this.LogException(ex, "Error sending bytes. Error: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
}
}
}
@@ -730,7 +747,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
@@ -744,7 +761,8 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
this.LogException(ex, "Error in socket status change callback. Error: {0}", ex.Message);
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
}
}

View File

@@ -13,7 +13,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Timers;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
@@ -80,7 +80,7 @@ public class GenericTcpIpServer : Device
/// <summary>
/// Timer to operate the bandaid monitor client in a loop.
/// </summary>
CTimer MonitorClientTimer;
Timer MonitorClientTimer;
/// <summary>
///
@@ -250,7 +250,7 @@ public class GenericTcpIpServer : Device
public string HeartbeatStringToMatch { get; set; }
//private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
//flags to show the secure server is waiting for client at index to send the shared key
List<uint> WaitingForSharedKey = new List<uint>();
@@ -577,11 +577,17 @@ public class GenericTcpIpServer : Device
if (noDelimiter.Contains(HeartbeatStringToMatch))
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
// Return Heartbeat
@@ -592,11 +598,17 @@ public class GenericTcpIpServer : Device
else
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
{
HeartbeatTimerDictionary[clientIndex].Stop();
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
HeartbeatTimerDictionary[clientIndex].Start();
}
else
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
}
@@ -650,7 +662,6 @@ public class GenericTcpIpServer : Device
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
var discoResult = myTcpServer.Disconnect(clientIndex);
//Debug.Console(1, this, "{0}", discoResult);
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
@@ -661,7 +672,8 @@ public class GenericTcpIpServer : Device
}
catch (Exception ex)
{
ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key);
this.LogException(ex, "Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message);
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
}
}
@@ -746,7 +758,10 @@ public class GenericTcpIpServer : Device
{
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
{
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
heartbeatTimer.Start();
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
}
}
@@ -765,9 +780,9 @@ public class GenericTcpIpServer : Device
}
catch (Exception ex)
{
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex);
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
}
//Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))",
// server.State,
// MaxClients,
// ServerStopped);
@@ -929,7 +944,9 @@ public class GenericTcpIpServer : Device
{
return;
}
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
MonitorClientTimer = new Timer(60000) { AutoReset = false };
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
MonitorClientTimer.Start();
}
/// <summary>

View File

@@ -183,7 +183,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
if (programEventType != eProgramStatusEventType.Stopping)
return;
Debug.Console(1, this, "Program stopping. Disabling Server");
this.LogInformation("Program stopping. Disabling Server");
Disconnect();
}
@@ -199,20 +199,20 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key);
this.LogWarning("GenericUdpServer '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key);
this.LogWarning("GenericUdpServer '{0}': Invalid port", Key);
return;
}
}
var status = Server.EnableUDPServer(Hostname, Port);
Debug.Console(2, this, "SocketErrorCode: {0}", status);
this.LogVerbose("SocketErrorCode: {0}", status);
if (status == SocketErrorCodes.SOCKET_OK)
IsConnected = true;
@@ -247,7 +247,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
/// <param name="numBytes"></param>
void Receive(UDPServer server, int numBytes)
{
Debug.Console(2, this, "Received {0} bytes", numBytes);
this.LogVerbose("Received {0} bytes", numBytes);
try
{
@@ -263,13 +263,13 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
if (dataRecivedExtra != null)
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
Debug.Console(2, this, "Bytes: {0}", bytes.ToString());
this.LogVerbose("Bytes: {0}", bytes.ToString());
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
@@ -277,7 +277,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
if (textHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
@@ -302,7 +302,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
if (IsConnected && Server != null)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
this.LogVerbose("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
Server.SendData(bytes, bytes.Length);
}
@@ -315,7 +315,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
public void SendBytes(byte[] bytes)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (IsConnected && Server != null)
Server.SendData(bytes, bytes.Length);

View File

@@ -155,7 +155,6 @@ namespace PepperDash.Core.Config;
else
merged.Add(global, template[global]);
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
return merged;
}

View File

@@ -62,7 +62,6 @@ public class Device : IKeyName
public Device(string key, string name) : this(key)
{
Name = name;
}
//public Device(DeviceConfig config)
@@ -75,7 +74,7 @@ public class Device : IKeyName
/// Adds a pre activation action
/// </summary>
/// <param name="act"></param>
public void AddPreActivationAction(Action act)
protected void AddPreActivationAction(Action act)
{
if (_PreActivationActions == null)
_PreActivationActions = new List<Action>();
@@ -89,7 +88,7 @@ public class Device : IKeyName
/// <summary>
/// AddPostActivationAction method
/// </summary>
public void AddPostActivationAction(Action act)
protected void AddPostActivationAction(Action act)
{
if (_PostActivationActions == null)
_PostActivationActions = new List<Action>();
@@ -156,7 +155,7 @@ public class Device : IKeyName
/// <summary>
/// CustomActivate method
/// </summary>
public virtual bool CustomActivate() { return true; }
protected virtual bool CustomActivate() { return true; }
/// <summary>
/// Call to deactivate device - unlink events, etc. Overriding classes do not
@@ -168,7 +167,7 @@ public class Device : IKeyName
/// <summary>
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
/// </summary>
public virtual void Initialize()
protected virtual void Initialize()
{
}

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using PepperDash.Core.Logging;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
namespace PepperDash.Core.JsonToSimpl;
@@ -91,7 +92,7 @@ namespace PepperDash.Core.JsonToSimpl;
if (Master != null)
Master.AddChild(this);
else
Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
this.LogWarning("JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
}
/// <summary>
@@ -107,7 +108,7 @@ namespace PepperDash.Core.JsonToSimpl;
/// </summary>
public void SetBoolPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
this.LogDebug("JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
BoolPaths[index] = path;
}
@@ -117,7 +118,7 @@ namespace PepperDash.Core.JsonToSimpl;
/// </summary>
public void SetUshortPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
this.LogDebug("JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
UshortPaths[index] = path;
}
@@ -127,7 +128,7 @@ namespace PepperDash.Core.JsonToSimpl;
/// </summary>
public void SetStringPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
this.LogDebug("JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
StringPaths[index] = path;
}
@@ -140,13 +141,13 @@ namespace PepperDash.Core.JsonToSimpl;
{
if (!LinkedToObject)
{
Debug.Console(1, this, "Not linked to object in file. Skipping");
this.LogDebug("Not linked to object in file. Skipping");
return;
}
if (SetAllPathsDelegate == null)
{
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring ProcessAll");
this.LogDebug("No SetAllPathsDelegate set. Ignoring ProcessAll");
return;
}
SetAllPathsDelegate();
@@ -206,11 +207,11 @@ namespace PepperDash.Core.JsonToSimpl;
bool Process(string path, out string response)
{
path = GetFullPath(path);
Debug.Console(1, "JSON Child[{0}] Processing {1}", Key, path);
this.LogDebug("JSON Child[{0}] Processing {1}", Key, path);
response = "";
if (Master == null)
{
Debug.Console(1, "JSONChild[{0}] cannot process without Master attached", Key);
this.LogWarning("JSONChild[{0}] cannot process without Master attached", Key);
return false;
}
@@ -233,7 +234,7 @@ namespace PepperDash.Core.JsonToSimpl;
response = (t.HasValues ? t.Children().Count() : 0).ToString();
else
response = (string)t;
Debug.Console(1, " ='{0}'", response);
this.LogDebug(" ='{0}'", response);
return true;
}
}
@@ -259,13 +260,13 @@ namespace PepperDash.Core.JsonToSimpl;
{
if (!LinkedToObject)
{
Debug.Console(1, this, "Not linked to object in file. Skipping");
this.LogDebug("Not linked to object in file. Skipping");
return;
}
if (SetAllPathsDelegate == null)
{
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
this.LogDebug("No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
return;
}
SetAllPathsDelegate();
@@ -327,7 +328,7 @@ namespace PepperDash.Core.JsonToSimpl;
var path = GetFullPath(keyPath);
try
{
Debug.Console(1, "JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
this.LogDebug("JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
//var token = Master.JsonObject.SelectToken(path);
//if (token != null) // The path exists in the file
@@ -335,7 +336,7 @@ namespace PepperDash.Core.JsonToSimpl;
}
catch (Exception e)
{
Debug.Console(1, "JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
this.LogDebug("JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
}
}

View File

@@ -7,6 +7,7 @@ using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using PepperDash.Core.Logging;
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
@@ -129,7 +130,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
var fileName = Path.GetFileName(Filepath);
OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName);
this.LogInformation("Checking '{0}' for '{1}'", fileDirectory, fileName);
if (Directory.Exists(fileDirectory))
{
@@ -143,7 +144,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
var msg = string.Format("JSON file not found: {0}", Filepath);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
this.LogError(msg);
return;
}
@@ -152,18 +153,18 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
ActualFilePath = actualFile.FullName;
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Actual JSON file is {0}", ActualFilePath);
this.LogInformation("Actual JSON file is {0}", ActualFilePath);
Filename = actualFile.Name;
OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange);
OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON Filename is {0}", Filename);
this.LogInformation("JSON Filename is {0}", Filename);
FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator);
OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange);
OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON File Path is {0}", FilePathName);
this.LogInformation("JSON File Path is {0}", FilePathName);
var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
@@ -176,7 +177,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
else
{
OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "'{0}' not found", fileDirectory);
this.LogError("'{0}' not found", fileDirectory);
}
}
catch (Exception e)
@@ -184,12 +185,12 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
this.LogException(e, "EvaluateFile Exception: {0}", e.Message);
var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(stackTrace);
ErrorLog.Error(stackTrace);
this.LogVerbose("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
}
}
@@ -213,63 +214,31 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID);
return;
}
lock (FileLock)
{
Debug.Console(1, "Saving");
this.LogInformation("Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path);
}
else // No token. Let's make one
{
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
// JContainer jpart = JsonObject;
// // walk down the path and find where it goes
//#warning Does not handle arrays.
// foreach (var part in path.Split('.'))
// {
// var openPos = part.IndexOf('[');
// if (openPos > -1)
// {
// openPos++; // move to number
// var closePos = part.IndexOf(']');
// var arrayName = part.Substring(0, openPos - 1); // get the name
// var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos));
// // Check if the array itself exists and add the item if so
// if (jpart[arrayName] != null)
// {
// var arrayObj = jpart[arrayName] as JArray;
// var item = arrayObj[index];
// if (item == null)
// arrayObj.Add(new JObject());
// }
// Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW");
// continue;
// }
// // Build the
// if (jpart[part] == null)
// jpart.Add(new JProperty(part, new JObject()));
// jpart = jpart[part] as JContainer;
// }
// jpart.Replace(UnsavedValues[path]);
}
}
using (StreamWriter sw = new StreamWriter(ActualFilePath))
@@ -282,11 +251,13 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
catch (Exception e)
{
string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err);
ErrorLog.Warn(err);
this.LogException(e, "Error writing JSON file: {0}", e.Message);
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
return;
}
}
}
}
}

View File

@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using PepperDash.Core.Logging;
using Renci.SshNet.Messages;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
@@ -74,7 +76,8 @@ namespace PepperDash.Core.JsonToSimpl;
}
catch (Exception e)
{
Debug.Console(0, this, "JSON parsing failed:\r{0}", e);
this.LogException(e, "JSON parsing failed:\r{0}", e.Message);
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
}
}
@@ -89,36 +92,36 @@ namespace PepperDash.Core.JsonToSimpl;
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, this, "Master. checking child [{0}] for updates to save", child.Key);
this.LogDebug("Master. checking child [{0}] for updates to save", child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, this, "Master. No updated values to save. Skipping");
this.LogDebug("Master. No updated values to save. Skipping");
return;
}
lock (WriteLock)
{
Debug.Console(1, this, "Saving");
this.LogDebug("Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, this, "Master Updating '{0}'", path);
this.LogDebug("Master Updating '{0}'", path);
}
else // No token. Let's make one
{
Debug.Console(1, "Master Cannot write value onto missing property: '{0}'", path);
this.LogDebug("Master Cannot write value onto missing property: '{0}'", path);
}
}
}
if (SaveCallback != null)
SaveCallback(JsonObject.ToString());
else
Debug.Console(0, this, "WARNING: No save callback defined.");
this.LogDebug("WARNING: No save callback defined.");
}
}

View File

@@ -10,6 +10,7 @@ using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
using JsonSerializationException = NewtonsoftJson::Newtonsoft.Json.JsonSerializationException;
using JsonTextReader = NewtonsoftJson::Newtonsoft.Json.JsonTextReader;
using PepperDash.Core.Logging;
namespace PepperDash.Core.JsonToSimpl;
@@ -142,11 +143,10 @@ namespace PepperDash.Core.JsonToSimpl;
{
if (UnsavedValues.ContainsKey(path))
{
Debug.Console(0, "Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path);
this.LogWarning("Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path);
}
else
UnsavedValues.Add(path, value);
//Debug.Console(0, "Master[{0}] Unsaved size={1}", UniqueID, UnsavedValues.Count);
}
/// <summary>

View File

@@ -8,6 +8,7 @@ using Crestron.SimplSharp.CrestronIO;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
using PepperDash.Core.Config;
using PepperDash.Core.Logging;
namespace PepperDash.Core.JsonToSimpl;
@@ -61,7 +62,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
// If the portal file is xyz.json, then
// the file we want to check for first will be called xyz.local.json
var localFilepath = Path.ChangeExtension(PortalFilepath, "local.json");
Debug.Console(0, this, "Checking for local file {0}", localFilepath);
this.LogInformation("Checking for local file {0}", localFilepath);
var actualLocalFile = GetActualFileInfoFromPath(localFilepath);
if (actualLocalFile != null)
@@ -73,7 +74,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
// and create the local.
else
{
Debug.Console(1, this, "Local JSON file not found {0}\rLoading portal JSON file", localFilepath);
this.LogInformation("Local JSON file not found {0}\rLoading portal JSON file", localFilepath);
var actualPortalFile = GetActualFileInfoFromPath(portalFilepath);
if (actualPortalFile != null)
{
@@ -86,14 +87,13 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
else
{
var msg = string.Format("Portal JSON file not found: {0}", PortalFilepath);
Debug.Console(1, this, msg);
ErrorLog.Error(msg);
this.LogError(msg);
return;
}
}
// At this point we should have a local file. Do it.
Debug.Console(1, "Reading local JSON file {0}", ActualFilePath);
this.LogInformation("Reading local JSON file {0}", ActualFilePath);
string json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
@@ -107,8 +107,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
catch (Exception e)
{
var msg = string.Format("JSON parsing failed:\r{0}", e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
this.LogError(msg);
return;
}
}
@@ -149,30 +148,30 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID);
return;
}
lock (FileLock)
{
Debug.Console(1, "Saving");
this.LogInformation("Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path);
}
else // No token. Let's make one
{
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
}
}
@@ -186,8 +185,8 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
catch (Exception e)
{
string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err);
ErrorLog.Warn(err);
this.LogException(e, "Error writing JSON file: {0}", e.Message);
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
return;
}
}

View File

@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace PepperDash.Core.Logging;
/// <summary>
/// Enriches log events with Crestron-specific context properties, such as the application name based on the device platform.
/// </summary>
public class CrestronEnricher : ILogEventEnricher
{
static readonly string _appName;
@@ -27,6 +30,11 @@ public class CrestronEnricher : ILogEventEnricher
}
/// <summary>
/// Enriches the log event with Crestron-specific properties.
/// </summary>
/// <param name="logEvent"></param>
/// <param name="propertyFactory"></param>
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var property = propertyFactory.CreateProperty("App", _appName);

File diff suppressed because it is too large Load Diff

View File

@@ -1,283 +0,0 @@
extern alias NewtonsoftJson;
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert;
namespace PepperDash.Core;
/// <summary>
/// Represents a debugging context
/// </summary>
public class DebugContext
{
/// <summary>
/// Describes the folder location where a given program stores it's debug level memory. By default, the
/// file written will be named appNdebug where N is 1-10.
/// </summary>
public string Key { get; private set; }
///// <summary>
///// The name of the file containing the current debug settings.
///// </summary>
//string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber);
DebugContextSaveData SaveData;
int SaveTimeoutMs = 30000;
CTimer SaveTimer;
static List<DebugContext> Contexts = new List<DebugContext>();
/// <summary>
/// Creates or gets a debug context
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static DebugContext GetDebugContext(string key)
{
var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (context == null)
{
context = new DebugContext(key);
Contexts.Add(context);
}
return context;
}
/// <summary>
/// Do not use. For S+ access.
/// </summary>
public DebugContext() { }
DebugContext(string key)
{
Key = key;
if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro)
{
// Add command to console
CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug",
"appdebug:P [0-2]: Sets the application's console debug message level",
ConsoleAccessLevelEnum.AccessOperator);
}
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
LoadMemory();
}
/// <summary>
/// Used to save memory when shutting down
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (SaveTimer != null)
{
SaveTimer.Stop();
SaveTimer = null;
}
Console(0, "Saving debug settings");
SaveMemory();
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="levelString"></param>
public void SetDebugFromConsole(string levelString)
{
try
{
if (string.IsNullOrEmpty(levelString.Trim()))
{
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level);
return;
}
SetDebugLevel(Convert.ToInt32(levelString));
}
catch
{
CrestronConsole.PrintLine("Usage: appdebug:P [0-2]");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0 (no debug), 1 (critical), 2 (all messages)</param>
public void SetDebugLevel(int level)
{
if (level <= 2)
{
SaveData.Level = level;
SaveMemoryOnTimeout();
CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}",
InitialParametersClass.ApplicationNumber, SaveData.Level);
}
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
public void Console(uint level, string format, params object[] items)
{
if (SaveData.Level >= level)
CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber,
string.Format(format, items));
}
/// <summary>
/// Appends a device Key to the beginning of a message
/// </summary>
public void Console(uint level, IKeyed dev, string format, params object[] items)
{
if (SaveData.Level >= level)
Console(level, "[{0}] {1}", dev.Key, string.Format(format, items));
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="dev"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
if (SaveData.Level >= level)
{
var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items));
Console(level, str);
LogError(errorLogLevel, str);
}
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
public void Console(uint level, Debug.ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
if (SaveData.Level >= level)
{
var str = string.Format(format, items);
Console(level, str);
LogError(errorLogLevel, str);
}
}
/// <summary>
///
/// </summary>
/// <param name="errorLogLevel"></param>
/// <param name="str"></param>
public void LogError(Debug.ErrorLogLevel errorLogLevel, string str)
{
string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str);
switch (errorLogLevel)
{
case Debug.ErrorLogLevel.Error:
ErrorLog.Error(msg);
break;
case Debug.ErrorLogLevel.Warning:
ErrorLog.Warn(msg);
break;
case Debug.ErrorLogLevel.Notice:
ErrorLog.Notice(msg);
break;
}
}
/// <summary>
/// Writes the memory object after timeout
/// </summary>
void SaveMemoryOnTimeout()
{
if (SaveTimer == null)
SaveTimer = new CTimer(o =>
{
SaveTimer = null;
SaveMemory();
}, SaveTimeoutMs);
else
SaveTimer.Reset(SaveTimeoutMs);
}
/// <summary>
/// Writes the memory - use SaveMemoryOnTimeout
/// </summary>
void SaveMemory()
{
using (StreamWriter sw = new StreamWriter(GetMemoryFileName()))
{
var json = JsonConvert.SerializeObject(SaveData);
sw.Write(json);
sw.Flush();
}
}
/// <summary>
///
/// </summary>
void LoadMemory()
{
var file = GetMemoryFileName();
if (File.Exists(file))
{
using (StreamReader sr = new StreamReader(file))
{
var data = JsonConvert.DeserializeObject<DebugContextSaveData>(sr.ReadToEnd());
if (data != null)
{
SaveData = data;
Debug.Console(1, "Debug memory restored from file");
return;
}
else
SaveData = new DebugContextSaveData();
}
}
}
/// <summary>
/// Helper to get the file path for this app's debug memory
/// </summary>
string GetMemoryFileName()
{
return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key);
}
}
/// <summary>
///
/// </summary>
public class DebugContextSaveData
{
/// <summary>
///
/// </summary>
public int Level { get; set; }
}

View File

@@ -1,8 +1,10 @@
extern alias NewtonsoftJson;
using System;
using Crestron.SimplSharp;
using Org.BouncyCastle.Asn1.X509;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Crestron.SimplSharp;
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
@@ -11,10 +13,8 @@ using Serilog.Formatting;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using Serilog.Formatting.Json;
using System.IO;
using System.Security.Authentication;
using WebSocketSharp;
using WebSocketSharp.Server;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using WebSocketSharp.Net;
namespace PepperDash.Core;
@@ -29,21 +29,24 @@ namespace PepperDash.Core;
public class DebugWebsocketSink : ILogEventSink, IKeyed
{
private HttpServer _httpsServer;
private readonly string _path = "/debug/join/";
private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345";
private static string CertPath =>
$"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
/// <summary>
/// Gets the port number on which the HTTPS server is currently running.
/// </summary>
public int Port
{ get
{
if(_httpsServer == null) return 0;
public int Port
{
get
{
if (_httpsServer == null) return 0;
return _httpsServer.Port;
}
}
}
/// <summary>
@@ -55,15 +58,17 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
{
get
{
if (_httpsServer == null) return "";
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}";
if (_httpsServer == null || !_httpsServer.IsListening) return "";
var service = _httpsServer.WebSocketServices[_path];
if (service == null) return "";
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}";
}
}
/// <summary>
/// Gets a value indicating whether the HTTPS server is currently listening for incoming connections.
/// </summary>
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
/// <inheritdoc/>
public string Key => "DebugWebsocketSink";
@@ -83,43 +88,84 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
_textFormatter = formatProvider ?? new JsonFormatter();
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
if (!File.Exists(CertPath))
CreateCert();
CrestronEnvironment.ProgramStatusEventHandler += type =>
try
{
if (type == eProgramStatusEventType.Stopping)
CrestronEnvironment.ProgramStatusEventHandler += type =>
{
StopServer();
}
};
if (type == eProgramStatusEventType.Stopping)
StopServer();
};
}
catch
{
// CrestronEnvironment is not available in test / dev environments — safe to skip.
}
}
private static void CreateCert()
{
// NOTE: This method is called from the constructor, which is itself called during Debug's static
// constructor before _logger is assigned. Do NOT call any Debug.Log* methods here — use
// CrestronConsole.PrintLine only, to avoid a NullReferenceException that would poison the Debug type.
try
{
var utility = new BouncyCertificate();
{
var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
CrestronConsole.PrintLine(string.Format("CreateCert: DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), [string.Format("{0}.{1}", hostName, domainName), ipAddress], [KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth]);
var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
var fqdn = string.Format("{0}.{1}", hostName, domainName);
//Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested
using var rsa = RSA.Create(2048);
var request = new CertificateRequest(
subjectName,
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
// Subject Key Identifier
request.CertificateExtensions.Add(
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
// Extended Key Usage: server + client auth
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth
},
false));
// Subject Alternative Names: DNS + IP
var sanBuilder = new SubjectAlternativeNameBuilder();
sanBuilder.AddDnsName(fqdn);
if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
sanBuilder.AddIpAddress(ip);
request.CertificateExtensions.Add(sanBuilder.Build());
var notBefore = DateTimeOffset.UtcNow;
var notAfter = notBefore.AddYears(2);
using var cert = request.CreateSelfSigned(notBefore, notAfter);
var separator = Path.DirectorySeparatorChar;
utility.CertificatePassword = _certificatePassword;
utility.WriteCertificate(certificate, @$"{separator}user{separator}", _certificateName);
var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword);
File.WriteAllBytes(outputPath, pfxBytes);
CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
}
catch (Exception ex)
{
//Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace);
CrestronConsole.PrintLine("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace);
CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed: {0}\r\n{1}", ex.Message, ex.StackTrace));
}
}
@@ -137,7 +183,7 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
var sw = new StringWriter();
_textFormatter.Format(logEvent, sw);
_httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
_httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
}
/// <summary>
@@ -149,76 +195,68 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
/// <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);
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword);
Start(port, CertPath, _certificatePassword);
}
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
{
try
{
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
// key-container persistence failures, and avoids the private key export restriction.
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
}
catch (Exception ex)
{
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
// Delete it, regenerate with the BCL path, and retry once.
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
try { File.Delete(certPath); } catch { }
CreateCert();
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
}
}
private void Start(int port, string certPath = "", string certPassword = "")
{
try
{
_httpsServer = new HttpServer(port, true);
_httpsServer = new HttpServer(port, true);
if (!string.IsNullOrWhiteSpace(certPath))
{
Debug.Console(0, "Assigning SSL Configuration");
Debug.LogInformation("Assigning SSL Configuration");
_httpsServer.SslConfiguration.ServerCertificate = new X509Certificate2(certPath, certPassword);
_httpsServer.SslConfiguration.ServerCertificate = LoadOrRecreateCert(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");
Debug.LogInformation("HTTPS ClientCerticateValidation Callback triggered");
return true;
};
}
Debug.Console(0, "Adding Debug Client Service");
Debug.LogInformation("Adding Debug Client Service");
_httpsServer.AddWebSocketService<DebugClient>(_path);
Debug.Console(0, "Assigning Log Info");
Debug.LogInformation("Assigning Log Info");
_httpsServer.Log.Level = LogLevel.Trace;
_httpsServer.Log.Output = (d, s) =>
{
uint level;
switch(d.Level)
{
case WebSocketSharp.LogLevel.Fatal:
level = 3;
break;
case WebSocketSharp.LogLevel.Error:
level = 2;
break;
case WebSocketSharp.LogLevel.Warn:
level = 1;
break;
case WebSocketSharp.LogLevel.Info:
level = 0;
break;
case WebSocketSharp.LogLevel.Debug:
level = 4;
break;
case WebSocketSharp.LogLevel.Trace:
level = 5;
break;
default:
level = 4;
break;
}
Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s);
};
Debug.Console(0, "Starting");
_httpsServer.Log.Output = WriteWebSocketInternalLog;
Debug.LogInformation("Starting");
_httpsServer.Start();
Debug.Console(0, "Ready");
Debug.LogInformation("Ready");
}
catch (Exception ex)
{
Debug.Console(0, "WebSocket Failed to start {0}", ex.Message);
Debug.LogError(ex, "WebSocket Failed to start {0}", ex.Message);
Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
// Null out the server so callers can detect failure via IsRunning / Url null guards.
_httpsServer = null;
}
}
@@ -229,11 +267,70 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
/// calling this method, the server will no longer accept or process incoming connections.</remarks>
public void StopServer()
{
Debug.Console(0, "Stopping Websocket Server");
_httpsServer?.Stop();
Debug.LogInformation("Stopping Websocket Server");
_httpsServer = null;
try
{
if (_httpsServer == null || !_httpsServer.IsListening)
{
return;
}
// Prevent close-sequence internal websocket logs from re-entering the logging pipeline.
_httpsServer.Log.Output = (d, s) => { };
var serviceHost = _httpsServer.WebSocketServices[_path];
if (serviceHost == null)
{
_httpsServer.Stop();
_httpsServer = null;
return;
}
serviceHost.Sessions.Broadcast("Server is stopping");
foreach (var session in serviceHost.Sessions.Sessions)
{
if (session?.Context?.WebSocket != null && session.Context.WebSocket.IsAlive)
{
session.Context.WebSocket.Close(1001, "Server is stopping");
}
}
_httpsServer.Stop();
_httpsServer = null;
}
catch (Exception ex)
{
Debug.LogError(ex, "WebSocket Failed to stop gracefully {0}", ex.Message);
Debug.LogVerbose("Stack Trace\r\n{0}", ex.StackTrace);
}
}
private static void WriteWebSocketInternalLog(LogData data, string supplemental)
{
try
{
if (data == null)
{
return;
}
var message = string.IsNullOrWhiteSpace(data.Message) ? "<none>" : data.Message;
var details = string.IsNullOrWhiteSpace(supplemental) ? string.Empty : string.Format(" | details: {0}", supplemental);
// Use direct console output to avoid recursive log sink calls.
CrestronConsole.PrintLine(string.Format("WS[{0}] {1} | message: {2}{3}", data.Level, data.Date, message, details));
}
catch
{
// Never throw from websocket log callback.
}
}
}
/// <summary>
@@ -291,11 +388,9 @@ public class DebugClient : WebSocketBehavior
/// <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");
Debug.LogInformation("DebugClient Created");
}
/// <inheritdoc/>
@@ -304,7 +399,7 @@ public class DebugClient : WebSocketBehavior
base.OnOpen();
var url = Context.WebSocket.Url;
Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url);
Debug.LogInformation("New WebSocket Connection from: {0}", url);
_connectionTime = DateTime.Now;
}
@@ -314,7 +409,7 @@ public class DebugClient : WebSocketBehavior
{
base.OnMessage(e);
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
Debug.LogVerbose("WebSocket UiClient Message: {0}", e.Data);
}
/// <inheritdoc/>
@@ -322,8 +417,7 @@ public class DebugClient : WebSocketBehavior
{
base.OnClose(e);
Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
Debug.LogDebug("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
}
/// <inheritdoc/>
@@ -331,6 +425,7 @@ public class DebugClient : WebSocketBehavior
{
base.OnError(e);
Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
Debug.LogError(e.Exception, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
Debug.LogVerbose("Stack Trace:\r{0}", e.Exception.StackTrace);
}
}

View File

@@ -1,246 +1,252 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using System.Timers;
namespace PepperDash.Core.PasswordManagement;
/// <summary>
/// Allows passwords to be stored and managed
/// </summary>
public class PasswordManager
public class PasswordManager
{
/// <summary>
/// Public dictionary of known passwords
/// </summary>
public static Dictionary<uint, string> Passwords = new Dictionary<uint, string>();
/// <summary>
/// Private dictionary, used when passwords are updated
/// </summary>
private Dictionary<uint, string> _passwords = new Dictionary<uint, string>();
/// <summary>
/// Timer used to wait until password changes have stopped before updating the dictionary
/// </summary>
Timer PasswordTimer;
/// <summary>
/// Timer length
/// </summary>
public long PasswordTimerElapsedMs = 5000;
/// <summary>
/// Boolean event
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort event
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String event
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Event to notify clients of an updated password at the specified index (uint)
/// </summary>
public static event EventHandler<StringChangeEventArgs> PasswordChange;
/// <summary>
/// Constructor
/// </summary>
public PasswordManager()
{
/// <summary>
/// Public dictionary of known passwords
/// </summary>
public static Dictionary<uint, string> Passwords = new Dictionary<uint, string>();
/// <summary>
/// Private dictionary, used when passwords are updated
/// </summary>
private Dictionary<uint, string> _passwords = new Dictionary<uint, string>();
/// <summary>
/// Timer used to wait until password changes have stopped before updating the dictionary
/// </summary>
CTimer PasswordTimer;
/// <summary>
/// Timer length
/// </summary>
public long PasswordTimerElapsedMs = 5000;
}
/// <summary>
/// Boolean event
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort event
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String event
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Event to notify clients of an updated password at the specified index (uint)
/// </summary>
public static event EventHandler<StringChangeEventArgs> PasswordChange;
/// <summary>
/// Initialize password manager
/// </summary>
public void Initialize()
{
if (Passwords == null)
Passwords = new Dictionary<uint, string>();
/// <summary>
/// Constructor
/// </summary>
public PasswordManager()
if (_passwords == null)
_passwords = new Dictionary<uint, string>();
OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange);
}
/// <summary>
/// Updates password stored in the dictonary
/// </summary>
/// <param name="key"></param>
/// <param name="password"></param>
/// <summary>
/// UpdatePassword method
/// </summary>
public void UpdatePassword(ushort key, string password)
{
// validate the parameters
if (key > 0 && string.IsNullOrEmpty(password))
{
Debug.LogDebug("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password);
return;
}
/// <summary>
/// Initialize password manager
/// </summary>
public void Initialize()
try
{
if (Passwords == null)
Passwords = new Dictionary<uint, string>();
// if key exists, update the value
if (_passwords.ContainsKey(key))
_passwords[key] = password;
// else add the key & value
else
_passwords.Add(key, password);
if (_passwords == null)
_passwords = new Dictionary<uint, string>();
Debug.LogDebug("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]);
OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange);
}
/// <summary>
/// Updates password stored in the dictonary
/// </summary>
/// <param name="key"></param>
/// <param name="password"></param>
/// <summary>
/// UpdatePassword method
/// </summary>
public void UpdatePassword(ushort key, string password)
{
// validate the parameters
if (key > 0 && string.IsNullOrEmpty(password))
if (PasswordTimer == null)
{
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password));
return;
PasswordTimer = new Timer(PasswordTimerElapsedMs) { AutoReset = false };
PasswordTimer.Elapsed += (s, e) => PasswordTimerElapsed(s, e);
PasswordTimer.Start();
Debug.LogDebug("PasswordManager.UpdatePassword: Timer Started");
OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
}
try
{
// if key exists, update the value
if(_passwords.ContainsKey(key))
_passwords[key] = password;
// else add the key & value
else
_passwords.Add(key, password);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]));
if (PasswordTimer == null)
{
PasswordTimer = new CTimer((o) => PasswordTimerElapsed(), PasswordTimerElapsedMs);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started"));
OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
}
else
{
PasswordTimer.Reset(PasswordTimerElapsedMs);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset"));
}
}
catch (Exception e)
{
var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e);
Debug.Console(1, msg);
}
}
/// <summary>
/// CTimer callback function
/// </summary>
private void PasswordTimerElapsed()
{
try
else
{
PasswordTimer.Stop();
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: CTimer Stopped"));
OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
foreach (var pw in _passwords)
PasswordTimer.Interval = PasswordTimerElapsedMs;
PasswordTimer.Start();
Debug.LogDebug("PasswordManager.UpdatePassword: Timer Reset");
}
}
catch (Exception e)
{
var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e);
Debug.LogError(e, msg);
Debug.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
}
}
/// <summary>
/// Timer callback function
/// </summary>
private void PasswordTimerElapsed(object sender, ElapsedEventArgs e)
{
try
{
PasswordTimer.Stop();
Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Timer Stopped");
OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
foreach (var pw in _passwords)
{
// if key exists, continue
if (Passwords.ContainsKey(pw.Key))
{
// if key exists, continue
if (Passwords.ContainsKey(pw.Key))
Debug.LogDebug("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value);
if (Passwords[pw.Key] != _passwords[pw.Key])
{
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value));
if (Passwords[pw.Key] != _passwords[pw.Key])
{
Passwords[pw.Key] = _passwords[pw.Key];
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]));
OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange);
}
}
// else add the key & value
else
{
Passwords.Add(pw.Key, pw.Value);
Passwords[pw.Key] = _passwords[pw.Key];
Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]);
OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange);
}
}
OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
}
catch (Exception e)
{
var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", e);
Debug.Console(1, msg);
// else add the key & value
else
{
Passwords.Add(pw.Key, pw.Value);
}
}
OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
}
/// <summary>
/// Method to change the default timer value, (default 5000ms/5s)
/// </summary>
/// <param name="time"></param>
/// <summary>
/// PasswordTimerMs method
/// </summary>
public void PasswordTimerMs(ushort time)
catch (Exception ex)
{
PasswordTimerElapsedMs = Convert.ToInt64(time);
var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", ex.Message);
Debug.LogError(ex, msg);
Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
}
}
/// <summary>
/// Helper method for debugging to see what passwords are in the lists
/// </summary>
public void ListPasswords()
{
Debug.Console(0, "PasswordManager.ListPasswords:\r");
foreach (var pw in Passwords)
Debug.Console(0, "Passwords[{0}]: {1}\r", pw.Key, pw.Value);
Debug.Console(0, "\n");
foreach (var pw in _passwords)
Debug.Console(0, "_passwords[{0}]: {1}\r", pw.Key, pw.Value);
}
/// <summary>
/// Method to change the default timer value, (default 5000ms/5s)
/// </summary>
/// <param name="time"></param>
/// <summary>
/// PasswordTimerMs method
/// </summary>
public void PasswordTimerMs(ushort time)
{
PasswordTimerElapsedMs = Convert.ToInt64(time);
}
/// <summary>
/// Protected boolean change event handler
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Helper method for debugging to see what passwords are in the lists
/// </summary>
public void ListPasswords()
{
Debug.LogInformation("PasswordManager.ListPasswords:\r");
foreach (var pw in Passwords)
Debug.LogInformation("Passwords[{0}]: {1}\r", pw.Key, pw.Value);
Debug.LogInformation("\n");
foreach (var pw in _passwords)
Debug.LogInformation("_passwords[{0}]: {1}\r", pw.Key, pw.Value);
}
/// <summary>
/// Protected ushort change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort value, ushort index, ushort type)
/// <summary>
/// Protected boolean change event handler
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(value, type);
args.Index = index;
UshrtChange(this, args);
}
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Protected string change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
/// <summary>
/// Protected ushort change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort value, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
var args = new UshrtChangeEventArgs(value, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// Protected password change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnPasswordChange(string value, ushort index, ushort type)
/// <summary>
/// Protected string change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var handler = PasswordChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
PasswordChange(this, args);
}
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
}
/// <summary>
/// Protected password change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnPasswordChange(string value, ushort index, ushort type)
{
var handler = PasswordChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
PasswordChange(this, args);
}
}
}

View File

@@ -50,6 +50,9 @@
<PackageReference Include="SSH.NET" Version="2025.0.0" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Comm\._GenericSshClient.cs" />
<Compile Remove="Comm\._GenericTcpIpClient.cs" />

View File

@@ -1,14 +1,9 @@
extern alias NewtonsoftJson;
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Core.Logging;
@@ -76,10 +71,13 @@ public class WebApiServer : IKeyName
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
this.LogInformation("Creating Web API Server with Key: {Key}, Name: {Name}, BasePath: {BasePath}", Key, Name, BasePath);
if (_server == null) _server = new HttpCwsServer(BasePath);
_server.setProcessName(Key);
_server.HttpRequestHandler = new DefaultRequestHandler();
_server.ReceivedRequestEvent += ReceivedRequestEventHandler;
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
@@ -104,8 +102,12 @@ public class WebApiServer : IKeyName
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
if (ethernetEventArgs.EthernetEventType != eEthernetEventType.LinkUp)
{
return;
}
if (IsRegistered)
{
this.LogInformation("Ethernet link up. Server is already registered.");
return;
@@ -116,14 +118,14 @@ public class WebApiServer : IKeyName
Start();
}
/// <summary>
/// Initialize method
/// </summary>
public void Initialize(string key, string basePath)
{
Key = key;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
}
// /// <summary>
// /// Initialize method
// /// </summary>
// public void Initialize(string key, string basePath)
// {
// Key = key;
// BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
// }
/// <summary>
/// Adds a route to CWS
@@ -158,6 +160,22 @@ public class WebApiServer : IKeyName
_server.Routes.Remove(route);
}
/// <summary>
/// Sets the fallback request handler that is invoked when no registered route
/// matches an incoming request. Must be called before <see cref="Start"/>.
/// </summary>
/// <param name="handler">The handler to use as the server-level fallback.</param>
public void SetFallbackHandler(IHttpCwsHandler handler)
{
if (handler == null)
{
this.LogWarning("SetFallbackHandler: handler parameter is null, ignoring");
return;
}
_server.HttpRequestHandler = handler;
}
/// <summary>
/// GetRouteCollection method
/// </summary>
@@ -214,12 +232,10 @@ public class WebApiServer : IKeyName
return;
}
IsRegistered = _server.Unregister() == false;
var unregistered = _server.Unregister();
IsRegistered = !unregistered;
this.LogDebug("Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
_server.Dispose();
_server = null;
this.LogDebug("Stopping server, unregistration {0}", unregistered ? "was successful" : "failed");
}
catch (Exception ex)
{
@@ -240,13 +256,12 @@ public class WebApiServer : IKeyName
{
try
{
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
var req = args.Context?.Request;
this.LogVerbose("ReceivedRequestEventHandler: {Method} {Path}", req?.HttpMethod, req?.Path);
}
catch (Exception ex)
{
this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
this.LogVerbose("ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
}
}
}

View File

@@ -1,86 +0,0 @@
using System;
namespace PepperDash.Core.WebApi.Presets;
/// <summary>
/// Represents a preset
/// </summary>
public class Preset
{
/// <summary>
/// ID of preset
/// </summary>
public int Id { get; set; }
/// <summary>
/// User ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Room Type ID
/// </summary>
public int RoomTypeId { get; set; }
/// <summary>
/// Preset Name
/// </summary>
public string PresetName { get; set; }
/// <summary>
/// Preset Number
/// </summary>
public int PresetNumber { get; set; }
/// <summary>
/// Preset Data
/// </summary>
public string Data { get; set; }
/// <summary>
/// Constructor
/// </summary>
public Preset()
{
PresetName = "";
PresetNumber = 1;
Data = "{}";
}
}
/// <summary>
/// Represents a PresetReceivedEventArgs
/// </summary>
public class PresetReceivedEventArgs : EventArgs
{
/// <summary>
/// True when the preset is found
/// </summary>
public bool LookupSuccess { get; private set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
/// <summary>
/// The preset
/// </summary>
public Preset Preset { get; private set; }
/// <summary>
/// For Simpl+
/// </summary>
public PresetReceivedEventArgs() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="preset"></param>
/// <param name="success"></param>
public PresetReceivedEventArgs(Preset preset, bool success)
{
LookupSuccess = success;
Preset = preset;
}
}

View File

@@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.WebApi.Presets;
/// <summary>
///
/// </summary>
public class User
{
/// <summary>
///
/// </summary>
public int Id { get; set; }
/// <summary>
///
/// </summary>
public string ExternalId { get; set; }
/// <summary>
///
/// </summary>
public string FirstName { get; set; }
/// <summary>
///
/// </summary>
public string LastName { get; set; }
}
/// <summary>
///
/// </summary>
public class UserReceivedEventArgs : EventArgs
{
/// <summary>
/// True when user is found
/// </summary>
public bool LookupSuccess { get; private set; }
/// <summary>
/// For stupid S+
/// </summary>
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
/// <summary>
///
/// </summary>
public User User { get; private set; }
/// <summary>
/// For Simpl+
/// </summary>
public UserReceivedEventArgs() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="user"></param>
/// <param name="success"></param>
public UserReceivedEventArgs(User user, bool success)
{
LookupSuccess = success;
User = user;
}
}
/// <summary>
/// Represents a UserAndRoomMessage
/// </summary>
public class UserAndRoomMessage
{
/// <summary>
///
/// </summary>
public int UserId { get; set; }
/// <summary>
///
/// </summary>
public int RoomTypeId { get; set; }
/// <summary>
///
/// </summary>
public int PresetNumber { get; set; }
}

View File

@@ -1,280 +0,0 @@
extern alias NewtonsoftJson;
using System;
using Crestron.SimplSharp; // For Basic SIMPL# Classes
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.Net.Https;
using JsonConvert = NewtonsoftJson::Newtonsoft.Json.JsonConvert;
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
using PepperDash.Core.JsonToSimpl;
namespace PepperDash.Core.WebApi.Presets;
/// <summary>
/// Passcode client for the WebApi
/// </summary>
public class WebApiPasscodeClient : IKeyed
{
/// <summary>
/// Notifies when user received
/// </summary>
public event EventHandler<UserReceivedEventArgs> UserReceived;
/// <summary>
/// Notifies when Preset received
/// </summary>
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
/// <summary>
/// Unique identifier for this instance
/// </summary>
public string Key { get; private set; }
//string JsonMasterKey;
/// <summary>
/// An embedded JsonToSimpl master object.
/// </summary>
JsonToSimplGenericMaster J2SMaster;
string UrlBase;
string DefaultPresetJsonFilePath;
User CurrentUser;
Preset CurrentPreset;
/// <summary>
/// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please
/// use an Initialize method
/// </summary>
public WebApiPasscodeClient()
{
}
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="key"></param>
/// <param name="jsonMasterKey"></param>
/// <param name="urlBase"></param>
/// <param name="defaultPresetJsonFilePath"></param>
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
{
Key = key;
//JsonMasterKey = jsonMasterKey;
UrlBase = urlBase;
DefaultPresetJsonFilePath = defaultPresetJsonFilePath;
J2SMaster = new JsonToSimplGenericMaster();
J2SMaster.SaveCallback = this.SaveCallback;
J2SMaster.Initialize(jsonMasterKey);
}
/// <summary>
/// Gets the user for a passcode
/// </summary>
/// <param name="passcode"></param>
public void GetUserForPasscode(string passcode)
{
// Bullshit duplicate code here... These two cases should be the same
// except for https/http and the certificate ignores
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
var jo = new JObject();
jo.Add("pin", passcode);
req.ContentString = jo.ToString();
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
var resp = client.Dispatch(req);
var handler = UserReceived;
if (resp.Code == 200)
{
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
CurrentUser = user;
if (handler != null)
UserReceived(this, new UserReceivedEventArgs(user, true));
}
else
if (handler != null)
UserReceived(this, new UserReceivedEventArgs(null, false));
}
/// <summary>
///
/// </summary>
/// <param name="roomTypeId"></param>
/// <param name="presetNumber"></param>
/// <summary>
/// GetPresetForThisUser method
/// </summary>
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
{
if (CurrentUser == null)
{
CrestronConsole.PrintLine("GetPresetForThisUser no user loaded");
return;
}
var msg = new UserAndRoomMessage
{
UserId = CurrentUser.Id,
RoomTypeId = roomTypeId,
PresetNumber = presetNumber
};
var handler = PresetReceived;
try
{
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
req.ContentString = JsonConvert.SerializeObject(msg);
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
// ask for the preset
var resp = client.Dispatch(req);
if (resp.Code == 200) // got it
{
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
CurrentPreset = preset;
//if there's no preset data, load the template
if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0)
{
//Debug.Console(1, this, "Loaded preset has no data. Loading default template.");
LoadDefaultPresetData();
return;
}
J2SMaster.LoadWithJson(preset.Data);
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(preset, true));
}
else // no existing preset
{
CurrentPreset = new Preset();
LoadDefaultPresetData();
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(null, false));
}
}
catch (HttpException e)
{
var resp = e.Response;
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
LoadDefaultPresetData();
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(null, false));
}
}
void LoadDefaultPresetData()
{
CurrentPreset = null;
if (!File.Exists(DefaultPresetJsonFilePath))
{
Debug.Console(0, this, "Cannot load default preset file. Saving will not work");
return;
}
using (StreamReader sr = new StreamReader(DefaultPresetJsonFilePath))
{
try
{
var data = sr.ReadToEnd();
J2SMaster.SetJsonWithoutEvaluating(data);
CurrentPreset = new Preset() { Data = data, UserId = CurrentUser.Id };
}
catch (Exception e)
{
Debug.Console(0, this, "Error reading default preset JSON: \r{0}", e);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="roomTypeId"></param>
/// <param name="presetNumber"></param>
/// <summary>
/// SavePresetForThisUser method
/// </summary>
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
{
if (CurrentPreset == null)
LoadDefaultPresetData();
//return;
//// A new preset needs to have its numbers set
//if (CurrentPreset.IsNewPreset)
//{
CurrentPreset.UserId = CurrentUser.Id;
CurrentPreset.RoomTypeId = roomTypeId;
CurrentPreset.PresetNumber = presetNumber;
//}
J2SMaster.Save(); // Will trigger callback when ready
}
/// <summary>
/// After save operation on JSON master happens, send it to server
/// </summary>
/// <param name="json"></param>
void SaveCallback(string json)
{
CurrentPreset.Data = json;
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
try
{
var resp = client.Dispatch(req);
// 201=created
// 204=empty content
if (resp.Code == 201)
CrestronConsole.PrintLine("Preset added");
else if (resp.Code == 204)
CrestronConsole.PrintLine("Preset updated");
else if (resp.Code == 209)
CrestronConsole.PrintLine("Preset already exists. Cannot save as new.");
else
CrestronConsole.PrintLine("Preset save failed: {0}\r", resp.Code, resp.ContentString);
}
catch (HttpException e)
{
CrestronConsole.PrintLine("Preset save exception {0}", e.Response.Code);
}
}
}

View File

@@ -0,0 +1,73 @@
using FluentAssertions;
using PepperDash.Core.Abstractions;
using PepperDash.Core.Tests.Fakes;
using Xunit;
namespace PepperDash.Essentials.Core.Tests.Config;
/// <summary>
/// Tests for the configuration loading abstractions.
/// These verify behaviour of the test fakes and interfaces independently of
/// any Crestron SDK types (ConfigReader itself will be tested here once it
/// is migrated from Crestron.SimplSharp.CrestronIO to System.IO — see plan Phase 4).
/// </summary>
public class ConfigServiceFakesTests
{
[Fact]
public void DataStore_MultipleKeys_AreStoredIndependently()
{
var store = new InMemoryCrestronDataStore();
store.InitStore();
store.SetLocalInt("KeyA", 1);
store.SetLocalInt("KeyB", 2);
store.TryGetLocalInt("KeyA", out var a);
store.TryGetLocalInt("KeyB", out var b);
a.Should().Be(1);
b.Should().Be(2);
}
[Fact]
public void DataStore_OverwriteKey_ReturnsNewValue()
{
var store = new InMemoryCrestronDataStore();
store.SetLocalInt("Level", 1);
store.SetLocalInt("Level", 5);
store.TryGetLocalInt("Level", out var level);
level.Should().Be(5);
}
[Fact]
public void FakeEnvironment_CanBeConfiguredForServer()
{
var env = new FakeCrestronEnvironment
{
DevicePlatform = DevicePlatform.Server,
ApplicationNumber = 1,
RoomId = 42,
};
env.DevicePlatform.Should().Be(DevicePlatform.Server);
env.RoomId.Should().Be(42u);
}
[Fact]
public void FakeEthernetHelper_SeedAndRetrieve()
{
var eth = new FakeEthernetHelper()
.Seed(EthernetParameterType.GetCurrentIpAddress, "192.168.1.100")
.Seed(EthernetParameterType.GetHostname, "MC4-TEST");
eth.GetEthernetParameter(EthernetParameterType.GetCurrentIpAddress, 0)
.Should().Be("192.168.1.100");
eth.GetEthernetParameter(EthernetParameterType.GetHostname, 0)
.Should().Be("MC4-TEST");
eth.GetEthernetParameter(EthernetParameterType.GetDomainName, 0)
.Should().BeEmpty("unseeded parameter should return empty string");
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
</ItemGroup>
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
<!-- Reference Core.Tests to share the Fakes folder -->
<ProjectReference Include="..\PepperDash.Core.Tests\PepperDash.Core.Tests.csproj" />
</ItemGroup>
</Project>

View File

@@ -57,7 +57,7 @@ public class EiscApiAdvanced : BridgeApi, ICommunicationMonitor
AddPostActivationAction(RegisterEisc);
}
public override bool CustomActivate()
protected override bool CustomActivate()
{
CommunicationMonitor.Start();
return base.CustomActivate();

View File

@@ -57,7 +57,7 @@ namespace PepperDash.Essentials.Core;
/// CustomActivate method
/// </summary>
/// <inheritdoc />
public override bool CustomActivate()
protected override bool CustomActivate()
{
Communication.Connect();
CommunicationMonitor.StatusChange += (o, a) => { Debug.LogMessage(LogEventLevel.Verbose, this, "Communication monitor state: {0}", CommunicationMonitor.Status); };

View File

@@ -123,68 +123,48 @@ namespace PepperDash.Essentials.Core.Config;
{
Debug.LogMessage(LogEventLevel.Information, "Loading config file: '{0}'", filePath);
var fileContents = fs.ReadToEnd();
if (localConfigFound)
{
ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject<EssentialsConfig>();
if (localConfigFound)
{
ConfigObject = JObject.Parse(fs.ReadToEnd()).ToObject<EssentialsConfig>();
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config");
return true;
}
else
{
var parsedConfig = JObject.Parse(fs.ReadToEnd());
// Check if it's a v2 config (check for "version" node)
// this means it's already merged by the Portal API
// from the v2 config tool
var isV2Config = parsedConfig["versions"] != null;
if (isV2Config)
{
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config");
return true;
}
// Extract SystemUrl and TemplateUrl into final config output
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
if (parsedConfig["system_url"] != null)
{
ConfigObject.SystemUrl = parsedConfig["system_url"].Value<string>();
}
if (parsedConfig["template_url"] != null)
{
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
}
}
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Merged Config");
return true;
ConfigObject = JObject.Parse(fileContents).ToObject<EssentialsConfig>();
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config");
return ConfigObject != null;
}
else
{
var doubleObj = JObject.Parse(fs.ReadToEnd());
ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject<EssentialsConfig>();
var parsedConfig = JObject.Parse(fileContents);
// Extract SystemUrl and TemplateUrl into final config output
// Check if it's a v2 config (check for "version" node)
// this means it's already merged by the Portal API
// from the v2 config tool
var isV2Config = parsedConfig["versions"] != null;
if (doubleObj["system_url"] != null)
if (isV2Config)
{
ConfigObject.SystemUrl = doubleObj["system_url"].Value<string>();
Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary.");
ConfigObject = parsedConfig.ToObject<EssentialsConfig>();
Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config");
return ConfigObject != null;
}
if (doubleObj["template_url"] != null)
// Extract SystemUrl and TemplateUrl into final config output
ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject<EssentialsConfig>();
if (ConfigObject == null)
{
ConfigObject.TemplateUrl = doubleObj["template_url"].Value<string>();
Debug.LogMessage(LogEventLevel.Warning, "Config merge produced a null ConfigObject.");
return false;
}
if (parsedConfig["system_url"] != null)
{
ConfigObject.SystemUrl = parsedConfig["system_url"].Value<string>();
}
if (parsedConfig["template_url"] != null)
{
ConfigObject.TemplateUrl = parsedConfig["template_url"].Value<string>();
}
}

View File

@@ -1,259 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharpPro.Diagnostics;
using PepperDash.Core;
using Serilog.Events;
namespace PepperDash.Essentials.Core.Config;
public static class ConfigUpdater
{
public static event EventHandler<ConfigStatusEventArgs> ConfigStatusChanged;
public static void GetConfigFromServer(string url)
{
Debug.LogMessage(LogEventLevel.Information, "Attempting to get new config from '{0}'", url);
// HTTP GET
var req = new HttpClientRequest();
try
{
req.RequestType = RequestType.Get;
req.Url.Parse(url);
new HttpClient().DispatchAsync(req, (r, e) =>
{
if (e == HTTP_CALLBACK_ERROR.COMPLETED)
{
if (r.Code == 200)
{
var newConfig = r.ContentString;
OnStatusUpdate(eUpdateStatus.ConfigFileReceived);
ArchiveExistingPortalConfigs();
CheckForLocalConfigAndDelete();
WriteConfigToFile(newConfig);
RestartProgram();
}
else
{
Debug.LogMessage(LogEventLevel.Information, "Config Update Process Stopped. Failed to get config file from server: {0}", r.Code);
OnStatusUpdate(eUpdateStatus.UpdateFailed);
}
}
else
Debug.LogMessage(LogEventLevel.Information, "Request for config from Server Failed: {0}", e);
});
}
catch (Exception e)
{
Debug.LogMessage(LogEventLevel.Debug, "Error Getting Config from Server: {0}", e);
}
}
static void OnStatusUpdate(eUpdateStatus status)
{
var handler = ConfigStatusChanged;
if(handler != null)
{
handler(typeof(ConfigUpdater), new ConfigStatusEventArgs(status));
}
}
static void WriteConfigToFile(string configData)
{
var filePath = Global.FilePathPrefix+ "configurationFile-updated.json";
try
{
var config = JObject.Parse(configData).ToObject<EssentialsConfig>();
ConfigWriter.WriteFile(filePath, configData);
OnStatusUpdate(eUpdateStatus.WritingConfigFile);
}
catch (Exception e)
{
Debug.LogMessage(LogEventLevel.Debug, "Error parsing new config: {0}", e);
OnStatusUpdate(eUpdateStatus.UpdateFailed);
}
}
/// <summary>
/// Checks for any existing portal config files and archives them
/// </summary>
static void ArchiveExistingPortalConfigs()
{
var filePath = Global.FilePathPrefix + Global.ConfigFileName;
var configFiles = ConfigReader.GetConfigFiles(filePath);
if (configFiles != null)
{
Debug.LogMessage(LogEventLevel.Information, "Existing config files found. Moving to Archive folder.");
OnStatusUpdate(eUpdateStatus.ArchivingConfigs);
MoveFilesToArchiveFolder(configFiles);
}
else
{
Debug.LogMessage(LogEventLevel.Information, "No Existing config files found in '{0}'. Nothing to archive", filePath);
}
}
/// <summary>
/// Checks for presence of archive folder and if found deletes contents.
/// Moves any config files to the archive folder and adds a .bak suffix
/// </summary>
/// <param name="files"></param>
static void MoveFilesToArchiveFolder(FileInfo[] files)
{
string archiveDirectoryPath = Global.FilePathPrefix + "archive";
if (!Directory.Exists(archiveDirectoryPath))
{
// Directory does not exist, create it
Directory.Create(archiveDirectoryPath);
}
else
{
// Directory exists, first clear any contents
var archivedConfigFiles = ConfigReader.GetConfigFiles(archiveDirectoryPath + Global.DirectorySeparator + Global.ConfigFileName + ".bak");
if(archivedConfigFiles != null || archivedConfigFiles.Length > 0)
{
Debug.LogMessage(LogEventLevel.Information, "{0} Existing files found in archive folder. Deleting.", archivedConfigFiles.Length);
for (int i = 0; i < archivedConfigFiles.Length; i++ )
{
var file = archivedConfigFiles[i];
Debug.LogMessage(LogEventLevel.Information, "Deleting archived file: '{0}'", file.FullName);
file.Delete();
}
}
}
// Move any files from the program folder to the archive folder
foreach (var file in files)
{
Debug.LogMessage(LogEventLevel.Information, "Moving config file '{0}' to archive folder", file.FullName);
// Moves the file and appends the .bak extension
var fileDest = archiveDirectoryPath + "/" + file.Name + ".bak";
if(!File.Exists(fileDest))
{
file.MoveTo(fileDest);
}
else
Debug.LogMessage(LogEventLevel.Information, "Cannot move file to archive folder. Existing file already exists with same name: '{0}'", fileDest);
}
}
/// <summary>
/// Checks for LocalConfig folder in file system and deletes if found
/// </summary>
static void CheckForLocalConfigAndDelete()
{
var folderPath = Global.FilePathPrefix + ConfigWriter.LocalConfigFolder;
if (Directory.Exists(folderPath))
{
OnStatusUpdate(eUpdateStatus.DeletingLocalConfig);
Directory.Delete(folderPath);
Debug.LogMessage(LogEventLevel.Information, "Local Config Found in '{0}'. Deleting.", folderPath);
}
}
/// <summary>
/// Connects to the processor via SSH and restarts the program
/// </summary>
static void RestartProgram()
{
Debug.LogMessage(LogEventLevel.Information, "Attempting to Reset Program");
OnStatusUpdate(eUpdateStatus.RestartingProgram);
string response = string.Empty;
CrestronConsole.SendControlSystemCommand(string.Format("progreset -p:{0}", InitialParametersClass.ApplicationNumber), ref response);
Debug.LogMessage(LogEventLevel.Debug, "Console Response: {0}", response);
}
}
/// <summary>
/// Enumeration of eUpdateStatus values
/// </summary>
public enum eUpdateStatus
{
/// <summary>
/// UpdateStarted status
/// </summary>
UpdateStarted,
/// <summary>
/// ConfigFileReceived status
/// </summary>
ConfigFileReceived,
/// <summary>
/// ArchivingConfigs status
/// </summary>
ArchivingConfigs,
/// <summary>
/// DeletingLocalConfig status
/// </summary>
DeletingLocalConfig,
/// <summary>
/// WritingConfigFile status
/// </summary>
WritingConfigFile,
/// <summary>
/// RestartingProgram status
/// </summary>
RestartingProgram,
/// <summary>
/// UpdateSucceeded status
/// </summary>
UpdateSucceeded,
/// <summary>
/// UpdateFailed status
/// </summary>
UpdateFailed
}
public class ConfigStatusEventArgs : EventArgs
{
public eUpdateStatus UpdateStatus { get; private set; }
public ConfigStatusEventArgs(eUpdateStatus status)
{
UpdateStatus = status;
}
}

View File

@@ -87,7 +87,7 @@ namespace PepperDash.Essentials.Core;
/// CustomActivate method
/// </summary>
/// <inheritdoc />
public override bool CustomActivate()
protected override bool CustomActivate()
{
Debug.LogMessage(LogEventLevel.Information, this, "Activating");
if (!PreventRegistration)

View File

@@ -36,7 +36,7 @@ namespace PepperDash.Essentials.Core
/// Make sure that overriding classes call this!
/// Registers the Crestron device, connects up to the base events, starts communication monitor
/// </summary>
public override bool CustomActivate()
protected override bool CustomActivate()
{
Debug.LogMessage(LogEventLevel.Information, this, "Activating");
var response = Hardware.RegisterWithLogging(Key);

View File

@@ -1,60 +0,0 @@
using System;
namespace PepperDash.Essentials.Core.Devices.DeviceTypeInterfaces
{
/// <summary>
/// Defines the contract for IDisplayBasic
/// </summary>
[Obsolete("This interface is no longer used and will be removed in a future version. Please use IDisplay instead.")]
public interface IDisplayBasic
{
/// <summary>
/// Sets the input to HDMI 1
/// </summary>
void InputHdmi1();
/// <summary>
/// Sets the input to HDMI 2
/// </summary>
void InputHdmi2();
/// <summary>
/// Sets the input to HDMI 3
/// </summary>
void InputHdmi3();
/// <summary>
/// Sets the input to HDMI 4
/// </summary>
void InputHdmi4();
/// <summary>
/// Sets the input to DisplayPort 1
/// </summary>
void InputDisplayPort1();
/// <summary>
/// Sets the input to DVI 1
/// </summary>
void InputDvi1();
/// <summary>
/// Sets the input to Video 1
/// </summary>
void InputVideo1();
/// <summary>
/// Sets the input to VGA 1
/// </summary>
void InputVga1();
/// <summary>
/// Sets the input to VGA 2
/// </summary>
void InputVga2();
/// <summary>
/// Sets the input to RGB 1
/// </summary>
void InputRgb1();
}
}

View File

@@ -2,6 +2,7 @@
using Newtonsoft.Json;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Routing;
namespace PepperDash.Essentials.Core;
@@ -11,23 +12,22 @@ namespace PepperDash.Essentials.Core;
/// </summary>
public class DestinationListItem
{
/// <summary>
/// Gets or sets the key identifier for the sink device that this destination represents.
/// </summary>
[JsonProperty("sinkKey")]
public string SinkKey { get; set; }
private EssentialsDevice _sinkDevice;
private IRoutingSink _sinkDevice;
/// <summary>
/// Gets the actual device instance for this destination.
/// Lazily loads the device from the DeviceManager using the SinkKey.
/// </summary>
[JsonIgnore]
public EssentialsDevice SinkDevice
public IRoutingSink SinkDevice
{
get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as EssentialsDevice); }
get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as IRoutingSink); }
}
/// <summary>

View File

@@ -87,7 +87,7 @@ public abstract class EssentialsDevice : Device
/// Override this method to perform any initialization that requires all devices to be activated. This method is called automatically after the DeviceManager.AllDevicesActivated event is fired, and should not be called directly.
/// </summary>
/// <returns></returns>
public override bool CustomActivate()
protected override bool CustomActivate()
{
CreateMobileControlMessengers();

View File

@@ -63,7 +63,7 @@ namespace PepperDash.Essentials.Core.Devices;
/// CustomActivate method
/// </summary>
/// <inheritdoc />
public override bool CustomActivate()
protected override bool CustomActivate()
{
CommunicationMonitor.Start();
return true;

View File

@@ -43,17 +43,17 @@ public class SourceListItem
/// Returns the source Device for this, if it exists in DeviceManager
/// </summary>
[JsonIgnore]
public Device SourceDevice
public IRoutingSource SourceDevice
{
get
{
if (_SourceDevice == null)
_SourceDevice = DeviceManager.GetDeviceForKey(SourceKey) as Device;
_SourceDevice = DeviceManager.GetDeviceForKey<IRoutingSource>(SourceKey);
return _SourceDevice;
}
}
private Device _SourceDevice;
private IRoutingSource _SourceDevice;
/// <summary>
/// Gets either the source's Name or this AlternateName property, if

View File

@@ -1,94 +0,0 @@
using System;
namespace PepperDash.Essentials.Core;
/// <summary>
/// Defines the eSourceListItemDestinationTypes enumeration, which represents the various destination types for source list items in a room control system.
/// This enumeration is marked as obsolete, indicating that it may be removed in future versions and should not be used in new development.
/// Each member of the enumeration corresponds to a specific type of display or audio output commonly found in room control systems,
/// such as default displays, program audio, codec content, and auxiliary displays.
/// </summary>
[Obsolete]
public enum eSourceListItemDestinationTypes
{
/// <summary>
/// Default display, used for the main video output in a room
/// </summary>
defaultDisplay,
/// <summary>
/// Left display
/// </summary>
leftDisplay,
/// <summary>
/// Right display
/// </summary>
rightDisplay,
/// <summary>
/// Center display
/// </summary>
centerDisplay,
/// <summary>
/// Program audio, used for the main audio output in a room
/// </summary>
programAudio,
/// <summary>
/// Codec content, used for sharing content to the far end in a video call
/// </summary>
codecContent,
/// <summary>
/// Front left display, used for rooms with multiple displays
/// </summary>
frontLeftDisplay,
/// <summary>
/// Front right display, used for rooms with multiple displays
/// </summary>
frontRightDisplay,
/// <summary>
/// Rear left display, used for rooms with multiple displays
/// </summary>
rearLeftDisplay,
/// <summary>
/// Rear right display, used for rooms with multiple displays
/// </summary>
rearRightDisplay,
/// <summary>
/// Auxiliary display 1, used for additional displays in a room
/// </summary>
auxDisplay1,
/// <summary>
/// Auxiliary display 2, used for additional displays in a room
/// </summary>
auxDisplay2,
/// <summary>
/// Auxiliary display 3, used for additional displays in a room
/// </summary>
auxDisplay3,
/// <summary>
/// Auxiliary display 4, used for additional displays in a room
/// </summary>
auxDisplay4,
/// <summary>
/// Auxiliary display 5, used for additional displays in a room
/// </summary>
auxDisplay5,
/// <summary>
/// Auxiliary display 6, used for additional displays in a room
/// </summary>
auxDisplay6,
/// <summary>
/// Auxiliary display 7, used for additional displays in a room
/// </summary>
auxDisplay7,
/// <summary>
/// Auxiliary display 8, used for additional displays in a room
/// </summary>
auxDisplay8,
/// <summary>
/// Auxiliary display 9, used for additional displays in a room
/// </summary>
auxDisplay9,
/// <summary>
/// Auxiliary display 10, used for additional displays in a room
/// </summary>
auxDisplay10,
}

View File

@@ -1,4 +1,4 @@
using Crestron.SimplSharp;
using System.Timers;
namespace PepperDash.Essentials.Core
{
/// <summary>
@@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Core
/// Gets or sets the Feedback
/// </summary>
public BoolFeedback Feedback { get; private set; }
CTimer Timer;
Timer Timer;
bool _BoolValue;
@@ -51,16 +51,22 @@ namespace PepperDash.Essentials.Core
{
_BoolValue = true;
Feedback.FireUpdate();
Timer = new CTimer(o =>
Timer = new Timer(TimeoutMs) { AutoReset = false };
Timer.Elapsed += (s, e) =>
{
_BoolValue = false;
Feedback.FireUpdate();
Timer = null;
}, TimeoutMs);
};
Timer.Start();
}
// Timer is running, if retrigger is set, reset it.
else if (CanRetrigger)
Timer.Reset(TimeoutMs);
{
Timer.Stop();
Timer.Interval = TimeoutMs;
Timer.Start();
}
}
/// <summary>
@@ -69,7 +75,11 @@ namespace PepperDash.Essentials.Core
public void Cancel()
{
if (Timer != null)
Timer.Reset(0);
{
Timer.Stop();
Timer.Interval = 1;
Timer.Start();
}
}
}
}

View File

@@ -2,8 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using System.Timers;
namespace PepperDash.Essentials.Core;
/// <summary>
@@ -21,7 +20,7 @@ namespace PepperDash.Essentials.Core;
/// Gets the Feedback
/// </summary>
public BoolFeedback Feedback { get; private set; }
CTimer Timer;
Timer Timer;
/// <summary>
/// When set to true, will cause Feedback to go high, and cancel the timer.
@@ -49,7 +48,11 @@ namespace PepperDash.Essentials.Core;
else
{
if (Timer == null)
Timer = new CTimer(o => ClearFeedback(), TimeoutMs);
{
Timer = new Timer(TimeoutMs) { AutoReset = false };
Timer.Elapsed += (s, e) => ClearFeedback();
Timer.Start();
}
}
}
}

View File

@@ -43,12 +43,6 @@ namespace PepperDash.Essentials.Core
/// </summary>
public bool InTestMode { get; protected set; }
/// <summary>
/// Base Constructor - empty
/// </summary>
[Obsolete("use constructor with Key parameter. This constructor will be removed in a future version")]
protected Feedback() : this(null) { }
/// <summary>
/// Constructor with Key parameter
/// </summary>

View File

@@ -13,7 +13,9 @@ namespace PepperDash.Essentials.Core;
/// </summary>
public class SerialFeedback : Feedback
{
public override string SerialValue { get { return _SerialValue; } }
/// <inheritdoc />
public override string SerialValue { get { return _SerialValue; } }
string _SerialValue;
//public override eCueType Type { get { return eCueType.Serial; } }
@@ -25,20 +27,20 @@ public class SerialFeedback : Feedback
List<StringInputSig> LinkedInputSigs = new List<StringInputSig>();
public SerialFeedback()
{
}
/// <inheritdoc />
public SerialFeedback(string key)
: base(key)
{
}
/// <inheritdoc />
public override void FireUpdate()
{
throw new NotImplementedException("This feedback type does not use Funcs");
}
/// <inheritdoc />
public void FireUpdate(string newValue)
{
_SerialValue = newValue;
@@ -46,17 +48,20 @@ public class SerialFeedback : Feedback
OnOutputChange(newValue);
}
/// <inheritdoc />
public void LinkInputSig(StringInputSig sig)
{
LinkedInputSigs.Add(sig);
UpdateSig(sig);
}
/// <inheritdoc />
public void UnlinkInputSig(StringInputSig sig)
{
LinkedInputSigs.Remove(sig);
}
/// <inheritdoc />
public override string ToString()
{
return (InTestMode ? "TEST -- " : "") + SerialValue;

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using Timer = System.Timers.Timer;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
@@ -62,7 +63,7 @@ namespace PepperDash.Essentials.Core.Fusion
private Event _currentMeeting;
private RoomSchedule _currentSchedule;
private CTimer _dailyTimeRequestTimer;
private Timer _dailyTimeRequestTimer;
private StatusMonitorCollection _errorMessageRollUp;
private FusionRoomGuids _guids;
@@ -70,9 +71,9 @@ namespace PepperDash.Essentials.Core.Fusion
private bool _isRegisteredForSchedulePushNotifications;
private Event _nextMeeting;
private CTimer _pollTimer;
private Timer _pollTimer;
private CTimer _pushNotificationTimer;
private Timer _pushNotificationTimer;
private string _roomOccupancyRemoteString;
@@ -308,7 +309,7 @@ namespace PepperDash.Essentials.Core.Fusion
}
/// <inheritdoc />
public override void Initialize()
protected override void Initialize()
{
GenerateGuidFile(GetGuidFilePath(_config.IpIdInt));
@@ -500,12 +501,6 @@ namespace PepperDash.Essentials.Core.Fusion
// Moved to
CurrentRoomSourceNameSig = FusionRoom.CreateOffsetStringSig(JoinMap.Display1CurrentSourceName.JoinNumber, JoinMap.Display1CurrentSourceName.AttributeName,
eSigIoMask.InputSigOnly);
// Don't think we need to get current status of this as nothing should be alive yet.
if (Room is IHasCurrentSourceInfoChange hasCurrentSourceInfoChange)
{
hasCurrentSourceInfoChange.CurrentSourceChange += Room_CurrentSourceInfoChange;
}
FusionRoom.SystemPowerOn.OutputSig.SetSigFalseAction(Room.PowerOnToDefaultOrLastSource);
FusionRoom.SystemPowerOff.OutputSig.SetSigFalseAction(() =>
@@ -729,15 +724,15 @@ namespace PepperDash.Essentials.Core.Fusion
RequestLocalDateTime(null);
// Setup timer to request time daily
if (_dailyTimeRequestTimer != null && !_dailyTimeRequestTimer.Disposed)
if (_dailyTimeRequestTimer != null)
{
_dailyTimeRequestTimer.Stop();
_dailyTimeRequestTimer.Dispose();
}
_dailyTimeRequestTimer = new CTimer(RequestLocalDateTime, null, 86400000, 86400000);
_dailyTimeRequestTimer.Reset(86400000, 86400000);
_dailyTimeRequestTimer = new Timer(86400000) { AutoReset = true };
_dailyTimeRequestTimer.Elapsed += (s, e) => RequestLocalDateTime(null);
_dailyTimeRequestTimer.Start();
});
}
@@ -950,25 +945,25 @@ namespace PepperDash.Essentials.Core.Fusion
{
case 1:
_isRegisteredForSchedulePushNotifications = true;
if (_pollTimer != null && !_pollTimer.Disposed)
if (_pollTimer != null)
{
_pollTimer.Stop();
_pollTimer.Dispose();
}
_pushNotificationTimer = new CTimer(RequestFullRoomSchedule, null,
PushNotificationTimeout, PushNotificationTimeout);
_pushNotificationTimer.Reset(PushNotificationTimeout, PushNotificationTimeout);
_pushNotificationTimer = new Timer(PushNotificationTimeout) { AutoReset = true };
_pushNotificationTimer.Elapsed += (s, e) => RequestFullRoomSchedule(null);
_pushNotificationTimer.Start();
break;
case 0:
_isRegisteredForSchedulePushNotifications = false;
if (_pushNotificationTimer != null && !_pushNotificationTimer.Disposed)
if (_pushNotificationTimer != null)
{
_pushNotificationTimer.Stop();
_pushNotificationTimer.Dispose();
}
_pollTimer = new CTimer(RequestFullRoomSchedule, null, SchedulePollInterval,
SchedulePollInterval);
_pollTimer.Reset(SchedulePollInterval, SchedulePollInterval);
_pollTimer = new Timer(SchedulePollInterval) { AutoReset = true };
_pollTimer.Elapsed += (s, e) => RequestFullRoomSchedule(null);
_pollTimer.Start();
break;
}
}
@@ -1121,7 +1116,10 @@ namespace PepperDash.Essentials.Core.Fusion
if (action.OuterXml.IndexOf("RequestSchedule", StringComparison.Ordinal) > -1)
{
_pushNotificationTimer.Reset(PushNotificationTimeout, PushNotificationTimeout);
_pushNotificationTimer.Stop();
_pushNotificationTimer.Interval = PushNotificationTimeout;
_pushNotificationTimer.Start();
Debug.LogMessage(LogEventLevel.Verbose, this, "Received push notification for schedule change");
}
}
else // Not a push notification
@@ -1177,7 +1175,9 @@ namespace PepperDash.Essentials.Core.Fusion
if (!_isRegisteredForSchedulePushNotifications)
{
_pollTimer.Reset(SchedulePollInterval, SchedulePollInterval);
_pollTimer.Stop();
_pollTimer.Interval = SchedulePollInterval;
_pollTimer.Start();
}
// Fire Schedule Change Event
@@ -1236,11 +1236,14 @@ namespace PepperDash.Essentials.Core.Fusion
uint i = 0;
foreach (var kvp in setTopBoxes)
{
TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice);
i++;
if (i > JoinMap.Display1SetTopBoxSourceStart.JoinSpan) // We only have five spots
if (kvp.Value.SourceDevice is Device device)
{
break;
TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, device);
i++;
if (i > JoinMap.Display1SetTopBoxSourceStart.JoinSpan) // We only have five spots
{
break;
}
}
}
@@ -1248,11 +1251,14 @@ namespace PepperDash.Essentials.Core.Fusion
i = 0;
foreach (var kvp in discPlayers)
{
TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice);
i++;
if (i > JoinMap.Display1DiscPlayerSourceStart.JoinSpan) // We only have five spots
if (kvp.Value.SourceDevice is Device device)
{
break;
TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, device);
i++;
if (i > JoinMap.Display1DiscPlayerSourceStart.JoinSpan) // We only have five spots
{
break;
}
}
}
@@ -1260,11 +1266,14 @@ namespace PepperDash.Essentials.Core.Fusion
i = 0;
foreach (var kvp in laptops)
{
TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice);
i++;
if (i > JoinMap.Display1LaptopSourceStart.JoinSpan) // We only have ten spots???
if (kvp.Value.SourceDevice is Device device)
{
break;
TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, device);
i++;
if (i > JoinMap.Display1LaptopSourceStart.JoinSpan) // We only have ten spots???
{
break;
}
}
}
@@ -1734,36 +1743,6 @@ namespace PepperDash.Essentials.Core.Fusion
return Convert.ToInt32(capture.Groups[1].Value);
}
/// <summary>
/// Event handler for when room source changes
/// </summary>
protected void Room_CurrentSourceInfoChange(SourceListItem info, ChangeType type)
{
// Handle null. Nothing to do when switching from or to null
if (info == null || info.SourceDevice == null)
{
return;
}
var dev = info.SourceDevice;
if (type == ChangeType.WillChange)
{
if (_sourceToFeedbackSigs.ContainsKey(dev))
{
_sourceToFeedbackSigs[dev].BoolValue = false;
}
}
else
{
if (_sourceToFeedbackSigs.ContainsKey(dev))
{
_sourceToFeedbackSigs[dev].BoolValue = true;
}
//var name = (room == null ? "" : room.Name);
CurrentRoomSourceNameSig.InputSig.StringValue = info.SourceDevice.Name;
}
}
/// <summary>
/// Event handler for Fusion state changes
/// </summary>

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Essentials.Core.Interfaces;
/// <summary>
/// Defines the contract for ILogStrings
/// </summary>
public interface ILogStrings : IKeyed
{
/// <summary>
/// Defines a class that is capable of logging a string
/// </summary>
void SendToLog(IKeyed device, string logMessage);
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Essentials.Core.Interfaces;
/// <summary>
/// Defines the contract for ILogStringsWithLevel
/// </summary>
public interface ILogStringsWithLevel : IKeyed
{
/// <summary>
/// Defines a class that is capable of logging a string with an int level
/// </summary>
void SendToLog(IKeyed device, Debug.ErrorLogLevel level, string logMessage);
}

View File

@@ -51,16 +51,31 @@ public class MicrophonePrivacyController : EssentialsDevice
bool _enableLeds;
/// <summary>
/// Gets or sets the list of digital inputs that are used to toggle the privacy state. Each input is expected to be momentary contact closure that triggers a change in the privacy state when activated. The controller will subscribe to the OutputChange event of each input's InputStateFeedback to monitor for changes in the input state and respond accordingly by toggling the privacy state and updating the LED indicators if enabled.
/// </summary>
public List<IDigitalInput> Inputs { get; private set; }
/// <summary>
/// Gets or sets the GenericRelayDevice that is used to indicate the privacy state with a red LED. When the privacy mode is active, the red LED will be turned on to provide a visual indication of the privacy state. The controller will manage the state of this relay based on changes in the privacy mode, ensuring that it accurately reflects whether the privacy mode is currently active or not.
/// </summary>
public GenericRelayDevice RedLedRelay { get; private set; }
bool _redLedRelayState;
/// <summary>
/// Gets or sets the GenericRelayDevice that is used to indicate the privacy state with a green LED. When the privacy mode is inactive, the green LED will be turned on to provide a visual indication of the privacy state. The controller will manage the state of this relay based on changes in the privacy mode, ensuring that it accurately reflects whether the privacy mode is currently active or not.
/// </summary>
public GenericRelayDevice GreenLedRelay { get; private set; }
bool _greenLedRelayState;
/// <inheritdoc />
public IPrivacy PrivacyDevice { get; private set; }
/// <summary>
/// Constructor for the MicrophonePrivacyController class, which initializes the controller with the specified key and configuration. The constructor sets up the necessary properties and collections for managing the digital inputs, LED relays, and privacy device. It also prepares the controller for activation by ensuring that all required components are properly initialized and ready to be used when the controller is activated in the system.
/// </summary>
/// <param name="key"></param>
/// <param name="config"></param>
public MicrophonePrivacyController(string key, MicrophonePrivacyControllerConfig config) :
base(key)
{
@@ -69,7 +84,7 @@ public class MicrophonePrivacyController : EssentialsDevice
Inputs = new List<IDigitalInput>();
}
public override bool CustomActivate()
protected override bool CustomActivate()
{
foreach (var i in Config.Inputs)
{
@@ -105,13 +120,15 @@ public class MicrophonePrivacyController : EssentialsDevice
#region Overrides of Device
public override void Initialize()
/// <inheritdoc />
protected override void Initialize()
{
CheckPrivacyMode();
}
#endregion
/// <inheritdoc />
public void SetPrivacyDevice(IPrivacy privacyDevice)
{
PrivacyDevice = privacyDevice;
@@ -240,13 +257,21 @@ public class MicrophonePrivacyController : EssentialsDevice
}
}
/// <summary>
/// Factory for creating MicrophonePrivacyController devices
/// </summary>
public class MicrophonePrivacyControllerFactory : EssentialsDeviceFactory<MicrophonePrivacyController>
{
/// <summary>
/// Constructor for the MicrophonePrivacyControllerFactory class, which initializes the factory and sets up the necessary type names for device creation. This factory is responsible for creating instances of the MicrophonePrivacyController device based on the provided configuration and device key when requested by the system.
/// </summary>
public MicrophonePrivacyControllerFactory()
{
TypeNames = new List<string>() { "microphoneprivacycontroller" };
}
/// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new MIcrophonePrivacyController Device");

View File

@@ -7,6 +7,7 @@ using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
using System.ComponentModel;
using System.Timers;
using PepperDash.Core;
@@ -84,10 +85,8 @@ namespace PepperDash.Essentials.Core
long WarningTime;
long ErrorTime;
CTimer WarningTimer;
CTimer ErrorTimer;
/// <summary>
Timer WarningTimer;
Timer ErrorTimer;
/// Constructor
/// </summary>
/// <param name="parent">parent device</param>
@@ -155,8 +154,18 @@ namespace PepperDash.Essentials.Core
/// </summary>
protected void StartErrorTimers()
{
if (WarningTimer == null) WarningTimer = new CTimer(o => { Status = MonitorStatus.InWarning; }, WarningTime);
if (ErrorTimer == null) ErrorTimer = new CTimer(o => { Status = MonitorStatus.InError; }, ErrorTime);
if (WarningTimer == null)
{
WarningTimer = new Timer(WarningTime) { AutoReset = false };
WarningTimer.Elapsed += (s, e) => { Status = MonitorStatus.InWarning; };
WarningTimer.Start();
}
if (ErrorTimer == null)
{
ErrorTimer = new Timer(ErrorTime) { AutoReset = false };
ErrorTimer.Elapsed += (s, e) => { Status = MonitorStatus.InError; };
ErrorTimer.Start();
}
}
/// <summary>
@@ -176,10 +185,17 @@ namespace PepperDash.Essentials.Core
protected void ResetErrorTimers()
{
if (WarningTimer != null)
WarningTimer.Reset(WarningTime, WarningTime);
if (ErrorTimer != null)
ErrorTimer.Reset(ErrorTime, ErrorTime);
{
WarningTimer.Stop();
WarningTimer.Interval = WarningTime;
WarningTimer.Start();
}
if (ErrorTimer != null)
{
ErrorTimer.Stop();
ErrorTimer.Interval = ErrorTime;
ErrorTimer.Start();
}
}
}
}

View File

@@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters;
using PepperDash.Essentials.Core.Bridges;
using Serilog.Events;
using System.Threading.Tasks;
using System.Timers;
namespace PepperDash.Essentials.Core.Monitoring;
@@ -20,7 +21,7 @@ namespace PepperDash.Essentials.Core.Monitoring;
public class SystemMonitorController : EssentialsBridgeableDevice
{
private const long UptimePollTime = 300000;
private CTimer _uptimePollTimer;
private Timer _uptimePollTimer;
private string _uptime;
private string _lastStart;
@@ -147,7 +148,10 @@ public class SystemMonitorController : EssentialsBridgeableDevice
CreateEthernetStatusFeedbacks();
UpdateEthernetStatusFeeedbacks();
_uptimePollTimer = new CTimer(PollUptime, null, 0, UptimePollTime);
_uptimePollTimer = new Timer(UptimePollTime) { AutoReset = true };
_uptimePollTimer.Elapsed += (s, e) => PollUptime(null);
_uptimePollTimer.Start();
PollUptime(null);
SystemMonitor.ProgramChange += SystemMonitor_ProgramChange;
SystemMonitor.TimeZoneInformation.TimeZoneChange += TimeZoneInformation_TimeZoneChange;
@@ -164,6 +168,20 @@ public class SystemMonitorController : EssentialsBridgeableDevice
_uptimePollTimer = null;
}
/// <summary>
/// Polls the uptime and last start time from the control system and updates the feedbacks
/// This is necessary because there is no event that is fired when these values change, so we have to poll for them
/// at a regular interval
/// Uptime is also polled on activation to get initial values
/// Uptime is polled every 5 minutes (300000 ms) which should be often enough to keep the values updated without causing performance issues
/// Note: polling uptime can cause a delay in the feedback update, so it is done in a separate thread to avoid blocking the main thread
/// Note: this method uses CrestronConsole.SendControlSystemCommand which can be slow, so it is not recommended to call this method more often than every 5 minutes
/// Note: this method will not work on a server as the "uptime" command is not available, but it will not cause any issues either as it will just return an empty string
/// Note: if you need more real-time uptime updates, you could consider implementing a custom solution that tracks uptime internally and updates the feedbacks accordingly, but this would require more complex implementation and testing
/// Note: if you implement a custom solution for tracking uptime, you should still consider polling the uptime from the control system at a regular interval (e.g. every hour) to ensure that your internal tracking is accurate and to account for any potential issues that may arise with your custom implementation
/// Note: if you implement a custom solution for tracking uptime, you should also consider implementing a way to reset the uptime (e.g. on program start) to ensure that it reflects the actual uptime of the control system accurately
/// </summary>
/// <param name="obj"></param>
public void PollUptime(object obj)
{
var consoleResponse = string.Empty;
@@ -302,13 +320,15 @@ public class SystemMonitorController : EssentialsBridgeableDevice
}
}
public override bool CustomActivate()
/// <inheritdoc />
protected override bool CustomActivate()
{
RefreshSystemMonitorData();
return base.CustomActivate();
}
/// <inheritdoc />
public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
{
var joinMap = new SystemMonitorJoinMap(joinStart);
@@ -671,6 +691,9 @@ public class SystemMonitorController : EssentialsBridgeableDevice
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterIndex));
}
/// <summary>
/// Updates all the ethernet status feedbacks for this interface
/// </summary>
public void UpdateEthernetStatus()
{
HostNameFeedback.FireUpdate();
@@ -688,26 +711,77 @@ public class SystemMonitorController : EssentialsBridgeableDevice
}
}
/// <summary>
/// Represents a collection of feedbacks related to the status of a program running on the control system, including its operating state, registration state, and various pieces of information about the program such as its name, compile time, and environment. This class also includes methods for retrieving program information and responding to program change events to keep the feedbacks updated in real-time.
/// </summary>
public class ProgramStatusFeedbacks
{
/// <summary>
/// Event that is fired when any of the program information properties change, allowing external classes to respond to changes in program information in real-time
/// </summary>
public event EventHandler<ProgramInfoEventArgs> ProgramInfoChanged;
/// <summary>
/// Gets or sets the Program associated with this collection of feedbacks
/// </summary>
public Program Program;
/// <summary>
/// Gets or sets the ProgramInfo object that contains detailed information about the program, such as its file name, compile time, environment, and other relevant properties. This object is updated whenever there is a change in the program's operating state or registration state, ensuring that the feedbacks always reflect the most current information about the program.
/// </summary>
public ProgramInfo ProgramInfo { get; set; }
/// <summary>
/// Gets or sets the ProgramStartedFeedback, which is a BoolFeedback that indicates whether the program is currently started (true) or not (false). This feedback is updated in real-time based on the program's operating state, allowing external classes to easily monitor whether the program is running or not.
/// </summary>
public BoolFeedback ProgramStartedFeedback;
/// <summary>
/// Gets or sets the ProgramStoppedFeedback, which is a BoolFeedback that indicates whether the program is currently stopped (true) or not (false). This feedback is updated in real-time based on the program's operating state, allowing external classes to easily monitor whether the program is stopped or not.
/// </summary>
public BoolFeedback ProgramStoppedFeedback;
/// <summary>
/// Gets or sets the ProgramRegisteredFeedback, which is a BoolFeedback that indicates whether the program is currently registered (true) or not (false). This feedback is updated in real-time based on the program's registration state, allowing external classes to easily monitor whether the program is registered or not.
/// </summary>
public BoolFeedback ProgramRegisteredFeedback;
/// <summary>
/// Gets or sets the ProgramUnregisteredFeedback, which is a BoolFeedback that indicates whether the program is currently unregistered (true) or not (false). This feedback is updated in real-time based on the program's registration state, allowing external classes to easily monitor whether the program is unregistered or not.
/// </summary>
public BoolFeedback ProgramUnregisteredFeedback;
/// <summary>
/// Gets or sets the ProgramNameFeedback, which is a StringFeedback that provides the name of the program file. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current name of the program file.
/// </summary>
public StringFeedback ProgramNameFeedback;
/// <summary>
/// Gets or sets the ProgramCompileTimeFeedback, which is a StringFeedback that provides the compile time of the program. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current compile time of the program.
/// </summary>
public StringFeedback ProgramCompileTimeFeedback;
/// <summary>
/// Gets or sets the CrestronDataBaseVersionFeedback, which is a StringFeedback that provides the version of the Crestron database used by the program. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current version of the Crestron database used by the program.
/// </summary>
public StringFeedback CrestronDataBaseVersionFeedback;
// SIMPL windows version
/// <summary>
/// Gets or sets the EnvironmentVersionFeedback, which is a StringFeedback that provides the environment version of the program. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access the current environment version of the program.
/// </summary>
public StringFeedback EnvironmentVersionFeedback;
/// <summary>
/// Gets or sets the AggregatedProgramInfoFeedback, which is a StringFeedback that provides a JSON serialized string of the entire ProgramInfo object. This feedback is updated in real-time based on changes to the program's information, allowing external classes to easily access all current information about the program in a single feedback.
/// </summary>
public StringFeedback AggregatedProgramInfoFeedback;
/// <summary>
/// Constructor for the ProgramStatusFeedbacks class, which initializes all feedbacks based on the provided Program object and retrieves the initial program information to populate the feedbacks with accurate data. This constructor also sets up the necessary event handlers to ensure that the feedbacks are updated in real-time as changes occur to the program's operating state or registration state.
/// </summary>
/// <param name="program"></param>
public ProgramStatusFeedbacks(Program program)
{
ProgramInfo = new ProgramInfo(program.Number);
@@ -847,6 +921,9 @@ public class ProgramStatusFeedbacks
OnProgramInfoChanged();
}
/// <summary>
/// Fires the ProgramInfoChanged event to notify external classes that they should update any properties related to program information based on changes to the program's operating state or registration state. This method is called whenever there is a change in the program's information, ensuring that all feedbacks and external classes that rely on program information are always up-to-date with the most current information about the program.
/// </summary>
public void OnProgramInfoChanged()
{
//Debug.LogMessage(LogEventLevel.Debug, "Firing ProgramInfoChanged for slot: {0}", Program.Number);

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using System.Timers;
using PepperDash.Core;
@@ -15,15 +15,30 @@ namespace PepperDash.Essentials.Core;
/// </summary>
public class ActionIncrementer
{
/// <summary>
/// The amount to change the value by each increment
/// </summary>
public int ChangeAmount { get; set; }
/// <summary>
/// The maximum value the incrementer can reach
/// </summary>
public int MaxValue { get; set; }
/// <summary>
/// The minimum value the incrementer can reach
/// </summary>
public int MinValue { get; set; }
/// <summary>
/// The delay before the incrementer starts repeating
/// </summary>
public uint RepeatDelay { get; set; }
/// <summary>
/// The time interval between each repeat
/// </summary>
public uint RepeatTime { get; set; }
Action<int> SetAction;
Func<int> GetFunc;
CTimer Timer;
Timer Timer;
/// <summary>
///
@@ -89,7 +104,20 @@ public class ActionIncrementer
if (atLimit) // Don't go past end
Stop();
else if (Timer == null) // Only enter the timer if it's not already running
Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime);
{
Timer = new Timer(RepeatDelay) { AutoReset = false };
Timer.Elapsed += (s, e) =>
{
Go(change);
if (Timer != null)
{
Timer.Interval = RepeatTime;
Timer.AutoReset = true;
Timer.Start();
}
};
Timer.Start();
}
}
/// <summary>

View File

@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro;
using PepperDash.Core;
@@ -16,14 +16,39 @@ namespace PepperDash.Essentials.Core;
public class UshortSigIncrementer
{
UShortInputSig TheSig;
/// <summary>
/// Amount to change the signal on each step
/// </summary>
public ushort ChangeAmount { get; set; }
/// <summary>
/// Maximum value to ramp to
/// </summary>
public int MaxValue { get; set; }
/// <summary>
/// Minimum value to ramp to
/// </summary>
public int MinValue { get; set; }
/// <summary>
/// The delay before the incrementer starts repeating
/// </summary>
public uint RepeatDelay { get; set; }
/// <summary>
/// The time interval between each repeat
/// </summary>
public uint RepeatTime { get; set; }
bool SignedMode;
CTimer Timer;
Timer Timer;
/// <summary>
/// Constructor
/// </summary>
/// <param name="sig"></param>
/// <param name="changeAmount"></param>
/// <param name="minValue"></param>
/// <param name="maxValue"></param>
/// <param name="repeatDelay"></param>
/// <param name="repeatTime"></param>
public UshortSigIncrementer(UShortInputSig sig, ushort changeAmount, int minValue, int maxValue, uint repeatDelay, uint repeatTime)
{
TheSig = sig;
@@ -37,12 +62,19 @@ public class UshortSigIncrementer
Debug.LogMessage(LogEventLevel.Debug, "UshortSigIncrementer has signed values that exceed range of -32768, 32767");
}
/// <summary>
/// Starts incrementing cycle
/// </summary>
public void StartUp()
{
if (Timer != null) return;
Go(ChangeAmount);
}
/// <summary>
/// Starts decrementing cycle
/// </summary>
public void StartDown()
{
if (Timer != null) return;
@@ -64,7 +96,20 @@ public class UshortSigIncrementer
if (atLimit) // Don't go past end
Stop();
else if (Timer == null) // Only enter the timer if it's not already running
Timer = new CTimer(o => { Go(change); }, null, RepeatDelay, RepeatTime);
{
Timer = new Timer(RepeatDelay) { AutoReset = false };
Timer.Elapsed += (s, e) =>
{
Go(change);
if (Timer != null)
{
Timer.Interval = RepeatTime;
Timer.AutoReset = true;
Timer.Start();
}
};
Timer.Start();
}
}
bool CheckLevel(int levelIn, out int levelOut)
@@ -85,6 +130,9 @@ public class UshortSigIncrementer
return IsAtLimit;
}
/// <summary>
/// Stops incrementing/decrementing cycle
/// </summary>
public void Stop()
{
if (Timer != null)
@@ -92,6 +140,11 @@ public class UshortSigIncrementer
Timer = null;
}
/// <summary>
/// Sets the value of the signal
/// </summary>
/// <param name="value"></param>
void SetValue(ushort value)
{
//CrestronConsole.PrintLine("Increment level:{0} / {1}", value, (short)value);

View File

@@ -75,7 +75,7 @@ public class RoomOnToDefaultSourceWhenOccupied : ReconfigurableDevice
});
}
public override bool CustomActivate()
protected override bool CustomActivate()
{
SetUpDevice();

View File

@@ -1,12 +1,13 @@
using Crestron.SimplSharp;
using PepperDash.Core;
using PepperDash.Core;
using PepperDash.Core.Logging;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Timer = System.Timers.Timer;
namespace PepperDash.Essentials.Core;
@@ -73,7 +74,7 @@ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner
}
}
private CTimer _scenarioChangeDebounceTimer;
private Timer _scenarioChangeDebounceTimer;
private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s
@@ -204,18 +205,22 @@ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner
if (_scenarioChangeDebounceTimer == null)
{
_scenarioChangeDebounceTimer = new CTimer((o) => DetermineRoomCombinationScenario(), time);
_scenarioChangeDebounceTimer = new Timer(time) { AutoReset = false };
_scenarioChangeDebounceTimer.Elapsed += async (s, e) => await DetermineRoomCombinationScenario();
_scenarioChangeDebounceTimer.Start();
}
else
{
_scenarioChangeDebounceTimer.Reset(time);
_scenarioChangeDebounceTimer.Stop();
_scenarioChangeDebounceTimer.Interval = time;
_scenarioChangeDebounceTimer.Start();
}
}
/// <summary>
/// Determines the current room combination scenario based on the state of the partition sensors
/// </summary>
private void DetermineRoomCombinationScenario()
private async Task DetermineRoomCombinationScenario()
{
if (_scenarioChangeDebounceTimer != null)
{
@@ -250,7 +255,7 @@ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner
if (currentScenario != null)
{
this.LogInformation("Found combination Scenario {scenarioKey}", currentScenario.Key);
ChangeScenario(currentScenario);
await ChangeScenario(currentScenario);
}
}

View File

@@ -24,18 +24,12 @@ public class EssentialsNDisplayRoomPropertiesConfig : EssentialsConferenceRoomPr
[JsonProperty("defaultVideoBehavior")]
public string DefaultVideoBehavior { get; set; }
/// <summary>
/// Gets or sets the Displays
/// </summary>
[JsonProperty("displays")]
public Dictionary<eSourceListItemDestinationTypes, DisplayItem> Displays { get; set; }
/// <summary>
/// Constructor
/// </summary>
public EssentialsNDisplayRoomPropertiesConfig()
{
Displays = new Dictionary<eSourceListItemDestinationTypes, DisplayItem>();
}
}

View File

@@ -1,35 +0,0 @@
using System;
using PepperDash.Essentials.Core;
namespace PepperDash.Essentials.Room.Config;
/// <summary>
/// Configuration class for volume levels in the Essentials room. This is used to configure the volume levels for the master, program, audio call receive, and audio call transmit channels in the room.
/// </summary>
[Obsolete("This class is being deprecated in favor audio control point lists in the main config. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.")]
public class EssentialsRoomVolumesConfig
{
public EssentialsVolumeLevelConfig Master { get; set; }
public EssentialsVolumeLevelConfig Program { get; set; }
public EssentialsVolumeLevelConfig AudioCallRx { get; set; }
public EssentialsVolumeLevelConfig AudioCallTx { get; set; }
}
/// <summary>
/// Configuration class for a volume level in the Essentials room.
/// </summary>
public class EssentialsVolumeLevelConfig
{
public string DeviceKey { get; set; }
public string Label { get; set; }
public int Level { get; set; }
/// <summary>
/// Helper to get the device associated with key - one timer.
/// </summary>
[Obsolete("This method references DM CHASSIS Directly and should not be used in the Core library. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.")]
public IBasicVolumeWithFeedback GetDevice()
{
throw new NotImplementedException("This method references DM CHASSIS Directly and should not be used in the Core library. It is recommended to use the DeviceKey property to get the device from the main system and then cast it to the correct type.");
}
}

View File

@@ -250,7 +250,7 @@ public abstract class EssentialsRoomBase : ReconfigurableDevice, IEssentialsRoom
});
}
public override bool CustomActivate()
protected override bool CustomActivate()
{
SetUpMobileControl();

View File

@@ -28,18 +28,6 @@ public interface IHasDefaultDisplay
IRoutingSink DefaultDisplay { get; }
}
/// <summary>
/// For rooms with multiple displays
/// </summary>
[Obsolete("This interface is being deprecated in favor of using destination lists for routing to multiple displays.")]
public interface IHasMultipleDisplays
{
/// <summary>
/// A dictionary of displays in the room with the key being the type of display (presentation, calling, etc.) and the value being the display device
/// </summary>
Dictionary<eSourceListItemDestinationTypes, IRoutingSink> Displays { get; }
}
/// <summary>
/// For rooms with routing
/// </summary>

View File

@@ -6,6 +6,7 @@ namespace PepperDash.Essentials.Core.Routing
/// <summary>
/// The current sources for the room, keyed by eRoutingSignalType.
/// This allows for multiple sources to be tracked, such as audio and video.
/// Intended to be implemented on a DestinationListItem to provide access to the current sources for the room in its context.
/// </summary>
/// <remarks>
/// This interface is used to provide access to the current sources in a room,
@@ -17,7 +18,7 @@ namespace PepperDash.Essentials.Core.Routing
/// Gets the current sources for the room, keyed by eRoutingSignalType.
/// This dictionary contains the current source for each signal type, such as audio, video, and control signals.
/// </summary>
Dictionary<eRoutingSignalType, SourceListItem> CurrentSources { get; }
Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; }
/// <summary>
/// Gets the current source keys for the room, keyed by eRoutingSignalType.
@@ -28,16 +29,51 @@ namespace PepperDash.Essentials.Core.Routing
/// <summary>
/// Event raised when the current sources change.
/// </summary>
event EventHandler CurrentSourcesChanged;
event EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged;
/// <summary>
/// Sets the current source for a specific signal type.
/// This method updates the current source for the specified signal type and notifies any subscribers of the change.
/// </summary>
/// <param name="signalType">The signal type to update.</param>
/// <param name="sourceListKey">The key for the source list.</param>
/// <param name="sourceListItem">The source list item to set as the current source.</param>
void SetCurrentSource(eRoutingSignalType signalType, string sourceListKey, SourceListItem sourceListItem);
/// <param name="sourceDevice">The source device to set as the current source.</param>
void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice);
}
}
/// <summary>
/// Event arguments for the CurrentSourcesChanged event, providing details about the signal type and the previous and new sources.
/// </summary>
public class CurrentSourcesChangedEventArgs : EventArgs
{
/// <summary>
/// Gets the signal type for which the current source has changed.
/// </summary>
public eRoutingSignalType SignalType { get; }
/// <summary>
/// Gets the previous source for the signal type before the change occurred.
/// </summary>
public IRoutingSource PreviousSource { get; }
/// <summary>
/// Gets the new source for the signal type after the change occurred.
/// </summary>
public IRoutingSource NewSource { get; }
/// <summary>
/// Initializes a new instance of the CurrentSourcesChangedEventArgs class with the specified signal type, previous source, and new source.
/// </summary>
/// <param name="signalType">The signal type for which the current source has changed.</param>
/// <param name="previousSource">The previous source for the signal type before the change occurred.</param>
/// <param name="newSource">The new source for the signal type after the change occurred.</param>
public CurrentSourcesChangedEventArgs(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource newSource)
{
SignalType = signalType;
PreviousSource = previousSource;
NewSource = newSource;
}
}
}

View File

@@ -1,4 +1,5 @@
using PepperDash.Core;
using Newtonsoft.Json;
using PepperDash.Core;
namespace PepperDash.Essentials.Core;
@@ -11,6 +12,7 @@ public interface IRoutingOutputs : IKeyed
/// <summary>
/// Collection of Output Ports
/// </summary>
[JsonProperty("outputPorts")]
RoutingPortCollection<RoutingOutputPort> OutputPorts { get; }
}

View File

@@ -1,3 +1,4 @@
using PepperDash.Core;
using PepperDash.Essentials.Core.Routing;
namespace PepperDash.Essentials.Core;
@@ -5,7 +6,7 @@ namespace PepperDash.Essentials.Core;
/// <summary>
/// Defines the contract for IRoutingSink
/// </summary>
public interface IRoutingSink : IRoutingInputs, IHasCurrentSourceInfoChange
public interface IRoutingSink : IRoutingInputs, IKeyName, ICurrentSources
{
}
@@ -20,10 +21,4 @@ public interface IRoutingSinkWithInputPort : IRoutingSink
RoutingInputPort CurrentInputPort { get; }
}
/// <summary>
/// Interface for routing sinks that have access to the current source information.
/// </summary>
public interface IRoutingSinkWithCurrentSources : IRoutingSink, ICurrentSources
{
}

View File

@@ -5,8 +5,9 @@
/// <summary>
/// Defines the contract for IRoutingSinkWithFeedback
/// </summary>
public interface IRoutingSinkWithFeedback : IRoutingSinkWithSwitching
public interface IRoutingSinkWithFeedback : IRoutingSink
{
}

View File

@@ -1,8 +1,10 @@
namespace PepperDash.Essentials.Core;
using PepperDash.Core;
namespace PepperDash.Essentials.Core;
/// <summary>
/// Marker interface to identify a device that acts as the origin of a signal path (<see cref="IRoutingOutputs"/>).
/// </summary>
public interface IRoutingSource : IRoutingOutputs
public interface IRoutingSource : IRoutingOutputs, IKeyName
{
}

View File

@@ -9,15 +9,15 @@ namespace PepperDash.Essentials.Core.Routing;
/// 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
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)
{
public RoutingFeedbackManager(string key, string name) : base(key, name)
{
AddPreActivationAction(SubscribeForMidpointFeedback);
AddPreActivationAction(SubscribeForSinkFeedback);
}
@@ -41,12 +41,12 @@ public class RoutingFeedbackManager: EssentialsDevice
/// </summary>
private void SubscribeForSinkFeedback()
{
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
foreach (var device in sinkDevices)
{
device.InputChanged += HandleSinkUpdate;
}
foreach (var device in sinkDevices)
{
device.InputChanged += HandleSinkUpdate;
}
}
/// <summary>
@@ -59,7 +59,7 @@ public class RoutingFeedbackManager: EssentialsDevice
{
try
{
var devices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
var devices = DeviceManager.AllDevices.OfType<IRoutingSinkWithInputPort>();
foreach (var device in devices)
{
@@ -96,13 +96,13 @@ public class RoutingFeedbackManager: EssentialsDevice
/// </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)
{
private void UpdateDestination(IRoutingSink destination, RoutingInputPort inputPort)
{
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key);
if(inputPort == null)
if (inputPort == null)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this,destination.Key);
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this, destination.Key);
return;
}
@@ -116,19 +116,10 @@ public class RoutingFeedbackManager: EssentialsDevice
if (firstTieLine == null)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort);
var tempSourceListItem = new SourceListItem
{
SourceKey = "$transient",
Name = inputPort.Key,
};
destination.CurrentSourceInfo = tempSourceListItem; ;
destination.CurrentSourceInfoKey = "$transient";
return;
}
} catch (Exception ex)
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex);
return;
@@ -136,6 +127,7 @@ public class RoutingFeedbackManager: EssentialsDevice
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine);
TieLine sourceTieLine;
try
{
@@ -145,91 +137,34 @@ public class RoutingFeedbackManager: EssentialsDevice
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route found to source for inputPort {inputPort}. Clearing current source", this, inputPort);
var tempSourceListItem = new SourceListItem
{
SourceKey = "$transient",
Name = "None",
};
destination.CurrentSourceInfo = tempSourceListItem;
destination.CurrentSourceInfoKey = string.Empty;
destination.SetCurrentSource(sourceTieLine.Type, null);
return;
}
} catch(Exception ex)
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex);
return;
}
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine);
// Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet.
var room = DeviceManager.AllDevices.OfType<IEssentialsRoom>().FirstOrDefault((r) => {
if(r is IHasMultipleDisplays roomMultipleDisplays)
{
return roomMultipleDisplays.Displays.Any(d => d.Value.Key == destination.Key);
}
if(r is IHasDefaultDisplay roomDefaultDisplay)
{
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
}
return false;
});
if(room == null)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No room found for display {destination}", this, destination.Key);
return;
}
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key);
var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey);
if (sourceList == null)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", this, room.SourceListKey, sourceTieLine);
return;
}
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key);
var sourceListItem = sourceList.FirstOrDefault(sli => {
//// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose,
// "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}",
// this,
// sli.Key,
// sli.Value.SourceKey,
// sourceTieLine.SourcePort.ParentDevice.Key);
return sli.Value.SourceKey.Equals(sourceTieLine.SourcePort.ParentDevice.Key,StringComparison.InvariantCultureIgnoreCase);
});
var source = sourceListItem.Value;
var sourceKey = sourceListItem.Key;
if (source == null)
if (sourceTieLine.SourcePort.ParentDevice == null)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No source found for device {key}. Creating transient source for {destination}", this, sourceTieLine.SourcePort.ParentDevice.Key, destination);
var tempSourceListItem = new SourceListItem
{
SourceKey = "$transient",
Name = sourceTieLine.SourcePort.Key,
};
destination.SetCurrentSource(sourceTieLine.Type, null);
destination.CurrentSourceInfoKey = "$transient";
destination.CurrentSourceInfo = tempSourceListItem;
return;
}
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Got Source {@source} with key {sourceKey}", this, source, sourceKey);
destination.CurrentSourceInfoKey = sourceKey;
destination.CurrentSourceInfo = source;
if (sourceTieLine.SourcePort.ParentDevice is IRoutingSource sourceDevice)
{
destination.SetCurrentSource(sourceTieLine.Type, sourceDevice);
}
}
/// <summary>
@@ -249,13 +184,14 @@ public class RoutingFeedbackManager: EssentialsDevice
{
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
if(midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
{
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes",this, midpoint.Key);
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes", this, midpoint.Key);
return null;
}
var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => {
var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route =>
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine);
return route.OutputPort != null && route.InputPort != null && route.OutputPort?.Key == tieLine.SourcePort.Key && route.OutputPort?.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key;
@@ -269,9 +205,11 @@ public class RoutingFeedbackManager: EssentialsDevice
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint);
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => {
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key);
return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; });
return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key;
});
if (nextTieLine != null)
{
@@ -292,13 +230,14 @@ public class RoutingFeedbackManager: EssentialsDevice
return tieLine;
}
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key );
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key);
if (nextTieLine != null)
{
return GetRootTieLine(nextTieLine);
}
} catch (Exception ex)
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
return null;

View File

@@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using PepperDash.Core;
using PepperDash.Essentials.Core.CrestronIO;
namespace PepperDash.Essentials.Core.Shades;
/// <summary>
/// Base class for shades
/// </summary>
[Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
public abstract class ShadeBase : EssentialsDevice, IShadesOpenCloseStop
{
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">key of the shade device</param>
/// <param name="name">name of the shade device</param>
public ShadeBase(string key, string name)
: base(key, name)
{
}
#region iShadesOpenClose Members
/// <summary>
/// Opens the shade
/// </summary>
public abstract void Open();
/// <summary>
/// Stops the shade
/// </summary>
public abstract void Stop();
/// <summary>
/// Closes the shade
/// </summary>
public abstract void Close();
#endregion
}

View File

@@ -1,30 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using System.Timers;
using PepperDash.Core;
using Serilog.Events;
namespace PepperDash.Essentials.Core;
/// <summary>
/// A class that represents a countdown timer with feedbacks for time remaining, percent, and seconds
/// </summary>
public class SecondsCountdownTimer: IKeyed
{
/// <summary>
/// Event triggered when the timer starts.
/// </summary>
public event EventHandler<EventArgs> HasStarted;
/// <summary>
/// Event triggered when the timer finishes.
/// </summary>
public event EventHandler<EventArgs> HasFinished;
/// <summary>
/// Event triggered when the timer is cancelled.
/// </summary>
public event EventHandler<EventArgs> WasCancelled;
/// <inheritdoc />
public string Key { get; private set; }
/// <summary>
/// Indicates whether the timer is currently running
/// </summary>
public BoolFeedback IsRunningFeedback { get; private set; }
bool _isRunning;
/// <summary>
/// Feedback for the percentage of time remaining
/// </summary>
public IntFeedback PercentFeedback { get; private set; }
/// <summary>
/// Feedback for the time remaining in a string format
// </summary>
public StringFeedback TimeRemainingFeedback { get; private set; }
/// <summary>
/// Feedback for the time remaining in seconds
/// </summary>
public IntFeedback SecondsRemainingFeedback { get; private set; }
/// <summary>
/// When true, the timer will count down immediately upon calling Start. When false, the timer will count up, and when Finish is called, it will stop counting and fire the HasFinished event.
/// </summary>
public bool CountsDown { get; set; }
/// <summary>
@@ -32,10 +58,17 @@ public class SecondsCountdownTimer: IKeyed
/// </summary>
public int SecondsToCount { get; set; }
/// <summary>
/// The time at which the timer was started. Used to calculate percent and time remaining. Will be DateTime.MinValue if the timer is not currently running.
/// </summary>
public DateTime StartTime { get; private set; }
/// <summary>
/// The time at which the timer will finish counting down. Used to calculate percent and time remaining. Will be DateTime.MinValue if the timer is not currently running.
/// </summary>
public DateTime FinishTime { get; private set; }
private CTimer _secondTimer;
private Timer _secondTimer;
/// <summary>
/// Constructor
@@ -88,7 +121,10 @@ public class SecondsCountdownTimer: IKeyed
if (_secondTimer != null)
_secondTimer.Stop();
_secondTimer = new CTimer(SecondElapsedTimerCallback, null, 0, 1000);
_secondTimer = new Timer(1000) { AutoReset = true };
_secondTimer.Elapsed += (s, e) => SecondElapsedTimerCallback(null);
_secondTimer.Start();
SecondElapsedTimerCallback(null);
_isRunning = true;
IsRunningFeedback.FireUpdate();

View File

@@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using System.Timers;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
using Newtonsoft.Json;
@@ -17,9 +17,14 @@ public class RetriggerableTimer : EssentialsDevice
{
private RetriggerableTimerPropertiesConfig _propertiesConfig;
private CTimer _timer;
private Timer _timer;
private long _timerIntervalMs;
/// <summary>
/// Constructor for RetriggerableTimer
/// </summary>
/// <param name="key"></param>
/// <param name="config"></param>
public RetriggerableTimer(string key, DeviceConfig config)
: base(key, config.Name)
{
@@ -32,7 +37,8 @@ public class RetriggerableTimer : EssentialsDevice
}
}
public override bool CustomActivate()
/// <inheritdoc />
protected override bool CustomActivate()
{
if (_propertiesConfig.StartTimerOnActivation)
{
@@ -53,14 +59,26 @@ public class RetriggerableTimer : EssentialsDevice
_timer = null;
}
/// <summary>
/// Starts the timer with the interval specified in config. When the timer elapses, it executes the action specified in config for the Elapsed event. If the timer is already running, it will reset and start again.
/// When the timer is stopped, it executes the action specified in config for the Stopped event.
/// </summary>
public void StartTimer()
{
CleanUpTimer();
Debug.LogMessage(LogEventLevel.Information, this, "Starting Timer");
_timer = new CTimer(TimerElapsedCallback, GetActionFromConfig(eRetriggerableTimerEvents.Elapsed), _timerIntervalMs, _timerIntervalMs);
var action = GetActionFromConfig(eRetriggerableTimerEvents.Elapsed);
_timer = new Timer(_timerIntervalMs) { AutoReset = true };
_timer.Elapsed += (s, e) => TimerElapsedCallback(action);
_timer.Start();
}
/// <summary>
/// Stops the timer. If the timer is stopped before it elapses, it will execute the action specified in config for the Stopped event. If the timer is not running, this method does nothing.
/// If the timer is running, it will stop the timer and execute the Stopped action from config. If the timer is not running, it will do nothing.
/// If the timer is running and the Stopped action is not specified in config, it will stop the timer and do nothing else. If the timer is running and the Stopped action is specified in config, it will stop the timer and execute the action. If the timer is not running, it will do nothing regardless
/// </summary>
public void StopTimer()
{
Debug.LogMessage(LogEventLevel.Information, this, "Stopping Timer");
@@ -81,7 +99,7 @@ public class RetriggerableTimer : EssentialsDevice
/// <summary>
/// Executes the Elapsed action from confing when the timer elapses
/// </summary>
/// <param name="o"></param>
/// <param name="action">The action to execute when the timer elapses</param>
private void TimerElapsedCallback(object action)
{
Debug.LogMessage(LogEventLevel.Debug, this, "Timer Elapsed. Executing Action");
@@ -127,15 +145,29 @@ public class RetriggerableTimer : EssentialsDevice
/// </summary>
public class RetriggerableTimerPropertiesConfig
{
/// <summary>
/// When true, the timer will start immediately upon activation. When false, the timer will not start until StartTimer is called.
/// </summary>
[JsonProperty("startTimerOnActivation")]
public bool StartTimerOnActivation { get; set; }
/// <summary>
/// The interval at which the timer elapses, in milliseconds. This is required and must be greater than 0. If this value is not set or is less than or equal to 0, the timer will not start and an error will be logged.
/// </summary>
[JsonProperty("timerIntervalMs")]
public long TimerIntervalMs { get; set; }
/// <summary>
/// The actions to execute when timer events occur. The key is the type of event, and the value is the action to execute when that event occurs.
/// This is required and must contain at least an action for the Elapsed event.
/// If an action for the Stopped event is not included, then when the timer is stopped, it will simply stop without executing any action.
/// </summary>
[JsonProperty("events")]
public Dictionary<eRetriggerableTimerEvents, DeviceActionWrapper> Events { get; set; }
/// <summary>
/// Constructor
/// </summary>
public RetriggerableTimerPropertiesConfig()
{
Events = new Dictionary<eRetriggerableTimerEvents, DeviceActionWrapper>();
@@ -147,7 +179,14 @@ public class RetriggerableTimerPropertiesConfig
/// </summary>
public enum eRetriggerableTimerEvents
{
/// <summary>
/// Elapsed event state
/// </summary>
Elapsed,
/// <summary>
/// Stopped event state
/// </summary>
Stopped,
}
@@ -156,11 +195,16 @@ public enum eRetriggerableTimerEvents
/// </summary>
public class RetriggerableTimerFactory : EssentialsDeviceFactory<RetriggerableTimer>
{
/// <summary>
/// Constructor for factory
///
/// </summary>
public RetriggerableTimerFactory()
{
TypeNames = new List<string>() { "retriggerabletimer" };
}
/// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new RetriggerableTimer Device");

View File

@@ -1,10 +1,13 @@
using System;
using System.Text;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro.DeviceSupport;
namespace PepperDash.Essentials.Core.Touchpanels.Keyboards;
/// <summary>
/// Controller for the Habanero keyboard. This class handles all interaction with the keyboard, including showing/hiding, shift states, and key presses. It exposes a KeyPress event for single key presses, and an OutputFeedback string that contains the full text of what's been entered on the keyboard.
/// </summary>
public class HabaneroKeyboardController
{
/// <summary>
@@ -12,29 +15,57 @@ public class HabaneroKeyboardController
/// </summary>
public event EventHandler<KeyboardControllerPressEventArgs> KeyPress;
/// <summary>
/// The BasicTriList that the keyboard is connected to. This is used for all interaction with the keyboard, including showing/hiding, setting button text/visibility, and handling button presses.
/// </summary>
public BasicTriList TriList { get; private set; }
/// <summary>
/// Feedback for the current text entered on the keyboard. This is updated whenever a key is pressed, and can be used to get the full string of what's been entered on the keyboard.
/// </summary>
public StringFeedback OutputFeedback { get; private set; }
/// <summary>
/// Indicates whether the keyboard is currently visible. This is updated when Show and Hide are called, and can be used to determine the current visibility state of the keyboard.
/// </summary>
public bool IsVisible { get; private set; }
/// <summary>
/// The string displayed on the ".com" button.
/// </summary>
public string DotComButtonString { get; set; }
/// <summary>
/// The text displayed on the "Go" button.
/// </summary>
public string GoButtonText { get; set; }
/// <summary>
/// The text displayed on the secondary button.
/// </summary>
public string SecondaryButtonText { get; set; }
/// <summary>
/// Indicates whether the "Go" button is visible.
/// </summary>
public bool GoButtonVisible { get; set; }
/// <summary>
/// Indicates whether the secondary button is visible.
/// </summary>
public bool SecondaryButtonVisible { get; set; }
int ShiftMode = 0;
StringBuilder Output;
/// <summary>
/// An action that is run when the keyboard is hidden, either by calling the Hide method or by pressing the close button on the keyboard. This can be used to perform any necessary cleanup or state updates when the keyboard is dismissed.
/// </summary>
/// </summary>
public Action HideAction { get; set; }
CTimer BackspaceTimer;
Timer BackspaceTimer;
/// <summary>
///
@@ -88,8 +119,8 @@ public class HabaneroKeyboardController
TriList.SetSigTrueAction(2947, () => Press('.'));
TriList.SetSigTrueAction(2948, () => Press('@'));
TriList.SetSigTrueAction(2949, () => Press(' '));
TriList.SetSigHeldAction(2950, 500, StartBackspaceRepeat, StopBackspaceRepeat, Backspace);
//TriList.SetSigTrueAction(2950, Backspace);
TriList.SetSigHeldAction(2950, 500, StartBackspaceRepeat, StopBackspaceRepeat, Backspace);
//TriList.SetSigTrueAction(2950, Backspace);
TriList.SetSigTrueAction(2951, Shift);
TriList.SetSigTrueAction(2952, NumShift);
TriList.SetSigTrueAction(2953, Clear);
@@ -117,7 +148,7 @@ public class HabaneroKeyboardController
TriList.ClearBoolSigAction(i);
// run attached actions
if(HideAction != null)
if (HideAction != null)
HideAction();
TriList.SetBool(KeyboardVisible, false);
@@ -205,28 +236,31 @@ public class HabaneroKeyboardController
char Y(int i) { return new char[] { 'y', 'Y', '6', '^' }[i]; }
char Z(int i) { return new char[] { 'z', 'Z', ',', ',' }[i]; }
/// <summary>
/// Does what it says
/// </summary>
void StartBackspaceRepeat()
{
if (BackspaceTimer == null)
{
BackspaceTimer = new CTimer(o => Backspace(), null, 0, 175);
}
}
/// <summary>
/// Does what it says
/// </summary>
void StartBackspaceRepeat()
{
if (BackspaceTimer == null)
{
BackspaceTimer = new Timer(175) { AutoReset = true };
BackspaceTimer.Elapsed += (s, e) => Backspace();
BackspaceTimer.Start();
Backspace();
}
}
/// <summary>
/// Does what it says
/// </summary>
void StopBackspaceRepeat()
{
if (BackspaceTimer != null)
{
BackspaceTimer.Stop();
BackspaceTimer = null;
}
}
/// <summary>
/// Does what it says
/// </summary>
void StopBackspaceRepeat()
{
if (BackspaceTimer != null)
{
BackspaceTimer.Stop();
BackspaceTimer = null;
}
}
void Backspace()
{
@@ -384,7 +418,7 @@ public class HabaneroKeyboardController
/// <summary>
/// 2904
/// </summary>
public const uint SecondaryButtonTextJoin = 2904;
public const uint SecondaryButtonTextJoin = 2904;
/// <summary>
/// 2905
/// </summary>
@@ -413,21 +447,58 @@ public class HabaneroKeyboardController
/// </summary>
public class KeyboardControllerPressEventArgs : EventArgs
{
/// <summary>
/// The text of the key that was pressed. This will be null if a special key (backspace, clear, go, secondary) was pressed, in which case the SpecialKey property should be checked to determine which key was pressed.
/// </summary>
public string Text { get; private set; }
/// <summary>
/// If a special key (backspace, clear, go, secondary) was pressed, this property indicates which key it was. If a regular key was pressed, this will be KeyboardSpecialKey.None, and the Text property should be checked for the value of the key press.
/// </summary>
public KeyboardSpecialKey SpecialKey { get; private set; }
/// <summary>
/// Constructor for regular key presses
/// </summary>
public KeyboardControllerPressEventArgs(string text)
{
Text = text;
}
/// <summary>
/// Constructor for special key presses
/// </summary>
/// <param name="key"></param>
public KeyboardControllerPressEventArgs(KeyboardSpecialKey key)
{
SpecialKey = key;
}
}
/// <summary>
/// An enum representing special keys on the keyboard that don't have text values, such as backspace, clear, go, and the secondary button. The None value is used for regular key presses that do have text values, and should not be used for special key presses.
/// </summary>
public enum KeyboardSpecialKey
{
None = 0, Backspace, Clear, GoButton, SecondaryButton
/// <summary>
/// Indicates that a regular key with a text value was pressed, rather than a special key. When this value is set, the Text property of the KeyboardControllerPressEventArgs should be checked to get the value of the key press.
/// </summary>
None = 0,
/// <summary>
/// Indicates that the backspace key was pressed.
/// </summary>
Backspace,
/// <summary>
/// Indicates that the clear key was pressed.
/// </summary>
Clear,
/// <summary>
/// Indicates that the go button was pressed.
/// </summary>
GoButton,
/// <summary>
/// Indicates that the secondary button was pressed.
/// </summary>
SecondaryButton
}

View File

@@ -1,5 +1,5 @@
using System;
using Crestron.SimplSharp;
using System.Timers;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.DeviceSupport;
@@ -84,7 +84,7 @@ namespace PepperDash.Essentials.Core;
/// <returns></returns>
public static BoolOutputSig SetSigHeldAction(this BoolOutputSig sig, uint heldMs, Action heldAction, Action holdReleasedAction, Action releaseAction)
{
CTimer heldTimer = null;
Timer heldTimer = null;
bool wasHeld = false;
return sig.SetBoolSigAction(press =>
{
@@ -92,7 +92,8 @@ namespace PepperDash.Essentials.Core;
{
wasHeld = false;
// Could insert a pressed action here
heldTimer = new CTimer(o =>
heldTimer = new Timer(heldMs) { AutoReset = false };
heldTimer.Elapsed += (s, e) =>
{
// if still held and there's an action
if (sig.BoolValue && heldAction != null)
@@ -101,7 +102,8 @@ namespace PepperDash.Essentials.Core;
// Hold action here
heldAction();
}
}, heldMs);
};
heldTimer.Start();
}
else if (!press && !wasHeld) // released, no hold
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using PepperDash.Core;
@@ -17,6 +18,7 @@ namespace PepperDash.Essentials.Core.Web;
public class EssentialsWebApi : EssentialsDevice
{
private readonly WebApiServer _server;
private readonly WebApiServer _debugServer;
///<example>
/// http(s)://{ipaddress}/cws/{basePath}
@@ -26,10 +28,6 @@ public class EssentialsWebApi : EssentialsDevice
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
: "/api";
private const int DebugTrace = 0;
private const int DebugInfo = 1;
private const int DebugVerbose = 2;
/// <summary>
/// Gets or sets the BasePath
/// </summary>
@@ -71,6 +69,9 @@ public class EssentialsWebApi : EssentialsDevice
_server = new WebApiServer(Key, Name, BasePath);
_debugServer = new WebApiServer($"{key}-debug-app", $"{name} Debug App", "/debug");
_debugServer.SetFallbackHandler(new ServeDebugAppRequestHandler());
SetupRoutes();
}
@@ -207,7 +208,7 @@ public class EssentialsWebApi : EssentialsDevice
/// <summary>
/// Initializes the CWS class
/// </summary>
public override void Initialize()
protected override void Initialize()
{
AddRoute(new HttpCwsRoute("apiPaths")
{
@@ -229,8 +230,9 @@ public class EssentialsWebApi : EssentialsDevice
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
_server.Start();
_debugServer.Start();
GetPaths();
PrintPaths();
return;
}
@@ -239,12 +241,13 @@ public class EssentialsWebApi : EssentialsDevice
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
_server.Start();
_debugServer.Start();
GetPaths();
PrintPaths();
}
/// <summary>
/// Print the available pahts
/// Print the available paths
/// </summary>
/// <example>
/// http(s)://{ipaddress}/cws/{basePath}
@@ -253,7 +256,7 @@ public class EssentialsWebApi : EssentialsDevice
/// <summary>
/// GetPaths method
/// </summary>
public void GetPaths()
public void PrintPaths()
{
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
@@ -267,7 +270,7 @@ public class EssentialsWebApi : EssentialsDevice
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
: $"https://{currentIp}/cws{BasePath}";
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
Debug.LogMessage(LogEventLevel.Information, this, "Server: {path:l}", path);
var routeCollection = _server.GetRouteCollection();
if (routeCollection == null)
@@ -280,6 +283,12 @@ public class EssentialsWebApi : EssentialsDevice
{
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
}
var debugPath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws/debug"
: $"https://{currentIp}/cws/debug";
Debug.LogMessage(LogEventLevel.Information, this, "Debug App: {debugPath:l}", debugPath);
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
}
}

View File

@@ -6,18 +6,23 @@ using PepperDash.Core.Web.RequestHandlers;
using Serilog.Events;
using System;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.Web.RequestHandlers;
/// <summary>
/// Represents a DebugSessionRequestHandler
/// </summary>
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
{
private readonly DebugWebsocketSink _sink = new DebugWebsocketSink();
{
/// <summary>
/// Constructor
/// </summary>
public DebugSessionRequestHandler()
: base(true)
{
}
/// <summary>
/// Gets details for a debug session
/// </summary>
@@ -41,21 +46,30 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler
var port = 0;
if (!_sink.IsRunning)
if (!Debug.WebsocketSink.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
_sink.StartServerAndSetPort(port);
Debug.WebsocketSink.StartServerAndSetPort(port);
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
}
var url = _sink.Url;
if (!Debug.WebsocketSink.IsRunning)
{
context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error";
context.Response.Write(JsonConvert.SerializeObject(new { error = "Failed to start WebSocket debug server. Check logs for details." }), false);
context.Response.End();
return;
}
var url = Debug.WebsocketSink.Url;
object data = new
{
url = _sink.Url
url = Debug.WebsocketSink.Url
};
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
@@ -82,7 +96,8 @@ public class DebugSessionRequestHandler : WebApiBaseRequestHandler
/// <param name="context"></param>
protected override void HandlePost(HttpCwsContext context)
{
_sink.StopServer();
Task.Run(() => Debug.WebsocketSink.StopServer());
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";

View File

@@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.WebScripting;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using Serilog.Events;
namespace PepperDash.Essentials.Core.Web.RequestHandlers;
/// <summary>
/// Serves the React debug app from the processor's /HTML/debug folder.
/// The root route (debug) and all sub-paths (debug/{*filePath}) are handled here.
/// Text assets are sent as UTF-8 strings; binary assets are written to the response
/// OutputStream. Any sub-path that does not match a real file falls back to
/// index.html so that client-side (React Router) routing continues to work.
/// </summary>
public class ServeDebugAppRequestHandler : WebApiBaseRequestHandler
{
private static readonly Dictionary<string, string> MimeTypes = new(StringComparer.OrdinalIgnoreCase)
{
{ ".html", "text/html; charset=utf-8" },
{ ".htm", "text/html; charset=utf-8" },
{ ".js", "application/javascript" },
{ ".mjs", "application/javascript" },
{ ".jsx", "application/javascript" },
{ ".css", "text/css" },
{ ".json", "application/json" },
{ ".map", "application/json" },
{ ".svg", "image/svg+xml" },
{ ".ico", "image/x-icon" },
{ ".png", "image/png" },
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".gif", "image/gif" },
{ ".woff", "font/woff" },
{ ".woff2","font/woff2" },
{ ".ttf", "font/ttf" },
{ ".eot", "application/vnd.ms-fontobject" },
};
private static readonly HashSet<string> TextExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".html", ".htm", ".js", ".mjs", ".jsx", ".css", ".json", ".map", ".svg", ".txt", ".xml"
};
/// <summary>
/// Constructor. CORS is enabled so browser dev-tools requests succeed.
/// </summary>
public ServeDebugAppRequestHandler() : base(true) { }
/// <summary>
/// Handles GET requests for the debug app and its static assets.
/// </summary>
protected override void HandleGet(HttpCwsContext context)
{
// When acting as the server-level fallback handler, only handle
// requests that are actually for the /debug path; defer everything
// else to the base class (which returns 501 Not Implemented).
var rawUrl = context.Request.RawUrl ?? string.Empty;
if (rawUrl.IndexOf("/debug", StringComparison.OrdinalIgnoreCase) < 0)
{
base.HandleGet(context);
return;
}
try
{
var htmlDebugPath = GetHtmlDebugPath();
if (htmlDebugPath == null)
{
SendResponse(context, 500, "Internal Server Error");
return;
}
var requestedPath = GetRequestedFilePath(context);
// Paths with no file extension are SPA client-side routes — serve index.html
string candidate;
if (string.IsNullOrEmpty(requestedPath) || !System.IO.Path.HasExtension(requestedPath))
{
candidate = System.IO.Path.Combine(htmlDebugPath, "index.html");
}
else
{
var relativePart = requestedPath.Replace('/', System.IO.Path.DirectorySeparatorChar);
candidate = System.IO.Path.Combine(htmlDebugPath, relativePart);
}
// Resolve to an absolute path and guard against path-traversal attacks
var resolvedCandidate = System.IO.Path.GetFullPath(candidate);
var resolvedBase = System.IO.Path.GetFullPath(htmlDebugPath)
+ System.IO.Path.DirectorySeparatorChar;
if (!resolvedCandidate.StartsWith(resolvedBase, StringComparison.OrdinalIgnoreCase))
{
SendResponse(context, 403, "Forbidden");
return;
}
// Missing static asset → fall back to index.html (SPA deep-link support)
if (!File.Exists(resolvedCandidate) && System.IO.Path.HasExtension(requestedPath ?? string.Empty))
{
resolvedCandidate = System.IO.Path.Combine(htmlDebugPath, "index.html");
Debug.LogMessage(LogEventLevel.Debug,
"ServeDebugAppRequestHandler: '{requestedPath:l}' not found, falling back to index.html",
requestedPath);
}
if (!File.Exists(resolvedCandidate))
{
SendResponse(context, 404, "Not Found");
return;
}
var ext = System.IO.Path.GetExtension(resolvedCandidate);
var contentType = MimeTypes.TryGetValue(ext, out var mime) ? mime : "application/octet-stream";
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";
context.Response.ContentType = contentType;
if (TextExtensions.Contains(ext))
{
string content;
using (var reader = new StreamReader(resolvedCandidate))
content = reader.ReadToEnd();
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(content, false);
}
else
{
var bytes = System.IO.File.ReadAllBytes(resolvedCandidate);
context.Response.OutputStream.Write(bytes, 0, bytes.Length);
}
context.Response.End();
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Error,
"ServeDebugAppRequestHandler: Unhandled error serving '{rawUrl:l}': {ex}",
context.Request.RawUrl, ex.Message);
try { SendResponse(context, 500, "Internal Server Error"); } catch { /* best-effort */ }
}
}
/// <summary>
/// Resolves the absolute path of the /HTML/debug folder on the processor.
/// </summary>
/// <remarks>
/// On a 4-series appliance, <c>Global.FilePathPrefix</c> is
/// <c>{root}/user/programX/</c>, so walking up two parents gives the
/// processor root that contains the <c>html</c> folder.
/// On Virtual Control, <c>Global.FilePathPrefix</c> is <c>{root}/User/</c>,
/// so only one parent hop is needed.
/// </remarks>
private static string GetHtmlDebugPath()
{
try
{
var separators = new[] { System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar };
var programDir = new DirectoryInfo(Global.FilePathPrefix.TrimEnd(separators));
DirectoryInfo rootDir;
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server)
{
// Virtual Control: {root}/User/ → one parent up = {root}
rootDir = programDir.Parent;
}
else
{
// 4-series appliance: {root}/user/programX/ → two parents up = {root}
rootDir = programDir.Parent?.Parent;
}
if (rootDir == null)
{
Debug.LogMessage(LogEventLevel.Error,
"ServeDebugAppRequestHandler: Cannot resolve HTML root from FilePathPrefix '{prefix:l}'",
Global.FilePathPrefix);
return null;
}
return System.IO.Path.Combine(rootDir.FullName, "html", "debug");
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Error,
"ServeDebugAppRequestHandler: Error resolving HTML debug path: {ex}", ex.Message);
return null;
}
}
/// <summary>
/// Extracts the file sub-path from the request by parsing <c>RawUrl</c>.
/// Returns an empty string when the URL ends at <c>/debug</c> (root hit).
/// </summary>
private static string GetRequestedFilePath(HttpCwsContext context)
{
var rawUrl = context.Request.RawUrl ?? string.Empty;
// Locate the /debug segment in the URL
const string debugToken = "/debug";
var idx = rawUrl.IndexOf(debugToken, StringComparison.OrdinalIgnoreCase);
if (idx < 0)
return string.Empty;
var afterDebug = rawUrl.Substring(idx + debugToken.Length);
// Strip query string
var qIdx = afterDebug.IndexOf('?');
if (qIdx >= 0)
afterDebug = afterDebug.Substring(0, qIdx);
// Strip leading slash to get a relative file path
return afterDebug.TrimStart('/');
}
private static void SendResponse(HttpCwsContext context, int statusCode, string statusDescription)
{
context.Response.StatusCode = statusCode;
context.Response.StatusDescription = statusDescription;
context.Response.End();
}
}

View File

@@ -1,9 +1,10 @@
using System.Collections.Generic;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.Core.Routing;
using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common;
@@ -13,33 +14,19 @@ namespace PepperDash.Essentials.Devices.Common;
/// </summary>
public class GenericAudioOut : EssentialsDevice, IRoutingSink
{
/// <inheritdoc/>
public RoutingInputPort CurrentInputPort => AnyAudioIn;
public event SourceInfoChangeHandler CurrentSourceChange;
public string CurrentSourceInfoKey { get; set; }
public SourceListItem CurrentSourceInfo
{
get
{
return _CurrentSourceInfo;
}
set
{
if (value == _CurrentSourceInfo) return;
/// <inheritdoc/>
public Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; private set; }
var handler = CurrentSourceChange;
/// <inheritdoc/>
public Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; private set; }
if (handler != null)
handler(_CurrentSourceInfo, ChangeType.WillChange);
/// <inheritdoc />
public event System.EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged;
_CurrentSourceInfo = value;
if (handler != null)
handler(_CurrentSourceInfo, ChangeType.DidChange);
}
}
SourceListItem _CurrentSourceInfo;
/// <summary>
/// Gets or sets the AnyAudioIn
@@ -56,6 +43,66 @@ public class GenericAudioOut : EssentialsDevice, IRoutingSink
{
AnyAudioIn = new RoutingInputPort(RoutingPortNames.AnyAudioIn, eRoutingSignalType.Audio,
eRoutingPortConnectionType.LineAudio, null, this);
CurrentSources = new Dictionary<eRoutingSignalType, IRoutingSource>
{
{ eRoutingSignalType.Audio, null },
};
CurrentSourceKeys = new Dictionary<eRoutingSignalType, string>
{
{ eRoutingSignalType.Audio, string.Empty },
};
}
/// <inheritdoc />
public virtual void SetCurrentSource(eRoutingSignalType signalType, IRoutingSource sourceDevice)
{
foreach (eRoutingSignalType type in System.Enum.GetValues(typeof(eRoutingSignalType)))
{
var flagValue = System.Convert.ToInt32(type);
// Skip if flagValue is 0 or not a power of two (i.e., not a single-bit flag).
// (flagValue & (flagValue - 1)) != 0 checks if more than one bit is set.
if (flagValue == 0 || (flagValue & (flagValue - 1)) != 0)
{
this.LogDebug("Skipping {type}", type);
continue;
}
this.LogDebug("setting {type}", type);
var previousSource = CurrentSources[type];
if (signalType.HasFlag(type))
{
UpdateCurrentSources(type, previousSource, sourceDevice);
}
}
}
private void UpdateCurrentSources(eRoutingSignalType signalType, IRoutingSource previousSource, IRoutingSource sourceDevice)
{
if (CurrentSources.ContainsKey(signalType))
{
CurrentSources[signalType] = sourceDevice;
}
else
{
CurrentSources.Add(signalType, sourceDevice);
}
// Update the current source key for the specified signal type
if (CurrentSourceKeys.ContainsKey(signalType))
{
CurrentSourceKeys[signalType] = sourceDevice.Key;
}
else
{
CurrentSourceKeys.Add(signalType, sourceDevice.Key);
}
// Raise the CurrentSourcesChanged event
CurrentSourcesChanged?.Invoke(this, new CurrentSourcesChangedEventArgs(signalType, previousSource, sourceDevice));
}
#region IRoutingInputs Members
@@ -116,13 +163,20 @@ public class GenericAudioOutWithVolume : GenericAudioOut, IHasVolumeDevice
}
/// <summary>
/// Factory for creating GenericAudioOutWithVolume devices
/// </summary>
public class GenericAudioOutWithVolumeFactory : EssentialsDeviceFactory<GenericAudioOutWithVolume>
{
/// <summary>
/// Constructor for GenericAudioOutWithVolumeFactory
/// </summary>
public GenericAudioOutWithVolumeFactory()
{
TypeNames = new List<string>() { "genericaudiooutwithvolume" };
}
/// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc)
{
Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new GenericAudioOutWithVolumeFactory Device");

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More