mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-04-09 22:45:04 +00:00
Compare commits
29 Commits
v3.0.0-dev
...
net8-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4dce9e493 | ||
|
|
63f86e7499 | ||
|
|
a088166de9 | ||
|
|
c2ab2f34b7 | ||
|
|
083c270cf3 | ||
|
|
9c81546a07 | ||
|
|
d83266f634 | ||
|
|
bc60106afd | ||
|
|
4ee62088fa | ||
|
|
90b6f258f0 | ||
|
|
e1e32cea6f | ||
|
|
e31df338d6 | ||
|
|
d14058fc32 | ||
|
|
04d6508c80 | ||
|
|
1cbc8194ec | ||
|
|
6d2cd75cbe | ||
|
|
8b873b7248 | ||
|
|
58a2a5c008 | ||
|
|
cc7e2ab675 | ||
|
|
dc900f3f31 | ||
|
|
562f0ba793 | ||
|
|
9c3c924a29 | ||
|
|
9b5af60a46 | ||
|
|
a99b0a1fac | ||
|
|
7591913a9c | ||
|
|
66a6612b65 | ||
|
|
688cf34153 | ||
|
|
0c59237232 | ||
|
|
88eec9a3f1 |
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.2.4",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,9 @@ 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
|
||||
@@ -23,5 +19,4 @@ jobs:
|
||||
version: ${{ needs.getVersion.outputs.version }}
|
||||
tag: ${{ needs.getVersion.outputs.tag }}
|
||||
channel: ${{ needs.getVersion.outputs.channel }}
|
||||
bypassPackageCheck: true
|
||||
devToolsVersion: ${{ vars.ESSENTIALSDEVTOOLSVERSION }}
|
||||
bypassPackageCheck: true
|
||||
56
.github/workflows/ci.yml
vendored
Normal file
56
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: CI Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop, net8-updates ]
|
||||
pull_request:
|
||||
branches: [ main, develop, net8-updates ]
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --configuration Release --no-restore
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
|
||||
|
||||
- name: Generate coverage report
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@5.2.0
|
||||
with:
|
||||
reports: coverage/**/coverage.cobertura.xml
|
||||
targetdir: coverage-report
|
||||
reporttypes: Html;Cobertura;MarkdownSummary
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage-report/Cobertura.xml
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Write coverage summary
|
||||
run: cat coverage-report/Summary.md >> $GITHUB_STEP_SUMMARY
|
||||
if: always()
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
coverage/
|
||||
coverage-report/
|
||||
247
.github/workflows/essentials-3-dev-build.yml
vendored
Normal file
247
.github/workflows/essentials-3-dev-build.yml
vendored
Normal file
@@ -0,0 +1,247 @@
|
||||
name: Essentials v3 Development Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- feature-3.0.0/*
|
||||
- hotfix-3.0.0/*
|
||||
- release-3.0.0/*
|
||||
- development-3.0.0
|
||||
|
||||
env:
|
||||
SOLUTION_PATH: .
|
||||
SOLUTION_FILE: PepperDash.Essentials
|
||||
VERSION: 0.0.0-buildtype-buildnumber
|
||||
BUILD_TYPE: Debug
|
||||
RELEASE_BRANCH: main
|
||||
jobs:
|
||||
Build_Project_4-Series:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
# Detect environment (Act vs GitHub)
|
||||
- name: Detect environment
|
||||
id: detect_env
|
||||
run: |
|
||||
if [ -n "$ACT" ]; then
|
||||
echo "is_local=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_local=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
if [ "${{ steps.detect_env.outputs.is_local }}" == "true" ]; then
|
||||
# For Act - no sudo needed
|
||||
apt-get update
|
||||
apt-get install -y curl wget libicu-dev git unzip
|
||||
else
|
||||
# For GitHub runners - sudo required
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y curl wget libicu-dev git unzip
|
||||
fi
|
||||
|
||||
- name: Set Version Number
|
||||
id: setVersion
|
||||
shell: bash
|
||||
run: |
|
||||
latestVersion="3.0.0"
|
||||
newVersion=$latestVersion
|
||||
phase=""
|
||||
newVersionString=""
|
||||
|
||||
if [[ $GITHUB_REF =~ ^refs/pull/.* ]]; then
|
||||
phase="beta"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF =~ ^refs/heads/hotfix-3.0.0/.* ]]; then
|
||||
phase="hotfix"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF =~ ^refs/heads/feature-3.0.0/.* ]]; then
|
||||
phase="alpha"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF == "refs/heads/development-3.0.0" ]]; then
|
||||
phase="beta"
|
||||
newVersionString="${newVersion}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
elif [[ $GITHUB_REF =~ ^refs/heads/release-3.0.0/.* ]]; then
|
||||
version=$(echo $GITHUB_REF | awk -F '/' '{print $NF}' | sed 's/v//')
|
||||
phase="rc"
|
||||
newVersionString="${version}-${phase}-${GITHUB_RUN_NUMBER}"
|
||||
else
|
||||
# For local builds or unrecognized branches
|
||||
newVersionString="${newVersion}-local"
|
||||
fi
|
||||
|
||||
echo "version=$newVersionString" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create Build Properties file
|
||||
- name: Create Build Properties
|
||||
run: |
|
||||
cat > Directory.Build.props << EOF
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>${{ steps.setVersion.outputs.version }}</Version>
|
||||
<AssemblyVersion>${{ steps.setVersion.outputs.version }}</AssemblyVersion>
|
||||
<FileVersion>${{ steps.setVersion.outputs.version }}</FileVersion>
|
||||
<InformationalVersion>${{ steps.setVersion.outputs.version }}</InformationalVersion>
|
||||
<PackageVersion>${{ steps.setVersion.outputs.version }}</PackageVersion>
|
||||
<NuGetVersion>${{ steps.setVersion.outputs.version }}</NuGetVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
EOF
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore NuGet Packages
|
||||
run: dotnet restore ${SOLUTION_FILE}.sln
|
||||
|
||||
- name: Build Solution
|
||||
run: dotnet build ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --no-restore
|
||||
|
||||
# Copy the CPZ file to the output directory with version in the filename
|
||||
- name: Copy and Rename CPZ Files
|
||||
run: |
|
||||
mkdir -p ./output/cpz
|
||||
|
||||
# Find the main CPZ file in the build output
|
||||
if [ -f "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" ]; then
|
||||
cp "./src/PepperDash.Essentials/bin/${BUILD_TYPE}/net8/PepperDashEssentials.cpz" "./output/cpz/PepperDashEssentials.${{ steps.setVersion.outputs.version }}.cpz"
|
||||
echo "Main CPZ file copied and renamed successfully."
|
||||
else
|
||||
echo "Warning: Main CPZ file not found at expected location."
|
||||
find ./src -name "*.cpz" | xargs -I {} cp {} ./output/cpz/
|
||||
fi
|
||||
|
||||
- name: Pack Solution
|
||||
run: dotnet pack ${SOLUTION_FILE}.sln --configuration ${BUILD_TYPE} --output ./output/nuget --no-build
|
||||
|
||||
# List build artifacts (runs in both environments)
|
||||
- name: List Build Artifacts
|
||||
run: |
|
||||
echo "=== Build Artifacts ==="
|
||||
echo "NuGet Packages:"
|
||||
find ./output/nuget -type f | sort
|
||||
echo ""
|
||||
echo "CPZ/CPLZ Files:"
|
||||
find ./output -name "*.cpz" -o -name "*.cplz" | sort
|
||||
echo "======================="
|
||||
|
||||
# Enhanced package inspection for local runs
|
||||
- name: Inspect NuGet Packages
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== NuGet Package Details ==="
|
||||
for pkg in $(find ./output/nuget -name "*.nupkg"); do
|
||||
echo "Package: $(basename "$pkg")"
|
||||
echo "Size: $(du -h "$pkg" | cut -f1)"
|
||||
|
||||
# Extract and show package contents
|
||||
echo "Contents:"
|
||||
unzip -l "$pkg" | tail -n +4 | head -n -2
|
||||
echo "--------------------------"
|
||||
|
||||
# Try to extract and show the nuspec file (contains metadata)
|
||||
echo "Metadata:"
|
||||
unzip -p "$pkg" "*.nuspec" 2>/dev/null | grep -E "(<id>|<version>|<description>|<authors>|<dependencies>)" || echo "Metadata extraction failed"
|
||||
echo "--------------------------"
|
||||
done
|
||||
echo "==========================="
|
||||
|
||||
# Tag creation - GitHub version
|
||||
- name: Create tag for non-rc builds (GitHub)
|
||||
if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'false' }}
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
git tag ${{ steps.setVersion.outputs.version }}
|
||||
git push --tags origin
|
||||
|
||||
# Tag creation - Act mock version
|
||||
- name: Create tag for non-rc builds (Act Mock)
|
||||
if: ${{ !contains(steps.setVersion.outputs.version, 'rc') && steps.detect_env.outputs.is_local == 'true' }}
|
||||
run: |
|
||||
echo "Would create git tag: ${{ steps.setVersion.outputs.version }}"
|
||||
echo "Would push tag to: origin"
|
||||
|
||||
# Release creation - GitHub version
|
||||
- name: Create Release (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
artifacts: 'output/cpz/*,output/**/*.cplz'
|
||||
generateReleaseNotes: true
|
||||
prerelease: ${{contains('debug', env.BUILD_TYPE)}}
|
||||
tag: ${{ steps.setVersion.outputs.version }}
|
||||
|
||||
# Release creation - Act mock version with enhanced output
|
||||
- name: Create Release (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock Release Creation ==="
|
||||
echo "Would create release with:"
|
||||
echo "- Tag: ${{ steps.setVersion.outputs.version }}"
|
||||
echo "- Prerelease: ${{contains('debug', env.BUILD_TYPE)}}"
|
||||
echo "- Artifacts matching pattern: output/cpz/*,output/**/*.cplz"
|
||||
echo ""
|
||||
echo "Matching artifacts:"
|
||||
find ./output/cpz -type f
|
||||
find ./output -name "*.cplz"
|
||||
|
||||
# Detailed info about release artifacts
|
||||
echo ""
|
||||
echo "Artifact Details:"
|
||||
for artifact in $(find ./output/cpz -type f; find ./output -name "*.cplz"); do
|
||||
echo "File: $(basename "$artifact")"
|
||||
echo "Size: $(du -h "$artifact" | cut -f1)"
|
||||
echo "Created: $(stat -c %y "$artifact")"
|
||||
echo "MD5: $(md5sum "$artifact" | cut -d' ' -f1)"
|
||||
echo "--------------------------"
|
||||
done
|
||||
echo "============================"
|
||||
|
||||
# NuGet setup - GitHub version
|
||||
- name: Setup NuGet (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
run: |
|
||||
dotnet nuget add source https://nuget.pkg.github.com/pepperdash/index.json -n github -u pepperdash -p ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text
|
||||
|
||||
# NuGet setup - Act mock version
|
||||
- name: Setup NuGet (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock NuGet Setup ==="
|
||||
echo "Would add GitHub NuGet source: https://nuget.pkg.github.com/pepperdash/index.json"
|
||||
echo "======================="
|
||||
|
||||
# Publish to NuGet - GitHub version
|
||||
- name: Publish to Nuget (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
run: dotnet nuget push ./output/nuget/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_API_KEY }}
|
||||
|
||||
# Publish to NuGet - Act mock version
|
||||
- name: Publish to Nuget (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock Publish to NuGet ==="
|
||||
echo "Would publish the following packages to https://api.nuget.org/v3/index.json:"
|
||||
find ./output/nuget -name "*.nupkg" | sort
|
||||
echo "============================="
|
||||
|
||||
# Publish to GitHub NuGet - GitHub version
|
||||
- name: Publish to Github Nuget (GitHub)
|
||||
if: steps.detect_env.outputs.is_local == 'false'
|
||||
run: dotnet nuget push ./output/nuget/*.nupkg --source github --api-key ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Publish to GitHub NuGet - Act mock version
|
||||
- name: Publish to Github Nuget (Act Mock)
|
||||
if: steps.detect_env.outputs.is_local == 'true'
|
||||
run: |
|
||||
echo "=== Mock Publish to GitHub NuGet ==="
|
||||
echo "Would publish the following packages to the GitHub NuGet registry:"
|
||||
find ./output/nuget -name "*.nupkg" | sort
|
||||
echo "=================================="
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -26,9 +26,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Dotnet Setup
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.x
|
||||
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -393,7 +393,4 @@ essentials-framework/Essentials Interfaces/PepperDash_Essentials_Interfaces/Pepp
|
||||
/._PepperDash.Essentials.sln
|
||||
.vscode/settings.json
|
||||
_site/
|
||||
api/
|
||||
*.DS_Store
|
||||
/._PepperDash.Essentials.4Series.sln
|
||||
dotnet
|
||||
api/
|
||||
9
.vscode/extensions.json
vendored
9
.vscode/extensions.json
vendored
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"ms-dotnettools.csharp",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"vivaxy.vscode-conventional-commits",
|
||||
"mhutchie.git-graph"
|
||||
]
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
# Crestron Library Usage Analysis - PepperDash Essentials
|
||||
|
||||
This document provides a comprehensive analysis of Crestron classes and interfaces used throughout the PepperDash Essentials framework, organized by namespace and library component.
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The PepperDash Essentials framework extensively leverages Crestron SDK components across 100+ files, providing abstractions for:
|
||||
- Control system hardware (processors, touchpanels, IO devices)
|
||||
- Communication interfaces (Serial, TCP/IP, SSH, CEC, IR)
|
||||
- Device management and routing
|
||||
- User interface components and smart objects
|
||||
- System monitoring and diagnostics
|
||||
|
||||
## 1. Core Crestron Libraries
|
||||
|
||||
### 1.1 Crestron.SimplSharp
|
||||
|
||||
**Primary Usage**: Foundational framework components, collections, and basic types.
|
||||
|
||||
**Key Files**:
|
||||
- Multiple files across all projects use `Crestron.SimplSharp` namespaces
|
||||
- Provides basic C# runtime support for Crestron processors
|
||||
|
||||
### 1.2 Crestron.SimplSharpPro
|
||||
|
||||
**Primary Usage**: Main hardware abstraction layer for Crestron devices.
|
||||
|
||||
**Key Classes Used**:
|
||||
|
||||
#### CrestronControlSystem
|
||||
- **File**: `/src/PepperDash.Essentials/ControlSystem.cs`
|
||||
- **Usage**: Base class for the main control system implementation
|
||||
- **Implementation**: `public class ControlSystem : CrestronControlSystem, ILoadConfig`
|
||||
|
||||
#### Device (Base Class)
|
||||
- **Files**: 50+ files inherit from or use this class
|
||||
- **Key Implementations**:
|
||||
- `/src/PepperDash.Core/Device.cs` - Core device abstraction
|
||||
- `/src/PepperDash.Essentials.Core/Devices/EssentialsDevice.cs` - Extended device base
|
||||
- `/src/PepperDash.Essentials.Core/Room/Room.cs` - Room device implementation
|
||||
- `/src/PepperDash.Essentials.Core/Devices/CrestronProcessor.cs` - Processor device wrapper
|
||||
|
||||
#### BasicTriList
|
||||
- **Files**: 30+ files use this class extensively
|
||||
- **Primary Usage**: Touchpanel communication and SIMPL bridging
|
||||
- **Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/Touchpanels/TriListExtensions.cs` - Extension methods for signal handling
|
||||
- `/src/PepperDash.Essentials.Core/Devices/EssentialsBridgeableDevice.cs` - Bridge interface
|
||||
- `/src/PepperDash.Essentials.Core/Touchpanels/ModalDialog.cs` - UI dialog implementation
|
||||
|
||||
#### BasicTriListWithSmartObject
|
||||
- **Files**: Multiple touchpanel and UI files
|
||||
- **Usage**: Enhanced touchpanel support with smart object integration
|
||||
- **Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/Touchpanels/Interfaces.cs` - Interface definitions
|
||||
- `/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferenceList/SubpageReferenceList.cs`
|
||||
|
||||
## 2. Communication Hardware
|
||||
|
||||
### 2.1 Serial Communication (ComPort)
|
||||
|
||||
**Primary Class**: `ComPort`
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs`
|
||||
- `/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
public class ComPortController : Device, IBasicCommunicationWithStreamDebugging
|
||||
public static ComPort GetComPort(EssentialsControlPropertiesConfig config)
|
||||
```
|
||||
|
||||
**Interface Support**: `IComPorts` - Used for devices that provide multiple COM ports
|
||||
|
||||
### 2.2 IR Communication (IROutputPort)
|
||||
|
||||
**Primary Class**: `IROutputPort`
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/Devices/IrOutputPortController.cs`
|
||||
- `/src/PepperDash.Essentials.Core/Devices/GenericIRController.cs`
|
||||
- `/src/PepperDash.Essentials.Core/Comm and IR/IRPortHelper.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
public class IrOutputPortController : Device
|
||||
IROutputPort IrPort;
|
||||
public IrOutputPortController(string key, IROutputPort port, string irDriverFilepath)
|
||||
```
|
||||
|
||||
### 2.3 CEC Communication (ICec)
|
||||
|
||||
**Primary Interface**: `ICec`
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs`
|
||||
- `/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
public class CecPortController : Device, IBasicCommunicationWithStreamDebugging
|
||||
public static ICec GetCecPort(ControlPropertiesConfig config)
|
||||
```
|
||||
|
||||
## 3. Input/Output Hardware
|
||||
|
||||
### 3.1 Digital Input
|
||||
|
||||
**Primary Interface**: `IDigitalInput`
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericDigitalInputDevice.cs`
|
||||
- `/src/PepperDash.Essentials.Core/Microphone Privacy/MicrophonePrivacyController.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
public List<IDigitalInput> Inputs { get; private set; }
|
||||
void AddInput(IDigitalInput input)
|
||||
```
|
||||
|
||||
### 3.2 Versiport Support
|
||||
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportInputDevice.cs`
|
||||
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportAnalogInputDevice.cs`
|
||||
- `/src/PepperDash.Essentials.Core/CrestronIO/GenericVersiportOutputDevice.cs`
|
||||
|
||||
**Usage**: Provides flexible I/O port configuration for various signal types
|
||||
|
||||
## 4. Touchpanel Hardware
|
||||
|
||||
### 4.1 MPC3 Touchpanel
|
||||
|
||||
**Primary Class**: `MPC3Basic`
|
||||
**Key File**: `/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
public class Mpc3TouchpanelController : Device
|
||||
readonly MPC3Basic _touchpanel;
|
||||
_touchpanel = processor.ControllerTouchScreenSlotDevice as MPC3Basic;
|
||||
```
|
||||
|
||||
### 4.2 TSW Series Support
|
||||
|
||||
**Evidence**: References found in messenger files and mobile control components
|
||||
**Usage**: Integrated through mobile control messaging system for TSW touchpanel features
|
||||
|
||||
## 5. Timer and Threading
|
||||
|
||||
### 5.1 CTimer
|
||||
|
||||
**Primary Class**: `CTimer`
|
||||
**Key File**: `/src/PepperDash.Core/PasswordManagement/PasswordManager.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started"));
|
||||
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset"));
|
||||
```
|
||||
|
||||
## 6. Networking and Communication
|
||||
|
||||
### 6.1 Ethernet Communication
|
||||
|
||||
**Libraries Used**:
|
||||
- `Crestron.SimplSharpPro.EthernetCommunication`
|
||||
- `Crestron.SimplSharp.Net.Utilities.EthernetHelper`
|
||||
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Core/Comm/GenericTcpIpClient.cs`
|
||||
- `/src/PepperDash.Core/Comm/GenericTcpIpServer.cs`
|
||||
- `/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs`
|
||||
- `/src/PepperDash.Core/Comm/GenericSshClient.cs`
|
||||
- `/src/PepperDash.Core/Comm/GenericUdpServer.cs`
|
||||
|
||||
**Usage Pattern**:
|
||||
```csharp
|
||||
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
```
|
||||
|
||||
## 7. Device Management Libraries
|
||||
|
||||
### 7.1 DeviceSupport
|
||||
|
||||
**Library**: `Crestron.SimplSharpPro.DeviceSupport`
|
||||
**Usage**: Core device support infrastructure used throughout the framework
|
||||
|
||||
### 7.2 DM (DigitalMedia)
|
||||
|
||||
**Library**: `Crestron.SimplSharpPro.DM`
|
||||
**Usage**: Digital media routing and switching support
|
||||
**Evidence**: Found in routing configuration and DM output card references
|
||||
|
||||
## 8. User Interface Libraries
|
||||
|
||||
### 8.1 UI Components
|
||||
|
||||
**Library**: `Crestron.SimplSharpPro.UI`
|
||||
**Usage**: User interface elements and touchpanel controls
|
||||
|
||||
### 8.2 Smart Objects
|
||||
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/SmartObjects/SmartObjectDynamicList.cs`
|
||||
- `/src/PepperDash.Essentials.Core/SmartObjects/SubpageReferenceList/SubpageReferenceList.cs`
|
||||
|
||||
**Usage**: Advanced UI components with dynamic content
|
||||
|
||||
## 9. System Monitoring and Diagnostics
|
||||
|
||||
### 9.1 Diagnostics
|
||||
|
||||
**Library**: `Crestron.SimplSharpPro.Diagnostics`
|
||||
**Usage**: System health monitoring and performance tracking
|
||||
|
||||
### 9.2 System Information
|
||||
|
||||
**Key Files**:
|
||||
- `/src/PepperDash.Essentials.Core/Monitoring/SystemMonitorController.cs`
|
||||
|
||||
**Usage**: Provides system status, Ethernet information, and program details
|
||||
|
||||
## 10. Integration Patterns
|
||||
|
||||
### 10.1 SIMPL Bridging
|
||||
|
||||
**Pattern**: Extensive use of `BasicTriList` for SIMPL integration
|
||||
**Files**: Bridge classes throughout the framework implement `LinkToApi` methods:
|
||||
```csharp
|
||||
public abstract void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge);
|
||||
```
|
||||
|
||||
### 10.2 Device Factory Pattern
|
||||
|
||||
**Implementation**: Factory classes create hardware-specific implementations
|
||||
**Example**: `CommFactory.cs` provides communication device creation
|
||||
|
||||
### 10.3 Extension Methods
|
||||
|
||||
**Pattern**: Extensive use of extension methods for Crestron classes
|
||||
**Example**: `TriListExtensions.cs` adds 30+ extension methods to `BasicTriList`
|
||||
|
||||
## 11. Signal Processing
|
||||
|
||||
### 11.1 Signal Types
|
||||
|
||||
**Bool Signals**: Digital control and feedback
|
||||
**UShort Signals**: Analog values and numeric data
|
||||
**String Signals**: Text and configuration data
|
||||
|
||||
**Implementation**: Comprehensive signal handling in `TriListExtensions.cs`
|
||||
|
||||
## 12. Error Handling and Logging
|
||||
|
||||
**Pattern**: Consistent use of Crestron's Debug logging throughout
|
||||
**Examples**:
|
||||
```csharp
|
||||
Debug.LogMessage(LogEventLevel.Information, "Device {0} is not a valid device", dc.PortDeviceKey);
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Error Waking Panel. Maybe testing with Xpanel?");
|
||||
```
|
||||
|
||||
## 13. Threading and Synchronization
|
||||
|
||||
**Components**:
|
||||
- CTimer for time-based operations
|
||||
- Thread-safe collections and patterns
|
||||
- Event-driven programming models
|
||||
|
||||
## Conclusion
|
||||
|
||||
The PepperDash Essentials framework demonstrates sophisticated integration with the Crestron ecosystem, leveraging:
|
||||
|
||||
- **Core Infrastructure**: CrestronControlSystem, Device base classes
|
||||
- **Communication**: COM, IR, CEC, TCP/IP, SSH protocols
|
||||
- **Hardware Abstraction**: Touchpanels, I/O devices, processors
|
||||
- **User Interface**: Smart objects, signal processing, SIMPL bridging
|
||||
- **System Services**: Monitoring, diagnostics, device management
|
||||
|
||||
This analysis shows that Essentials serves as a comprehensive middleware layer, abstracting Crestron hardware complexities while providing modern software development patterns and practices.
|
||||
|
||||
---
|
||||
*Generated: [Current Date]*
|
||||
*Framework Version: PepperDash Essentials (Based on codebase analysis)*
|
||||
|
||||
88
PROGRESS_NET8_MOCKING.md
Normal file
88
PROGRESS_NET8_MOCKING.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# .NET 8 Upgrade Progress - Crestron Mocking
|
||||
|
||||
## Current Status (August 13, 2025)
|
||||
|
||||
### ✅ Completed Tasks
|
||||
1. **Namespace Migration**: Successfully migrated 26+ files from `Crestron.SimplSharp.CrestronIO` to `System.IO`
|
||||
2. **Main Projects Building**: All main solution projects (PepperDash.Essentials, PepperDash.Essentials.Core, etc.) are building successfully for .NET 8
|
||||
3. **CrestronMock Project Structure**: Established comprehensive mock structure with proper namespace hierarchy
|
||||
4. **Duplicate Definition Resolution**: Resolved 37+ duplicate type definition errors by cleaning up conflicting files
|
||||
5. **HTTP/HTTPS Client Mocks**: Implemented complete HTTP/HTTPS client mocks with proper instance-based architecture
|
||||
6. **Core Networking Mocks**: Basic TCP/UDP client/server mock implementations created
|
||||
|
||||
### 🔄 In Progress - PepperDash.Core Test Configuration
|
||||
Currently working on making PepperDash.Core build successfully with Test configuration using CrestronMock implementations.
|
||||
|
||||
#### Recent Progress:
|
||||
- ✅ Removed duplicate WebAndNetworking_New.cs file (eliminated 37 duplicate errors)
|
||||
- ✅ Cleaned duplicate type definitions from Extensions.cs
|
||||
- ✅ Implemented comprehensive HTTP/HTTPS client mocks with proper method signatures
|
||||
- ✅ Added missing TCP client properties and methods (LocalPortNumberOfClient, callback overloads)
|
||||
- 🔄 **Currently fixing**: TCPServer missing _bufferSize field and additional constructor overloads
|
||||
|
||||
#### Last Action Taken:
|
||||
Working on TCPServer.cs - added 2-parameter constructor but need to add missing `_bufferSize` private field.
|
||||
|
||||
### 🎯 Immediate Next Steps
|
||||
1. **Fix TCPServer.cs**:
|
||||
- Add missing `private int _bufferSize;` field
|
||||
- Add missing event handler properties (SocketStatusChange)
|
||||
- Add missing method overloads for SendDataAsync/ReceiveDataAsync
|
||||
|
||||
2. **Complete Remaining Mock Types**:
|
||||
- UDPServer properties (IPAddressLastMessageReceivedFrom, IPPortLastMessageReceivedFrom, IncomingDataBuffer)
|
||||
- SecureTCPServer/SecureTCPClient missing methods
|
||||
- CrestronQueue.TryToEnqueue method
|
||||
- ProgramStatusEventHandler delegate
|
||||
- Console command response methods
|
||||
|
||||
3. **System Types & Environment**:
|
||||
- InitialParametersClass properties (ApplicationNumber, RoomId, RoomName, etc.)
|
||||
- CrestronEnvironment methods (Sleep, OSVersion, GetTimeZone, etc.)
|
||||
- CrestronDataStoreStatic methods (InitCrestronDataStore, SetLocalIntValue, etc.)
|
||||
- IPAddress type and related networking types
|
||||
|
||||
### 📊 Build Status
|
||||
- **Main Projects**: ✅ All building successfully for .NET 8
|
||||
- **PepperDash.Core Test Config**: ❌ Multiple compilation errors (see below)
|
||||
- **Error Count**: ~150+ compilation errors remaining (down from 200+)
|
||||
|
||||
### 🚨 Key Error Categories Remaining
|
||||
1. **Missing Properties/Methods**: TCPClient.LocalPortNumberOfClient, UDPServer properties, etc.
|
||||
2. **Missing Types**: ProgramStatusEventHandler, SocketException, IPAddress
|
||||
3. **Method Signature Mismatches**: SendDataAsync/ReceiveDataAsync parameter counts
|
||||
4. **Enum Values**: eProgramStatusEventType.Stopping, ETHERNET_PARAMETER_TO_GET constants
|
||||
5. **Constructor Overloads**: TCPServer 2-parameter constructor, CrestronQueue constructor
|
||||
|
||||
### 📁 File Status
|
||||
#### ✅ Complete/Stable:
|
||||
- `WebAndNetworking.cs` - HTTP/HTTPS clients with proper namespace separation
|
||||
- `Extensions.cs` - CrestronInvoke and CrestronEthernetHelper (cleaned of duplicates)
|
||||
- `Console.cs` - ErrorLog, CrestronDataStoreStatic basics
|
||||
- `CrestronLogger.cs` - Proper namespace structure
|
||||
|
||||
#### 🔄 In Progress:
|
||||
- `TCPClient.cs` - Added most properties/methods, needs final validation
|
||||
- `TCPServer.cs` - Missing _bufferSize field, needs event handlers
|
||||
- `UDPServer.cs` - Missing properties and method overloads
|
||||
- `SystemTypes.cs` - Needs InitialParametersClass and CrestronEnvironment extensions
|
||||
|
||||
### 🧪 Test Strategy
|
||||
- **Transparent Mocking**: No modifications to PepperDash.Core source files required
|
||||
- **Test Configuration**: Uses CrestronMock project references instead of real Crestron libraries
|
||||
- **API Compatibility**: Mock implementations maintain identical public API surface
|
||||
|
||||
### 🔄 Command to Continue
|
||||
```bash
|
||||
cd /Users/awelker/source/pepperdash/Essentials
|
||||
dotnet build src/PepperDash.Core/PepperDash.Core.csproj -c Test --verbosity minimal
|
||||
```
|
||||
|
||||
### 📝 Notes
|
||||
- User has been manually editing files, so always check current file contents before making changes
|
||||
- Focus on Test configuration only - don't modify Debug/Release builds
|
||||
- All namespace migration work is complete and should be preserved
|
||||
- HTTP/HTTPS mocking architecture is solid and working well
|
||||
|
||||
### 🎯 Success Criteria
|
||||
Goal: Clean build of PepperDash.Core with Test configuration, enabling .NET 8 unit testing with transparent Crestron API mocking.
|
||||
@@ -1,201 +1,234 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.4.33213.308
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Devices.Common", "src\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj", "{53E204B7-97DD-441D-A96C-721DF014DF82}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "src\PepperDash.Essentials\PepperDash.Essentials.csproj", "{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Control", "Mobile Control", "{B24989D7-32B5-48D5-9AE1-5F3B17D25206}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl", "src\PepperDash.Essentials.MobileControl\PepperDash.Essentials.MobileControl.csproj", "{F6D362DE-2256-44B1-927A-8CE4705D839A}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl.Messengers", "src\PepperDash.Essentials.MobileControl.Messengers\PepperDash.Essentials.MobileControl.Messengers.csproj", "{B438694F-8FF7-464A-9EC8-10427374471F}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Essentials", "Essentials", "{AD98B742-8D85-481C-A69D-D8D8ABED39EA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
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
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
|
||||
{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}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.4.33213.308
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Devices.Common", "src\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj", "{53E204B7-97DD-441D-A96C-721DF014DF82}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "src\PepperDash.Essentials\PepperDash.Essentials.csproj", "{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Control", "Mobile Control", "{B24989D7-32B5-48D5-9AE1-5F3B17D25206}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl", "src\PepperDash.Essentials.MobileControl\PepperDash.Essentials.MobileControl.csproj", "{F6D362DE-2256-44B1-927A-8CE4705D839A}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl.Messengers", "src\PepperDash.Essentials.MobileControl.Messengers\PepperDash.Essentials.MobileControl.Messengers.csproj", "{B438694F-8FF7-464A-9EC8-10427374471F}"
|
||||
ProjectSection(ProjectDependencies) = postProject
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Essentials", "Essentials", "{AD98B742-8D85-481C-A69D-D8D8ABED39EA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
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}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrestronMock", "src\CrestronMock\CrestronMock.csproj", "{01191C7B-606D-4169-81B0-BC8BC1623CE9}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EssentialsTests", "Tests\EssentialsTests\EssentialsTests.csproj", "{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\tests\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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
|
||||
Test|Any CPU = Test|Any CPU
|
||||
Test|x64 = Test|x64
|
||||
Test|x86 = Test|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Test|x64.Build.0 = Test|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Test|x86.Build.0 = Test|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
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Test|x64.Build.0 = Test|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Test|x86.Build.0 = Test|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
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Test|x64.Build.0 = Test|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Test|x86.Build.0 = Test|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
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Test|x64.Build.0 = Test|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Test|x86.Build.0 = Test|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
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Test|x64.Build.0 = Test|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Test|x86.Build.0 = Test|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
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Test|x64.Build.0 = Test|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Test|x86.Build.0 = Test|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Release|x64.Build.0 = Release|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Release|x86.Build.0 = Release|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Test|x64.Build.0 = Test|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9}.Test|x86.Build.0 = Test|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Test|Any CPU.ActiveCfg = Test|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Test|Any CPU.Build.0 = Test|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Test|x64.ActiveCfg = Test|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Test|x64.Build.0 = Test|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Test|x86.ActiveCfg = Test|Any CPU
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB}.Test|x86.Build.0 = Test|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Test|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Test|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Test|x64.ActiveCfg = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Test|x64.Build.0 = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Test|x86.ActiveCfg = Debug|Any CPU
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6}.Test|x86.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
|
||||
{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}
|
||||
{01191C7B-606D-4169-81B0-BC8BC1623CE9} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{3EEC6E2D-ED96-4929-8BBB-C73BE499A4EB} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{E86229FE-9400-4F7E-B4CB-C43637FEE6A6} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
166
TESTING_STRATEGY.md
Normal file
166
TESTING_STRATEGY.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# PepperDash Essentials Unit Testing Strategy
|
||||
|
||||
## Problem Statement
|
||||
The PepperDash Essentials framework is tightly coupled to Crestron hardware libraries that only run on Crestron devices, making it impossible to run unit tests on development machines or in CI/CD pipelines.
|
||||
|
||||
## Solution: Abstraction Layer Pattern
|
||||
|
||||
### 1. Core Abstractions Created
|
||||
We've implemented abstraction interfaces that decouple business logic from Crestron hardware:
|
||||
|
||||
- **`ICrestronControlSystem`** - Abstracts the control system hardware
|
||||
- **`IRelayPort`** - Abstracts relay functionality
|
||||
- **`IDigitalInput`** - Abstracts digital inputs with event handling
|
||||
- **`IVersiPort`** - Abstracts VersiPort I/O
|
||||
|
||||
### 2. Adapter Pattern Implementation
|
||||
Created adapter classes that wrap Crestron objects in production:
|
||||
|
||||
```csharp
|
||||
// Production code uses adapters
|
||||
var controlSystem = new CrestronControlSystemAdapter(Global.ControlSystem);
|
||||
var processor = new CrestronProcessorTestable("key", controlSystem);
|
||||
|
||||
// Test code uses mocks
|
||||
var mockControlSystem = new Mock<ICrestronControlSystem>();
|
||||
var processor = new CrestronProcessorTestable("key", mockControlSystem.Object);
|
||||
```
|
||||
|
||||
### 3. Testable Classes
|
||||
Refactored classes to accept abstractions via dependency injection:
|
||||
|
||||
- **`CrestronProcessorTestable`** - Accepts `ICrestronControlSystem`
|
||||
- **`GenericRelayDeviceTestable`** - Accepts `IRelayPort`
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Identify Dependencies
|
||||
```bash
|
||||
# Find Crestron dependencies
|
||||
grep -r "using Crestron" --include="*.cs"
|
||||
```
|
||||
|
||||
### Step 2: Create Abstractions
|
||||
Define interfaces that mirror the Crestron API surface you need:
|
||||
```csharp
|
||||
public interface IRelayPort
|
||||
{
|
||||
void Open();
|
||||
void Close();
|
||||
void Pulse(int delayMs);
|
||||
bool State { get; }
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Implement Adapters
|
||||
Wrap Crestron objects with adapters:
|
||||
```csharp
|
||||
public class RelayPortAdapter : IRelayPort
|
||||
{
|
||||
private readonly Relay _relay;
|
||||
public void Open() => _relay.Open();
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Refactor Classes
|
||||
Accept abstractions in constructors:
|
||||
```csharp
|
||||
public class CrestronProcessorTestable
|
||||
{
|
||||
public CrestronProcessorTestable(string key, ICrestronControlSystem processor)
|
||||
{
|
||||
// Use abstraction instead of concrete type
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Write Tests
|
||||
Use mocking frameworks to test business logic:
|
||||
```csharp
|
||||
[Fact]
|
||||
public void OpenRelay_CallsRelayPortOpen()
|
||||
{
|
||||
var mockRelay = new Mock<IRelayPort>();
|
||||
var device = new GenericRelayDeviceTestable("test", mockRelay.Object);
|
||||
|
||||
device.OpenRelay();
|
||||
|
||||
mockRelay.Verify(r => r.Open(), Times.Once);
|
||||
}
|
||||
```
|
||||
|
||||
## Test Project Structure
|
||||
```
|
||||
tests/
|
||||
├── PepperDash.Essentials.Core.Tests/
|
||||
│ ├── Abstractions/ # Tests for abstraction adapters
|
||||
│ ├── Devices/ # Device-specific tests
|
||||
│ └── *.csproj # Test project file
|
||||
└── README.md # Testing documentation
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
The `.github/workflows/ci.yml` file runs tests automatically on:
|
||||
- Push to main/develop branches
|
||||
- Pull requests
|
||||
- Generates code coverage reports
|
||||
|
||||
### Running Tests Locally
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run with coverage
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
|
||||
# Run specific tests
|
||||
dotnet test --filter "FullyQualifiedName~CrestronProcessor"
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Unit Testing Without Hardware** - Tests run on any machine
|
||||
2. **CI/CD Integration** - Automated testing in pipelines
|
||||
3. **Better Design** - Encourages SOLID principles
|
||||
4. **Faster Development** - No need for hardware to test logic
|
||||
5. **Higher Code Quality** - Catch bugs before deployment
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Existing Code
|
||||
1. Identify classes with Crestron dependencies
|
||||
2. Create abstraction interfaces
|
||||
3. Implement adapters
|
||||
4. Create testable versions accepting abstractions
|
||||
5. Write unit tests
|
||||
|
||||
### For New Code
|
||||
1. Always code against abstractions, not Crestron types
|
||||
2. Use dependency injection
|
||||
3. Write tests first (TDD approach)
|
||||
|
||||
## Current Test Coverage
|
||||
- ✅ CrestronProcessor relay management
|
||||
- ✅ GenericRelayDevice operations
|
||||
- ✅ Digital input event handling
|
||||
- ✅ VersiPort analog/digital operations
|
||||
|
||||
## Next Steps
|
||||
1. Expand abstractions for more Crestron components
|
||||
2. Increase test coverage across all modules
|
||||
3. Add integration tests with mock hardware
|
||||
4. Document testing best practices
|
||||
5. Create code generation tools for adapters
|
||||
|
||||
## Tools Used
|
||||
- **xUnit** - Test framework
|
||||
- **Moq** - Mocking library
|
||||
- **FluentAssertions** - Readable assertions
|
||||
- **Coverlet** - Code coverage
|
||||
- **GitHub Actions** - CI/CD
|
||||
|
||||
## Summary
|
||||
By introducing an abstraction layer between the business logic and Crestron hardware dependencies, we've successfully enabled unit testing for the PepperDash Essentials framework. This approach allows development and testing without physical hardware while maintaining full compatibility with Crestron systems in production.
|
||||
@@ -52,7 +52,6 @@
|
||||
"_appLogoPath": "docs/images/favicon-32x32.png",
|
||||
"_appFaviconPath": "docs/images/favicon.ico",
|
||||
"_disableToc": false,
|
||||
"_enableNewTab": true,
|
||||
"pdf": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ Types of things in `DeviceManager`:
|
||||
|
||||
A Device doesn't always represent a physical piece of hardware, but rather a logical construct that "does something" and is used by one or more other devices in the running program. For example, we create a room device, and its corresponding Fusion device, and that room has a Cisco codec device, with an attached SSh client device. All of these lie in a flat collection in the `DeviceManager`.
|
||||
|
||||
> The `DeviceManager` is nothing more than a modified collection of things, and technically those things don't have to be Devices, but must at least implement the `IKeyed` (`PepperDash.Core.IKeyed`) interface (simply so items can be looked up by their key.) Items in the `DeviceManager` that are Devices are run through additional steps of [activation](~/docs/technical-docs/Arch-activate.md#2-pre-activation) at startup. This collection of devices is all interrelated by their string keys.
|
||||
> The `DeviceManager` is nothing more than a modified collection of things, and technically those things don't have to be Devices, but must at least implement the `IKeyed` (`PepperDash.Core.IKeyed`) interface (simply so items can be looked up by their key.) Items in the `DeviceManager` that are Devices are run through additional steps of [activation](~/docs/Arch-activate.md#2-pre-activation) at startup. This collection of devices is all interrelated by their string keys.
|
||||
|
||||
In this flat design, we spin up devices, and then introduce them to their "coworkers and bosses" - the other devices and logical units that they will interact with - and get them all operating together to form a running unit. For example: A room configuration will contain a "VideoCodecKey" property and a "DefaultDisplayKey" property. The `DeviceManager` provides the room with the codec or displays having the appropriate keys. What the room does with those is dependent on its coding.
|
||||
|
||||
@@ -38,4 +38,4 @@ This flat structure ensures that every device in a system exists in one place an
|
||||
|
||||

|
||||
|
||||
Next: [Configurable lifecycle](~/docs/technical-docs/Arch-lifecycle.md)
|
||||
Next: [Configurable lifecycle](~/docs/Arch-lifecycle.md)
|
||||
@@ -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
|
||||
protected override bool CustomActivate()
|
||||
public 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
|
||||
protected override bool CustomActivate()
|
||||
public override bool CustomActivate()
|
||||
{
|
||||
foreach (var i in Config.Inputs)
|
||||
{
|
||||
@@ -105,7 +105,7 @@ Each of the three activation phases operates in a try/catch block for each devic
|
||||
|
||||
In any real-world system, devices and business logic need to talk to each other, otherwise, what's the point of all this coding? When creating your classes and configuration, it is best practice to _try_ not to "plug" one device into another during construction or activation. For example your touchpanel controller class has a `Display1` property that holds the display-1 object. Rather, it may be better to refer to the device as it is stored in the `DeviceManager` when it's needed using the static `DeviceManager.GetDeviceForKey(key)` method to get a reference to the device, which can be cast using various interfaces/class types, and then interacted with. This prevents objects from being referenced in places where the developer may later forget to dereference them, causing memory leak. This will become more important as Essentials becomes more able to be reconfigured at runtime.
|
||||
|
||||
As an example, [connection-based routing](~/docs/technical-docs/Connection-based-routing.md#essentials-connection-based-routing) uses these methods. When a route is requested, the collection of tielines and devices is searched for the devices and paths necessary to complete a route, but there are no devices or tie lines that are object-referenced in running code. It can all be torn down and reconfigured without any memory-management dereferencing, setting things to null.
|
||||
As an example, [connection-based routing](~/docs/Connection-based-routing.md#essentials-connection-based-routing) uses these methods. When a route is requested, the collection of tielines and devices is searched for the devices and paths necessary to complete a route, but there are no devices or tie lines that are object-referenced in running code. It can all be torn down and reconfigured without any memory-management dereferencing, setting things to null.
|
||||
|
||||
## Device Initialization
|
||||
|
||||
@@ -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
|
||||
protected override void Initialize()
|
||||
public override void Initialize()
|
||||
{
|
||||
var socket = Communication as ISocketStatus;
|
||||
if (socket != null)
|
||||
@@ -155,4 +155,4 @@ Robust C#-based system code should not depend on "order" or "time" to get runnin
|
||||
|
||||
When designing new Device-based classes, be it rooms, devices, port controllers, bridges, make them as independent as possible. They could exist alone in a program with no required partner objects, and just quietly exist without failing. We want the system to be fast and flexible, and keeping the interdependence between objects at a minimum improves this flexibility into the future.
|
||||
|
||||
Next: [More architecture](~/docs/technical-docs/Arch-topics.md)
|
||||
Next: [More architecture](~/docs/Arch-topics.md)
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
The diagram below describes how Essentials gets a program up and running.
|
||||
|
||||
(The various activation phases are covered in more detail on the [next page](~/docs/technical-docs/Arch-activate.md))
|
||||
(The various activation phases are covered in more detail on the [next page](~/docs/Arch-activate.md))
|
||||
|
||||

|
||||
|
||||
Next: [Activation phases](~/docs/technical-docs/Arch-activate.md)
|
||||
Next: [Activation phases](~/docs/Arch-activate.md)
|
||||
@@ -16,4 +16,4 @@ The diagram below shows the reference dependencies that exist between the differ
|
||||
|
||||

|
||||
|
||||
Next: [Architecture](~/docs/technical-docs/Arch-1.md)
|
||||
Next: [Architecture](~/docs/Arch-1.md)
|
||||
@@ -6,10 +6,10 @@ One of the most powerful features of Essentials is the ability to bridge SIMPL t
|
||||
|
||||
Follow the links below for examples of bridging to hardware and network resources.
|
||||
|
||||
**[GenericComm Bridging](~/docs/usage/GenericComm.md)**
|
||||
**[GenericComm Bridging](~/docs/GenericComm.md)**
|
||||
|
||||
**[RelayOutput Bridging](~/docs/usage/RelayOutput.md)**
|
||||
**[RelayOutput Bridging](~/docs/RelayOutput.md)**
|
||||
|
||||
**[Digital Input Bridging](~/docs/usage/DigitalInput.md)**
|
||||
**[Digital Input Bridging](~/docs/DigitalInput.md)**
|
||||
|
||||
**[Card Frame Bridging](~/docs/CardFrame.md)**
|
||||
@@ -4,44 +4,23 @@
|
||||
[YouTube Video - Getting Started with PepperDash Essentials](https://youtu.be/FxEZtbpCwiQ)
|
||||
***
|
||||
|
||||
## Get a CPZ
|
||||
## Download or clone
|
||||
|
||||
### Prerequisites
|
||||
|
||||
* [VS Code](https://code.visualstudio.com/)
|
||||
* [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download)
|
||||
* [Git](https://git-scm.com/)
|
||||
|
||||
> Note: Essentials 2.x.x uses .NET Framework 4.7.2 currently. The .NET 9 SDK will build the project with the appropriate references
|
||||
|
||||
### Build From Source
|
||||
|
||||
1. Clone the repo: `git clone https://github.com/PepperDash/Essentials.git`
|
||||
2. Open the folder in VS Code
|
||||
3. Build using the dotnet CLI: `dotnet build`
|
||||
|
||||
### Download the latest release
|
||||
|
||||
The latest release can be found on [Github](https://github.com/PepperDash/Essentials/releases/latest)
|
||||
You may clone Essentials at <https://github.com/PepperDash/Essentials.git>
|
||||
|
||||
## How to Get Started
|
||||
|
||||
2. Using an SFTP client or Crestron Toolbox, load the downloaded (or built) cpz to the processor in program slot 1
|
||||
1. If using SFTP, connect via SSH and start the program by sending console command `progload -p:1`
|
||||
3. On first boot, the Essentials Application will build the necessary configuration folder structure in the user/program1/ path.
|
||||
4. The application has some example configuration files included. Copy `/Program01/Example Configuration/EssentialsSpaceHuddleRoom/configurationFile-HuddleSpace-2-Source.json` to the `/User/Program1/` folder.
|
||||
6. Reset the program via console `progreset -p:1`. The program will load the example configuration file.
|
||||
This section assumes knowledge of loading programs to and working with the file system on a Crestron processor.
|
||||
|
||||
Once Essentials is running with a valid configuration, the following console commands can be used to see what's going on:
|
||||
1. Using an SFTP client, load `PepperDashEssentials1.4.32.cpz` to the processor in program slot 1 and start the program by sending console command `progload -p:1`
|
||||
1. On first boot, the Essentials Application will build the necessary configuration folder structure in the User/Program1/ path.
|
||||
1. The application has some example configuration files included. Copy `/Program01/Example Configuration/EssentialsSpaceHuddleRoom/configurationFile-HuddleSpace-2-Source.json` to the `/User/Program1/` folder.
|
||||
1. Copy the SGD files from `/Program01/SGD` to `/User/Program1/sgd`
|
||||
1. Reset the program via console `progreset -p:1`. The program will load the example configuration file.
|
||||
1. Via console, you can run the `devlist:1` command to get some insight into what has been loaded from the configuration file into the system . This will print the basic device information in the form of ["key"] "Name". The "key" value is what we can use to interact with each device uniquely.
|
||||
1. Run the command `devprops:1 display-1`. This will print the real-time property values of the device with key "display-1".
|
||||
1. Run the command `devmethods:1 display-1`. This will print the public methods available for the device with key "display-1".
|
||||
1. Run the command `devjson:1 {"deviceKey":"display-1","methodName":"PowerOn", "params": []}`. This will call the method PowerOn() on the device with key "display-1".
|
||||
1. Run the provided example XPanel SmartGraphics project and connect to your processor at the appropriate IPID.
|
||||
|
||||
* ```devlist:1```
|
||||
* Print the list of devices in [{key}] {name} format
|
||||
* The key of a device can be used with the rest of the commands to get more information
|
||||
* `devprops:1 {deviceKey}`
|
||||
* Print the real-time property values of the device with key "display-1".
|
||||
* `devmethods:1 display-1`
|
||||
* Print the public methods available for the device with key "display-1".
|
||||
* `devjson:1 {"deviceKey":"display-1","methodName":"PowerOn", "params": []}`
|
||||
* Call the method `PowerOn()` on the device with key "display-1".
|
||||
|
||||
Next: [Standalone use](~/docs/usage/Standalone-Use.md)
|
||||
Next: [Standalone use](~/docs/Standalone-Use.md)
|
||||
|
||||
@@ -39,7 +39,7 @@ Thanks!
|
||||
|
||||
## Collaboration
|
||||
|
||||
Essentials is an open-source project and we encourage collaboration on this community project. For features that may not be useful to the greater community, or for just-plain learning, we want to remind developers to try writing plugins for Essentials. More information can be found here: [Plugins](~/docs/technical-docs/Plugins.md)
|
||||
Essentials is an open-source project and we encourage collaboration on this community project. For features that may not be useful to the greater community, or for just-plain learning, we want to remind developers to try writing plugins for Essentials. More information can be found here: [Plugins](~/docs/Plugins.md)
|
||||
|
||||
### Open-source-collaborative workflow
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Deprecated
|
||||
|
||||
**Note : this entry is out of date - please see [Plugins](~/docs/technical-docs/Plugins.md)**
|
||||
**Note : this entry is out of date - please see [Plugins](~/docs/Plugins.md)**
|
||||
|
||||
## What are Essentials Plugins?
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ Example device config: <https://github.com/PepperDash/Essentials/blob/main/Peppe
|
||||
|
||||
1. A bridge need not only bridge between applications on the same processor. A bridge may bridge to an application on a completely separate processor; simply define the ip address in the Bridge control properties accordingly.
|
||||
|
||||
1. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/technical-docs/Feedback-Classes.md) for this.
|
||||
1. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/Feedback-Classes.md) for this.
|
||||
|
||||
1. When building plugins, we highly recommend reusing JoinMaps, as this will make code more easily interchangeable. For example; if you were to build a display plugin, we'd recommend you use/extend the existing DisplayControllerJoinMap. This way, you can swap plugins without needing any change on the Simpl Windows side. This is extremely powerful when maintaining Simpl Windows code bases for large deployments that may utilize differing equipment per room. If you can build a Simpl Windows program that interacts with established join maps, you can swap out the device via config without any change needed to Simpl Windows.
|
||||
|
||||
@@ -474,4 +474,4 @@ Example device config: <https://github.com/PepperDash/Essentials/blob/main/Peppe
|
||||
|
||||
We've provided an [example program](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for SIMPL Windows that works with the provided example Essentials configuration file [SIMPLBridgeExample_configurationFile.json](https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json). Load Essentials and the example SIMPL program to two slots on the same processor and you can get a better idea of how to take advantage of SIMPL Windows bridging.
|
||||
|
||||
Next: [Essentials architecture](~/docs/technical-docs/Arch-summary.md)
|
||||
Next: [Essentials architecture](~/docs/Arch-summary.md)
|
||||
|
||||
@@ -286,7 +286,7 @@ Example device config: <https://github.com/PepperDash/Essentials/blob/main/Peppe
|
||||
|
||||
3. A bridge need not only bridge between applications on the same processor. A bridge may bridge to an application on a completely separate processor; simply define the ip address in the Bridge control properties accordingly.
|
||||
|
||||
4. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/technical-docs/Feedback-Classes.md) for this.
|
||||
4. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/Feedback-Classes.md) for this.
|
||||
|
||||
5. When building plugins, we highly recommend reusing JoinMaps, as this will make code more easily interchangeable. For example; if you were to build a display plugin, we'd recommend you use/extend the existing `DisplayControllerJoinMap`. This way, you can swap plugins without needing any change on the SIMPL Windows side. This is extremely powerful when maintaining SIMPL Windows code bases for large deployments that may utilize differing equipment per room. If you can build a SIMPL Windows program that interacts with established join maps, you can swap out the device via config without any change needed to SIMPL Windows.
|
||||
|
||||
@@ -302,7 +302,7 @@ Example device config: <https://github.com/PepperDash/Essentials/blob/main/Peppe
|
||||
|
||||
## Join Map Documentation
|
||||
|
||||
[Join Map Documentation](~/docs/usage/JoinMaps.md)
|
||||
[Join Map Documentation](~/docs/JoinMaps.md)
|
||||
|
||||
## Device Type Join Maps
|
||||
|
||||
@@ -408,4 +408,4 @@ Please note that these joinmaps _may_ be using a deprecated implementation. The
|
||||
|
||||
We've provided an [example program](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for SIMPL Windows that works with the provided example Essentials configuration file [SIMPLBridgeExample_configurationFile.json](https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json). Load Essentials and the example SIMPL program to two slots on the same processor and you can get a better idea of how to take advantage of SIMPL Windows bridging.
|
||||
|
||||
Next: [Essentials architecture](~/docs/technical-docs/Arch-summary.md)
|
||||
Next: [Essentials architecture](~/docs/Arch-summary.md)
|
||||
@@ -1,6 +1,6 @@
|
||||
# SIMPL Windows Bridging
|
||||
|
||||
**Note : this entry is out of date - please see [SIMPL Windows Bridging - Updated](~/docs/usage/SIMPL-Bridging-Updated.md)**
|
||||
**Note : this entry is out of date - please see [SIMPL Windows Bridging - Updated](~/docs/SIMPL-Bridging-Updated.md)**
|
||||
|
||||
Essentials allows for devices defined within the SIMPL# Pro application to be bridged to a SIMPL Windows application over Ethernet Intersystem Communication (EISC). This allows a SIMPL Windows program to take advantage of some of the features of the SIMPL# Pro environment, without requiring the entire application to be written in C#.
|
||||
|
||||
@@ -356,7 +356,7 @@ Example device config: <https://github.com/PepperDash/Essentials/blob/main/Peppe
|
||||
|
||||
1. A bridge need not only bridge between applications on the same processor. A bridge may bridge to an application on a completely separate processor; simply define the ip address in the Bridge control properties accordingly.
|
||||
|
||||
1. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/technical-docs/Feedback-Classes.md) for this.
|
||||
1. For devices included in Essentials, you will be able to find defined join maps below. If you are building your own plugins, you will need to build the join map yourself. It would be beneficial to review the wiki entry on the [Feedback Class](~/docs/Feedback-Classes.md) for this.
|
||||
|
||||
1. When building plugins, we highly recommend reusing JoinMaps, as this will make code more easily interchangeable. For example; if you were to build a display plugin, we'd recommend you use/extend the existing DisplayControllerJoinMap. This way, you can swap plugins without needing any change on the Simpl Windows side. This is extremely powerful when maintaining Simpl Windows code bases for large deployments that may utilize differing equipment per room. If you can build a Simpl Windows program that interacts with established join maps, you can swap out the device via config without any change needed to Simpl Windows.
|
||||
|
||||
@@ -472,4 +472,4 @@ Example device config: <https://github.com/PepperDash/Essentials/blob/main/Peppe
|
||||
|
||||
We've provided an [example program](https://github.com/PepperDash/EssentialsSIMPLWindowsBridgeExample) for SIMPL Windows that works with the provided example Essentials configuration file [SIMPLBridgeExample_configurationFile.json](https://github.com/PepperDash/Essentials/blob/main/PepperDashEssentials/Example%20Configuration/SIMPLBridging/SIMPLBridgeExample_configurationFile.json). Load Essentials and the example SIMPL program to two slots on the same processor and you can get a better idea of how to take advantage of SIMPL Windows bridging.
|
||||
|
||||
Next: [Essentials architecture](~/docs/technical-docs/Arch-summary.md)
|
||||
Next: [Essentials architecture](~/docs/Arch-summary.md)
|
||||
|
||||
@@ -8,7 +8,7 @@ By defining devices and a room in a JSON configuration file, Essentials can cont
|
||||
|
||||
### Devices
|
||||
|
||||
Essentials supports device plugins for communicating with various devices using both standard Crestron CIP communications, Cresnet, SSH, or other TCP/IP-based communication methods. See [the Plugins section](~/docs/technical-docs/Plugins.md) for more details
|
||||
Essentials supports device plugins for communicating with various devices using both standard Crestron CIP communications, Cresnet, SSH, or other TCP/IP-based communication methods. See [the Plugins section](~/docs/Plugins.md) for more details
|
||||
|
||||
### Rooms
|
||||
|
||||
@@ -16,4 +16,4 @@ In order to tie together equipment into a unit that comprises what devices are u
|
||||
|
||||
See Also: [[Supported Devices|Supported-Devices]]
|
||||
|
||||
Next: [Simpl Windows bridging](~/docs/usage/SIMPL-Bridging-Updated.md)
|
||||
Next: [Simpl Windows bridging](~/docs/SIMPL-Bridging-Updated.md)
|
||||
@@ -1,148 +0,0 @@
|
||||
# How to Add Documentation to Essentials
|
||||
|
||||
This guide explains how to add new documentation articles to the Essentials docFx site.
|
||||
|
||||
## Overview
|
||||
|
||||
The Essentials documentation uses [docFx](https://dotnet.github.io/docfx/) to generate a static documentation website. Documentation files are organized in a hierarchical structure with a table of contents (TOC) file that defines the site navigation. Documentation should be organized and written to fit into the [Diátaxis](https://diataxis.fr/start-here/) conceptual framework.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Documentation files are located in `/docs/docs/` and organized into the following subdirectories:
|
||||
|
||||
- **how-to/** - Step-by-step guides and tutorials
|
||||
- **usage/** - Usage documentation for SIMPL bridging, standalone use, and hardware integration
|
||||
- **technical-docs/** - Technical documentation including architecture, plugins, and API references
|
||||
- **images/** - Image assets used in documentation
|
||||
|
||||
## Adding a New Document
|
||||
|
||||
### Step 1: Create Your Markdown File
|
||||
|
||||
1. Determine which category your document belongs to (how-to, usage, or technical-docs)
|
||||
2. Create a new `.md` file in the appropriate subdirectory
|
||||
3. Use a descriptive filename with hyphens (e.g., `my-new-feature.md`)
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
# For a how-to guide
|
||||
touch /docs/docs/how-to/configure-audio-settings.md
|
||||
|
||||
# For usage documentation
|
||||
touch /docs/docs/usage/video-switcher-control.md
|
||||
|
||||
# For technical documentation
|
||||
touch /docs/docs/technical-docs/custom-device-plugin.md
|
||||
```
|
||||
|
||||
### Step 2: Write Your Content
|
||||
|
||||
Start your markdown file with a level 1 heading (`#`) that serves as the page title:
|
||||
|
||||
```markdown
|
||||
# Your Document Title
|
||||
|
||||
Brief introduction to the topic.
|
||||
|
||||
## Section Heading
|
||||
|
||||
Content goes here...
|
||||
|
||||
### Subsection
|
||||
|
||||
More detailed content...
|
||||
```
|
||||
|
||||
**Markdown Features:**
|
||||
- Use standard markdown syntax
|
||||
- Include code blocks with language specifiers (```csharp, ```json, etc.)
|
||||
- Add images: ``
|
||||
- Link to other docs: `[Link text](../usage/related-doc.md)`
|
||||
|
||||
### Step 3: Add to Table of Contents
|
||||
|
||||
Edit `/docs/docs/toc.yml` to add your new document to the navigation:
|
||||
|
||||
```yaml
|
||||
- name: How-to's
|
||||
items:
|
||||
- href: how-to/how-to-add-docs.md
|
||||
- href: how-to/your-new-doc.md # Add your document here
|
||||
```
|
||||
|
||||
**TOC Structure:**
|
||||
- `name:` - Display name in the navigation menu
|
||||
- `href:` - Relative path to the markdown file
|
||||
- `items:` - Nested items for hierarchical navigation
|
||||
|
||||
**Example with nested items:**
|
||||
```yaml
|
||||
- name: Usage
|
||||
items:
|
||||
- name: SIMPL Bridging
|
||||
href: usage/SIMPL-Bridging-Updated.md
|
||||
items:
|
||||
- name: Your Sub-Topic
|
||||
href: usage/your-sub-topic.md
|
||||
```
|
||||
|
||||
### Step 4: Test Locally
|
||||
|
||||
Build and preview the docFx site locally to verify your changes:
|
||||
|
||||
```bash
|
||||
# Navigate to the docs directory
|
||||
cd docs
|
||||
|
||||
# Build the documentation
|
||||
docfx build
|
||||
|
||||
# Serve the site locally
|
||||
docfx serve _site
|
||||
```
|
||||
|
||||
Then open your browser to `http://localhost:8080` to view the site.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### File Naming
|
||||
- Use lowercase with hyphens: `my-document-name.md`
|
||||
- Be descriptive but concise
|
||||
- Avoid special characters
|
||||
|
||||
### Content Guidelines
|
||||
- Start with a clear introduction
|
||||
- Use hierarchical headings (H1 → H2 → H3)
|
||||
- Include code examples where appropriate
|
||||
- Add images to illustrate complex concepts
|
||||
- Link to related documentation
|
||||
|
||||
### TOC Organization
|
||||
- Group related documents under the same parent
|
||||
- Order items logically (basic → advanced)
|
||||
- Keep the TOC hierarchy shallow (2-3 levels max)
|
||||
- Use clear, descriptive names for navigation items
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Document Not Appearing
|
||||
- Verify the file path in `toc.yml` is correct and uses forward slashes
|
||||
- Ensure the markdown file exists in the specified location
|
||||
- Check for YAML syntax errors in `toc.yml`
|
||||
|
||||
### Images Not Loading
|
||||
- Verify image path is relative to the markdown file location
|
||||
- Use `../images/` for files in the images directory
|
||||
- Ensure image files are committed to the repository
|
||||
|
||||
### Broken Links
|
||||
- Use relative paths for internal links
|
||||
- Test all links after building the site
|
||||
- Use `.md` extension when linking to other documentation files
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [docFx Documentation](https://dotnet.github.io/docfx/)
|
||||
- [Markdown Guide](https://www.markdownguide.org/)
|
||||
- [YAML Syntax](https://yaml.org/spec/1.2/spec.html)
|
||||
- [Diátaxis](https://diataxis.fr/start-here/)
|
||||
@@ -1,52 +1,48 @@
|
||||
- name: Get Started With Essentials
|
||||
- href: ../index.md
|
||||
- href: Get-started.md
|
||||
- name: How-to's
|
||||
items:
|
||||
- name: How to add an article or doc page
|
||||
href: how-to/how-to-add-docs.md
|
||||
- href: Get-started.md
|
||||
- name: Usage
|
||||
items:
|
||||
- href: usage/Standalone-Use.md
|
||||
- href: usage/SIMPL-Bridging-Updated.md
|
||||
- href: Standalone-Use.md
|
||||
- href: SIMPL-Bridging-Updated.md
|
||||
items:
|
||||
- name: Join Maps
|
||||
href: usage/JoinMaps.md
|
||||
href: JoinMaps.md
|
||||
- name: Bridging to Hardware Resources
|
||||
href: usage/Bridging-To-Hardware-Resources.md
|
||||
href: Bridging-To-Hardware-Resources.md
|
||||
items:
|
||||
- name: GenericComm Bridging
|
||||
href: usage/GenericComm.md
|
||||
href: GenericComm.md
|
||||
- name: RelayOutput Bridging
|
||||
href: usage/RelayOutput.md
|
||||
href: RelayOutput.md
|
||||
- name: Digital Input Bridging
|
||||
href: usage/DigitalInput.md
|
||||
href: DigitalInput.md
|
||||
- name: IR Driver Bridging
|
||||
href: usage/IR-Driver-Bridging.md
|
||||
href: IR-Driver-Bridging.md
|
||||
- name: Technical documentation
|
||||
items:
|
||||
- href: technical-docs/Arch-summary.md
|
||||
- href: Arch-summary.md
|
||||
- name: Devices and DeviceManager
|
||||
href: technical-docs/Arch-1.md
|
||||
href: Arch-1.md
|
||||
- name: Configurable lifecycle
|
||||
href: technical-docs/Arch-lifecycle.md
|
||||
href: Arch-lifecycle.md
|
||||
- name: Activation phases
|
||||
href: technical-docs/Arch-activate.md
|
||||
href: Arch-activate.md
|
||||
- name: More
|
||||
href: technical-docs/Arch-topics.md
|
||||
href: Arch-topics.md
|
||||
- name: Plugins
|
||||
href: technical-docs/Plugins.md
|
||||
href: Plugins.md
|
||||
- name: Communication Basics
|
||||
href: technical-docs/Communication-Basics.md
|
||||
href: Communication-Basics.md
|
||||
- name: Debugging
|
||||
href: technical-docs/Debugging.md
|
||||
href: Debugging.md
|
||||
- name: Feedback Classes
|
||||
href: technical-docs/Feedback-Classes.md
|
||||
href: Feedback-Classes.md
|
||||
- name: Connection Based Routing
|
||||
href: technical-docs/Connection-Based-Routing.md
|
||||
href: Connection-Based-Routing.md
|
||||
- name: Configuration Structure
|
||||
href: technical-docs/ConfigurationStructure.md
|
||||
href: ConfigurationStructure.md
|
||||
- name: Supported Devices
|
||||
href: technical-docs/Supported-Devices.md
|
||||
href: Supported-Devices.md
|
||||
- name: Glossary of Terms
|
||||
href: technical-docs/Glossary-of-Terms.md
|
||||
href: Glossary-of-Terms.md
|
||||
@@ -8,12 +8,12 @@ Essentials is a collection of C# libraries that can be used in many ways. It is
|
||||
|
||||
## Get started
|
||||
|
||||
- [Download an Essentials build or clone the repo](~/docs/Get-started.md)
|
||||
- [Get started](~/docs/Get-started.md)
|
||||
- [Download essentials build or clone repo](~/docs/Get-started.md)
|
||||
- [How to get started](~/docs/Get-started.md)
|
||||
- [YouTube Video Series Playlist](https://youtube.com/playlist?list=PLKOoNNwgPFZdV5wDEBDZxTHu1KROspaBu)
|
||||
- [Discord Server](https://discord.gg/6Vh3ssDdPs)
|
||||
|
||||
Or use the links to the left to navigate our documentation.
|
||||
Or use the links to the right to navigate our documentation.
|
||||
|
||||
---
|
||||
|
||||
@@ -25,12 +25,21 @@ Or use the links to the left to navigate our documentation.
|
||||
- Shared resources made easily available
|
||||
- More flexibility with less code
|
||||
- Configurable using simple JSON files
|
||||
- Is awesome
|
||||
|
||||
---
|
||||
|
||||
## Comment
|
||||
|
||||
The Essentials wiki is clearly in-progress right now. Take a look at the links to the right. We are actively working on this documentation, so please be patient with us. If you have any comments on or suggestions for the documentation, please file an issue here, with as much detail as you can provide: <https://github.com/PepperDash/Essentials/issues>
|
||||
|
||||
Thanks!
|
||||
|
||||
---
|
||||
|
||||
## Collaboration
|
||||
|
||||
Essentials is an open-source project and we encourage collaboration on this community project. For features that may not be useful to the greater community, or for just-plain learning, we want to remind developers to try writing plugins for Essentials. More information can be found here: [Plugins](~/docs/technical-docs/Plugins.md)
|
||||
Essentials is an open-source project and we encourage collaboration on this community project. For features that may not be useful to the greater community, or for just-plain learning, we want to remind developers to try writing plugins for Essentials. More information can be found here: [Plugins](~/docs/Plugins.md)
|
||||
|
||||
### Open-source-collaborative workflow
|
||||
|
||||
@@ -43,7 +52,7 @@ The `main` branch always contain the latest stable version. The `development` br
|
||||
- Example: `feature/add-awesomeness` or `hotfix/really-big-oops`
|
||||
- When working on a new feature or bugfix, branch from the `development` branch. When working on a hotfix, branch from `main`.
|
||||
3. Make commits as necessary (often is better). And use concise, descriptive language, leveraging issue notation and/or [Closing Keywords](https://help.github.com/articles/closing-issues-using-keywords) to ensure any issues addressed by your work are referenced accordingly.
|
||||
4. When the scope of the work for your branch is complete, make sure to update your branch in case further progress has been made since the repo was forked
|
||||
4. When the scope of the work for your branch is complete, make sure to rebase your branch in case further progress has been made since the repo was forked
|
||||
5. Create a Pull Request to pull your branch into the appropriate branch in the main repository.
|
||||
6. Your Pull Request will be reviewed by our team and evaluated for inclusion into the main repository.
|
||||
|
||||
|
||||
183
src/CrestronMock/CCriticalSection.cs
Normal file
183
src/CrestronMock/CCriticalSection.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// Mock implementation of Crestron CCriticalSection for testing purposes
|
||||
/// Provides the same public API surface as the real CCriticalSection
|
||||
/// </summary>
|
||||
public class CCriticalSection : IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly object _lockObject = new object();
|
||||
private readonly ReaderWriterLockSlim _readerWriterLock = new ReaderWriterLockSlim();
|
||||
private bool _disposed = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>Initializes a new instance of the CCriticalSection class</summary>
|
||||
public CCriticalSection()
|
||||
{
|
||||
// Mock implementation - no actual initialization required
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>Enters the critical section</summary>
|
||||
public void Enter()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(CCriticalSection));
|
||||
Monitor.Enter(_lockObject);
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter the critical section</summary>
|
||||
/// <returns>True if the critical section was entered successfully</returns>
|
||||
public bool TryEnter()
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return Monitor.TryEnter(_lockObject);
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter the critical section with a timeout</summary>
|
||||
/// <param name="timeout">Timeout in milliseconds</param>
|
||||
/// <returns>True if the critical section was entered successfully</returns>
|
||||
public bool TryEnter(int timeout)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return Monitor.TryEnter(_lockObject, timeout);
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter the critical section with a TimeSpan timeout</summary>
|
||||
/// <param name="timeout">Timeout as TimeSpan</param>
|
||||
/// <returns>True if the critical section was entered successfully</returns>
|
||||
public bool TryEnter(TimeSpan timeout)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return Monitor.TryEnter(_lockObject, timeout);
|
||||
}
|
||||
|
||||
/// <summary>Leaves the critical section</summary>
|
||||
public void Leave()
|
||||
{
|
||||
if (_disposed) return;
|
||||
try
|
||||
{
|
||||
Monitor.Exit(_lockObject);
|
||||
}
|
||||
catch (SynchronizationLockException)
|
||||
{
|
||||
// Ignore if not held by current thread
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enters a read lock</summary>
|
||||
public void EnterReadLock()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(CCriticalSection));
|
||||
_readerWriterLock.EnterReadLock();
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter a read lock</summary>
|
||||
/// <returns>True if the read lock was acquired successfully</returns>
|
||||
public bool TryEnterReadLock()
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return _readerWriterLock.TryEnterReadLock(0);
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter a read lock with a timeout</summary>
|
||||
/// <param name="timeout">Timeout in milliseconds</param>
|
||||
/// <returns>True if the read lock was acquired successfully</returns>
|
||||
public bool TryEnterReadLock(int timeout)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return _readerWriterLock.TryEnterReadLock(timeout);
|
||||
}
|
||||
|
||||
/// <summary>Exits the read lock</summary>
|
||||
public void ExitReadLock()
|
||||
{
|
||||
if (_disposed) return;
|
||||
try
|
||||
{
|
||||
_readerWriterLock.ExitReadLock();
|
||||
}
|
||||
catch (SynchronizationLockException)
|
||||
{
|
||||
// Ignore if not held by current thread
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enters a write lock</summary>
|
||||
public void EnterWriteLock()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(CCriticalSection));
|
||||
_readerWriterLock.EnterWriteLock();
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter a write lock</summary>
|
||||
/// <returns>True if the write lock was acquired successfully</returns>
|
||||
public bool TryEnterWriteLock()
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return _readerWriterLock.TryEnterWriteLock(0);
|
||||
}
|
||||
|
||||
/// <summary>Tries to enter a write lock with a timeout</summary>
|
||||
/// <param name="timeout">Timeout in milliseconds</param>
|
||||
/// <returns>True if the write lock was acquired successfully</returns>
|
||||
public bool TryEnterWriteLock(int timeout)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
return _readerWriterLock.TryEnterWriteLock(timeout);
|
||||
}
|
||||
|
||||
/// <summary>Exits the write lock</summary>
|
||||
public void ExitWriteLock()
|
||||
{
|
||||
if (_disposed) return;
|
||||
try
|
||||
{
|
||||
_readerWriterLock.ExitWriteLock();
|
||||
}
|
||||
catch (SynchronizationLockException)
|
||||
{
|
||||
// Ignore if not held by current thread
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
/// <summary>Disposes the critical section and releases resources</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Protected dispose method</summary>
|
||||
/// <param name="disposing">True if disposing managed resources</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_readerWriterLock?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
191
src/CrestronMock/CTimer.cs
Normal file
191
src/CrestronMock/CTimer.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
/// <summary>Mock timer event handler</summary>
|
||||
/// <param name="userObject">User-defined object</param>
|
||||
public delegate void CTimerEventHandler(object? userObject);
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of Crestron CTimer for testing purposes
|
||||
/// Provides the same public API surface as the real CTimer
|
||||
/// </summary>
|
||||
public class CTimer : IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private Timer? _timer;
|
||||
private readonly object _lockObject = new object();
|
||||
private bool _disposed = false;
|
||||
private readonly CTimerEventHandler? _callback;
|
||||
private readonly object? _userObject;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>Gets whether the timer is currently running</summary>
|
||||
public bool Running { get; private set; } = false;
|
||||
|
||||
/// <summary>Gets the timer interval in milliseconds</summary>
|
||||
public long TimeToFire { get; private set; } = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>Initializes a new instance of the CTimer class</summary>
|
||||
/// <param name="callbackFunction">Function to call when timer fires</param>
|
||||
/// <param name="userObject">User-defined object to pass to callback</param>
|
||||
/// <param name="dueTime">Time before timer first fires (milliseconds)</param>
|
||||
/// <param name="period">Interval between timer fires (milliseconds), or -1 for one-shot</param>
|
||||
public CTimer(CTimerEventHandler callbackFunction, object? userObject, long dueTime, long period)
|
||||
{
|
||||
_callback = callbackFunction;
|
||||
_userObject = userObject;
|
||||
TimeToFire = period;
|
||||
|
||||
var dueTimeInt = dueTime > int.MaxValue ? Timeout.Infinite : (int)dueTime;
|
||||
var periodInt = period > int.MaxValue || period < 0 ? Timeout.Infinite : (int)period;
|
||||
|
||||
_timer = new Timer(TimerCallback, null, dueTimeInt, periodInt);
|
||||
Running = dueTime != Timeout.Infinite;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the CTimer class</summary>
|
||||
/// <param name="callbackFunction">Function to call when timer fires</param>
|
||||
/// <param name="userObject">User-defined object to pass to callback</param>
|
||||
/// <param name="dueTime">Time before timer first fires (milliseconds)</param>
|
||||
public CTimer(CTimerEventHandler callbackFunction, object? userObject, long dueTime)
|
||||
: this(callbackFunction, userObject, dueTime, -1)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the CTimer class</summary>
|
||||
/// <param name="callbackFunction">Function to call when timer fires</param>
|
||||
/// <param name="userObject">User-defined object to pass to callback</param>
|
||||
public CTimer(CTimerEventHandler callbackFunction, object? userObject)
|
||||
: this(callbackFunction, userObject, Timeout.Infinite, -1)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the CTimer class</summary>
|
||||
/// <param name="callbackFunction">Function to call when timer fires</param>
|
||||
public CTimer(CTimerEventHandler callbackFunction)
|
||||
: this(callbackFunction, null, Timeout.Infinite, -1)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>Resets the timer with a new due time</summary>
|
||||
/// <param name="dueTime">New due time in milliseconds</param>
|
||||
/// <returns>True if successful</returns>
|
||||
public bool Reset(long dueTime)
|
||||
{
|
||||
return Reset(dueTime, -1);
|
||||
}
|
||||
|
||||
/// <summary>Resets the timer with new due time and period</summary>
|
||||
/// <param name="dueTime">New due time in milliseconds</param>
|
||||
/// <param name="period">New period in milliseconds, or -1 for one-shot</param>
|
||||
/// <returns>True if successful</returns>
|
||||
public bool Reset(long dueTime, long period)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_disposed || _timer == null) return false;
|
||||
|
||||
TimeToFire = period;
|
||||
var dueTimeInt = dueTime > int.MaxValue ? Timeout.Infinite : (int)dueTime;
|
||||
var periodInt = period > int.MaxValue || period < 0 ? Timeout.Infinite : (int)period;
|
||||
|
||||
try
|
||||
{
|
||||
_timer.Change(dueTimeInt, periodInt);
|
||||
Running = dueTime != Timeout.Infinite;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stops the timer</summary>
|
||||
/// <returns>True if successful</returns>
|
||||
public bool Stop()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_disposed || _timer == null) return false;
|
||||
|
||||
try
|
||||
{
|
||||
_timer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
Running = false;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>Internal timer callback</summary>
|
||||
/// <param name="state">Timer state (unused)</param>
|
||||
private void TimerCallback(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
_callback?.Invoke(_userObject);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Suppress exceptions in callback to prevent timer from stopping
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
/// <summary>Disposes the timer and releases resources</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Protected dispose method</summary>
|
||||
/// <param name="disposing">True if disposing managed resources</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
Running = false;
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
119
src/CrestronMock/Console.cs
Normal file
119
src/CrestronMock/Console.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
public static class ErrorLog
|
||||
{
|
||||
public static void Error(string message, params object[] args)
|
||||
{
|
||||
Console.WriteLine($"[ERROR] {string.Format(message, args)}");
|
||||
}
|
||||
|
||||
public static void Notice(string message, params object[] args)
|
||||
{
|
||||
Console.WriteLine($"[NOTICE] {string.Format(message, args)}");
|
||||
}
|
||||
|
||||
public static void Warn(string message, params object[] args)
|
||||
{
|
||||
Console.WriteLine($"[WARN] {string.Format(message, args)}");
|
||||
}
|
||||
|
||||
public static void Info(string message, params object[] args)
|
||||
{
|
||||
Console.WriteLine($"[INFO] {string.Format(message, args)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronDataStore
|
||||
{
|
||||
public static class CrestronDataStoreStatic
|
||||
{
|
||||
public static CDS_ERROR SetLocalStringValue(string key, string value)
|
||||
{
|
||||
return CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
|
||||
public static CDS_ERROR GetLocalStringValue(string key, out string value)
|
||||
{
|
||||
value = "";
|
||||
return CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
|
||||
/// <summary>Initialize the Crestron data store</summary>
|
||||
/// <returns>0 on success, negative on error</returns>
|
||||
public static int InitCrestronDataStore()
|
||||
{
|
||||
// Mock implementation
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Get a boolean value from local storage</summary>
|
||||
/// <param name="key">The key to retrieve</param>
|
||||
/// <param name="value">The retrieved value</param>
|
||||
/// <returns>0 on success, negative on error</returns>
|
||||
public static int GetLocalBoolValue(string key, out bool value)
|
||||
{
|
||||
// Mock implementation - always return false for now
|
||||
value = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Set a boolean value in local storage</summary>
|
||||
/// <param name="key">The key to set</param>
|
||||
/// <param name="value">The value to set</param>
|
||||
/// <returns>0 on success, negative on error</returns>
|
||||
public static int SetLocalBoolValue(string key, bool value)
|
||||
{
|
||||
// Mock implementation
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Get an integer value from local storage</summary>
|
||||
/// <param name="key">The key to retrieve</param>
|
||||
/// <param name="value">The retrieved value</param>
|
||||
/// <returns>0 on success, negative on error</returns>
|
||||
public static int GetLocalIntValue(string key, out int value)
|
||||
{
|
||||
// Mock implementation - always return 0 for now
|
||||
value = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Set an integer value in local storage</summary>
|
||||
/// <param name="key">The key to set</param>
|
||||
/// <param name="value">The value to set</param>
|
||||
/// <returns>0 on success, negative on error</returns>
|
||||
public static int SetLocalIntValue(string key, int value)
|
||||
{
|
||||
// Mock implementation
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Set an unsigned integer value in local storage</summary>
|
||||
/// <param name="key">The key to set</param>
|
||||
/// <param name="value">The value to set</param>
|
||||
/// <returns>0 on success, negative on error</returns>
|
||||
public static int SetLocalUintValue(string key, uint value)
|
||||
{
|
||||
// Mock implementation
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CDS_ERROR
|
||||
{
|
||||
CDS_SUCCESS = 0,
|
||||
CDS_ERROR = -1
|
||||
}
|
||||
|
||||
/// <summary>Mock CrestronDataStore for local data storage</summary>
|
||||
public static class CrestronDataStore
|
||||
{
|
||||
/// <summary>Error constant for CDS operations</summary>
|
||||
public static readonly int CDS_ERROR = -1;
|
||||
}
|
||||
}
|
||||
27
src/CrestronMock/ConsoleTypes.cs
Normal file
27
src/CrestronMock/ConsoleTypes.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
/// <summary>Mock console command response utility</summary>
|
||||
public static class ConsoleCommandResponseUtility
|
||||
{
|
||||
/// <summary>Send console command response with response code</summary>
|
||||
/// <param name="response">The response text</param>
|
||||
/// <param name="responseCode">The response code</param>
|
||||
public static void ConsoleCommandResponse(string response, int responseCode = 0)
|
||||
{
|
||||
// Mock implementation - just log to console or ignore
|
||||
Console.WriteLine($"Console Response ({responseCode}): {response}");
|
||||
}
|
||||
|
||||
/// <summary>Send console command response with additional parameter</summary>
|
||||
/// <param name="response">The response text</param>
|
||||
/// <param name="param1">First parameter</param>
|
||||
/// <param name="param2">Second parameter</param>
|
||||
public static void ConsoleCommandResponse(string response, object param1, object param2)
|
||||
{
|
||||
// Mock implementation
|
||||
Console.WriteLine($"Console Response: {response} - {param1}, {param2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
266
src/CrestronMock/CrestronControlSystem.cs
Normal file
266
src/CrestronMock/CrestronControlSystem.cs
Normal file
@@ -0,0 +1,266 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CrestronMock;
|
||||
|
||||
/// <summary>Mock Crestron signal base class</summary>
|
||||
public class Sig { }
|
||||
|
||||
/// <summary>Mock UShort input signal</summary>
|
||||
public class UShortInputSig { }
|
||||
|
||||
/// <summary>Mock UShort output signal</summary>
|
||||
public class UShortOutputSig { }
|
||||
|
||||
/// <summary>Mock Boolean input signal</summary>
|
||||
public class BoolInputSig { }
|
||||
|
||||
/// <summary>Mock String input signal</summary>
|
||||
public class StringInputSig { }
|
||||
|
||||
/// <summary>Mock Boolean output signal</summary>
|
||||
public class BoolOutputSig { }
|
||||
|
||||
/// <summary>Mock String output signal</summary>
|
||||
public class StringOutputSig { }
|
||||
|
||||
/// <summary>Mock signal group</summary>
|
||||
public class SigGroup { }
|
||||
|
||||
/// <summary>Mock COM port</summary>
|
||||
public class ComPort { }
|
||||
|
||||
/// <summary>Mock relay</summary>
|
||||
public class Relay { }
|
||||
|
||||
/// <summary>Mock IR output port</summary>
|
||||
public class IROutputPort { }
|
||||
|
||||
/// <summary>Mock IO port</summary>
|
||||
public class IOPort { }
|
||||
|
||||
/// <summary>Mock VersiPort</summary>
|
||||
public class VersiPort { }
|
||||
|
||||
/// <summary>Mock IR input port</summary>
|
||||
public class IRInputPort { }
|
||||
|
||||
/// <summary>Signal type enumeration</summary>
|
||||
public enum eSigType
|
||||
{
|
||||
Bool,
|
||||
UShort,
|
||||
String
|
||||
}
|
||||
|
||||
/// <summary>Mock read-only collection</summary>
|
||||
public class ReadOnlyCollection<TKey, TValue> : Dictionary<TKey, TValue> where TKey : notnull
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Mock COM ports interface</summary>
|
||||
public interface IComPorts
|
||||
{
|
||||
ComPort[] ComPorts { get; }
|
||||
}
|
||||
|
||||
/// <summary>Mock relay ports interface</summary>
|
||||
public interface IRelayPorts
|
||||
{
|
||||
Relay[] RelayPorts { get; }
|
||||
}
|
||||
|
||||
/// <summary>Mock IR output ports interface</summary>
|
||||
public interface IIROutputPorts
|
||||
{
|
||||
IROutputPort[] IROutputPorts { get; }
|
||||
}
|
||||
|
||||
/// <summary>Mock IO ports interface</summary>
|
||||
public interface IIOPorts
|
||||
{
|
||||
IOPort[] IOPorts { get; }
|
||||
}
|
||||
|
||||
/// <summary>Mock digital input ports interface</summary>
|
||||
public interface IDigitalInputPorts
|
||||
{
|
||||
VersiPort[] DigitalInputPorts { get; }
|
||||
}
|
||||
|
||||
/// <summary>Mock IR input port interface</summary>
|
||||
public interface IIRInputPort
|
||||
{
|
||||
IRInputPort IRInputPort { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of CrestronControlSystem for testing purposes
|
||||
/// Base class for the CrestronControlSystem The Customer application is derived over this class
|
||||
/// </summary>
|
||||
public class CrestronControlSystem : IComPorts, IRelayPorts, IIROutputPorts, IIOPorts, IDigitalInputPorts, IIRInputPort
|
||||
{
|
||||
// Static fields
|
||||
public static Sig NullCue { get; set; } = new Sig();
|
||||
public static UShortInputSig NullUShortInputSig { get; set; } = new UShortInputSig();
|
||||
public static UShortOutputSig NullUShortOutputSig { get; set; } = new UShortOutputSig();
|
||||
public static BoolInputSig NullBoolInputSig { get; set; } = new BoolInputSig();
|
||||
public static StringInputSig NullStringInputSig { get; set; } = new StringInputSig();
|
||||
public static BoolOutputSig NullBoolOutputSig { get; set; } = new BoolOutputSig();
|
||||
public static StringOutputSig NullStringOutputSig { get; set; } = new StringOutputSig();
|
||||
public static ReadOnlyCollection<int, SigGroup> SigGroups { get; set; } = new ReadOnlyCollection<int, SigGroup>();
|
||||
public static int MaxNumberOfEventsInQueue { get; set; } = 1000;
|
||||
|
||||
// Constructor
|
||||
public CrestronControlSystem()
|
||||
{
|
||||
// Initialize collections and properties
|
||||
ComPorts = Array.Empty<ComPort>();
|
||||
RelayPorts = Array.Empty<Relay>();
|
||||
IROutputPorts = Array.Empty<IROutputPort>();
|
||||
IOPorts = Array.Empty<IOPort>();
|
||||
DigitalInputPorts = Array.Empty<VersiPort>();
|
||||
IRInputPort = new IRInputPort();
|
||||
}
|
||||
|
||||
// Virtual methods that can be overridden
|
||||
public virtual void InitializeSystem()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void SavePreset()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void RecallPreset()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void BassFlat()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void TrebleFlat()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void LimiterEnable()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void LimiterDisable()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void LimiterSoftKneeOn()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void LimiterSoftKneeOff()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void MasterMuteOn()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void MasterMuteOff()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void MicMasterMuteOn()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void MicMasterMuteOff()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void SourceMuteOn()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
public virtual void SourceMuteOff()
|
||||
{
|
||||
// Override in derived classes
|
||||
}
|
||||
|
||||
// Non-virtual methods
|
||||
public void MicMuteOn(uint MicNumber)
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
public void MicMuteOff(uint MicNumber)
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
public void MonoOutput()
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
public void StereoOutput()
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Static methods for SigGroup management
|
||||
public static SigGroup CreateSigGroup(int groupID, params BoolInputSig[] boolInputSigs)
|
||||
{
|
||||
return new SigGroup();
|
||||
}
|
||||
|
||||
public static SigGroup CreateSigGroup(int groupID, params UShortInputSig[] ushortInputSigs)
|
||||
{
|
||||
return new SigGroup();
|
||||
}
|
||||
|
||||
public static SigGroup CreateSigGroup(int groupID, eSigType type)
|
||||
{
|
||||
return new SigGroup();
|
||||
}
|
||||
|
||||
public static SigGroup CreateSigGroup(int groupID, params StringInputSig[] stringInputSigs)
|
||||
{
|
||||
return new SigGroup();
|
||||
}
|
||||
|
||||
public static void RemoveSigGroup(int groupID)
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
public static void RemoveSigGroup(SigGroup sigGroupToRemove)
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
public static void ClearSigGroups()
|
||||
{
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Interface implementations
|
||||
public ComPort[] ComPorts { get; set; }
|
||||
public Relay[] RelayPorts { get; set; }
|
||||
public IROutputPort[] IROutputPorts { get; set; }
|
||||
public IOPort[] IOPorts { get; set; }
|
||||
public VersiPort[] DigitalInputPorts { get; set; }
|
||||
public IRInputPort IRInputPort { get; set; }
|
||||
}
|
||||
277
src/CrestronMock/CrestronEnvironment.cs
Normal file
277
src/CrestronMock/CrestronEnvironment.cs
Normal file
@@ -0,0 +1,277 @@
|
||||
using System;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
// Console and logging types needed by CrestronConsole and CrestronLogger
|
||||
public delegate string ConsoleCommandFunction(string parameters);
|
||||
|
||||
public enum ConsoleAccessLevelEnum
|
||||
{
|
||||
AccessOperator = 0,
|
||||
AccessProgrammer = 1,
|
||||
AccessAdministrator = 2
|
||||
}
|
||||
|
||||
public class ConsoleCommandParameterSpecClass
|
||||
{
|
||||
// Mock implementation
|
||||
}
|
||||
|
||||
/// <summary>Ethernet event handler delegate</summary>
|
||||
public delegate void EthernetEventHandler(EthernetEventArgs args);
|
||||
|
||||
/// <summary>Mock CrestronEnvironment for system event handling</summary>
|
||||
public static class CrestronEnvironment
|
||||
{
|
||||
/// <summary>Event fired when program status changes</summary>
|
||||
public static event ProgramStatusEventHandler? ProgramStatusEventHandler;
|
||||
|
||||
/// <summary>Event fired when ethernet status changes</summary>
|
||||
public static event EthernetEventHandler? EthernetEventHandler;
|
||||
|
||||
/// <summary>Gets the device platform</summary>
|
||||
public static eDevicePlatform DevicePlatform => eDevicePlatform.Appliance;
|
||||
|
||||
/// <summary>Gets the runtime environment</summary>
|
||||
public static eRuntimeEnvironment RuntimeEnvironment => eRuntimeEnvironment.SimplSharpPro;
|
||||
|
||||
/// <summary>Gets system information</summary>
|
||||
public static string SystemInfo => "Mock System v1.0";
|
||||
|
||||
/// <summary>Gets OS version</summary>
|
||||
public static string OSVersion => "Mock OS 1.0.0";
|
||||
|
||||
/// <summary>Gets new line character sequence</summary>
|
||||
public static string NewLine => Environment.NewLine;
|
||||
|
||||
/// <summary>Gets program compatibility level</summary>
|
||||
public static eProgramCompatibility ProgramCompatibility => eProgramCompatibility.Series3And4;
|
||||
|
||||
/// <summary>Sleep for specified milliseconds</summary>
|
||||
/// <param name="milliseconds">Sleep duration</param>
|
||||
public static void Sleep(int milliseconds)
|
||||
{
|
||||
System.Threading.Thread.Sleep(milliseconds);
|
||||
}
|
||||
|
||||
/// <summary>Gets the time zone</summary>
|
||||
/// <returns>Time zone string</returns>
|
||||
public static string GetTimeZone()
|
||||
{
|
||||
return TimeZoneInfo.Local.Id;
|
||||
}
|
||||
|
||||
/// <summary>Triggers a program status event (for testing)</summary>
|
||||
/// <param name="eventType">Event type</param>
|
||||
public static void TriggerProgramStatusEvent(eProgramStatusEventType eventType)
|
||||
{
|
||||
ProgramStatusEventHandler?.Invoke(eventType);
|
||||
}
|
||||
|
||||
/// <summary>Triggers an ethernet event (for testing)</summary>
|
||||
/// <param name="args">Event arguments</param>
|
||||
public static void TriggerEthernetEvent(EthernetEventArgs args)
|
||||
{
|
||||
EthernetEventHandler?.Invoke(args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock ethernet event type enumeration</summary>
|
||||
public enum eEthernetEventType
|
||||
{
|
||||
/// <summary>Link down</summary>
|
||||
LinkDown = 0,
|
||||
/// <summary>Link up</summary>
|
||||
LinkUp = 1
|
||||
}
|
||||
|
||||
/// <summary>Mock CrestronConsole for console output</summary>
|
||||
public static class CrestronConsole
|
||||
{
|
||||
/// <summary>Prints a line to the console</summary>
|
||||
/// <param name="message">Message to print</param>
|
||||
public static void PrintLine(string message)
|
||||
{
|
||||
// Mock implementation - could write to System.Console in test environment
|
||||
Console.WriteLine($"[CrestronConsole] {message}");
|
||||
}
|
||||
|
||||
/// <summary>Prints formatted text to the console</summary>
|
||||
/// <param name="format">Format string</param>
|
||||
/// <param name="args">Arguments</param>
|
||||
public static void PrintLine(string format, params object[] args)
|
||||
{
|
||||
Console.WriteLine($"[CrestronConsole] {string.Format(format, args)}");
|
||||
}
|
||||
|
||||
/// <summary>Prints text to the console without a newline</summary>
|
||||
/// <param name="message">Message to print</param>
|
||||
public static void Print(string message)
|
||||
{
|
||||
Console.Write($"[CrestronConsole] {message}");
|
||||
}
|
||||
|
||||
/// <summary>Console command response</summary>
|
||||
/// <param name="command">Command to execute</param>
|
||||
/// <returns>Response string</returns>
|
||||
public static string ConsoleCommandResponse(string command)
|
||||
{
|
||||
return $"Mock response for command: {command}";
|
||||
}
|
||||
|
||||
/// <summary>Add new console command</summary>
|
||||
/// <param name="function">Command function</param>
|
||||
/// <param name="command">Command name</param>
|
||||
/// <param name="help">Help text</param>
|
||||
/// <param name="accessLevel">Access level</param>
|
||||
/// <returns>0 for success</returns>
|
||||
public static int AddNewConsoleCommand(ConsoleCommandFunction function, string command, string help, ConsoleAccessLevelEnum accessLevel)
|
||||
{
|
||||
return 0; // Mock success
|
||||
}
|
||||
|
||||
/// <summary>Add new console command with parameter spec</summary>
|
||||
/// <param name="function">Command function</param>
|
||||
/// <param name="command">Command name</param>
|
||||
/// <param name="help">Help text</param>
|
||||
/// <param name="accessLevel">Access level</param>
|
||||
/// <param name="spec">Parameter specification</param>
|
||||
/// <returns>0 for success</returns>
|
||||
public static int AddNewConsoleCommand(ConsoleCommandFunction function, string command, string help, ConsoleAccessLevelEnum accessLevel, ConsoleCommandParameterSpecClass spec)
|
||||
{
|
||||
return 0; // Mock success
|
||||
}
|
||||
|
||||
/// <summary>Send control system command</summary>
|
||||
/// <param name="command">Command to send</param>
|
||||
/// <param name="programNumber">Program number</param>
|
||||
public static void SendControlSystemCommand(string command, uint programNumber)
|
||||
{
|
||||
// Mock implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronIO
|
||||
{
|
||||
/// <summary>Mock File class for basic file operations</summary>
|
||||
public static class File
|
||||
{
|
||||
/// <summary>Checks if a file exists</summary>
|
||||
/// <param name="path">File path</param>
|
||||
/// <returns>True if file exists</returns>
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
// Mock implementation - use System.IO.File for actual file operations
|
||||
return System.IO.File.Exists(path);
|
||||
}
|
||||
|
||||
/// <summary>Reads all text from a file</summary>
|
||||
/// <param name="path">File path</param>
|
||||
/// <returns>File contents</returns>
|
||||
public static string ReadToEnd(string path)
|
||||
{
|
||||
return System.IO.File.ReadAllText(path);
|
||||
}
|
||||
|
||||
/// <summary>Reads all text from a file with specified encoding</summary>
|
||||
/// <param name="path">File path</param>
|
||||
/// <param name="encoding">Text encoding</param>
|
||||
/// <returns>File contents</returns>
|
||||
public static string ReadToEnd(string path, System.Text.Encoding encoding)
|
||||
{
|
||||
return System.IO.File.ReadAllText(path, encoding);
|
||||
}
|
||||
|
||||
/// <summary>Writes text to a file</summary>
|
||||
/// <param name="path">File path</param>
|
||||
/// <param name="contents">Contents to write</param>
|
||||
public static void WriteAllText(string path, string contents)
|
||||
{
|
||||
System.IO.File.WriteAllText(path, contents);
|
||||
}
|
||||
|
||||
/// <summary>Deletes a file</summary>
|
||||
/// <param name="path">File path</param>
|
||||
public static void Delete(string path)
|
||||
{
|
||||
if (System.IO.File.Exists(path))
|
||||
System.IO.File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock Directory class for basic directory operations</summary>
|
||||
public static class Directory
|
||||
{
|
||||
/// <summary>Gets the application directory path</summary>
|
||||
/// <returns>Application directory path</returns>
|
||||
public static string GetApplicationDirectory()
|
||||
{
|
||||
// Mock implementation - return current directory or a typical Crestron path
|
||||
return System.IO.Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
/// <summary>Gets the application root directory path</summary>
|
||||
/// <returns>Application root directory path</returns>
|
||||
public static string GetApplicationRootDirectory()
|
||||
{
|
||||
// Mock implementation - return current directory or a typical Crestron path
|
||||
return System.IO.Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
/// <summary>Checks if a directory exists</summary>
|
||||
/// <param name="path">Directory path</param>
|
||||
/// <returns>True if directory exists</returns>
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
return System.IO.Directory.Exists(path);
|
||||
}
|
||||
|
||||
/// <summary>Creates a directory</summary>
|
||||
/// <param name="path">Directory path</param>
|
||||
public static void CreateDirectory(string path)
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock Path class for path operations</summary>
|
||||
public static class Path
|
||||
{
|
||||
/// <summary>Directory separator character</summary>
|
||||
public static readonly char DirectorySeparatorChar = System.IO.Path.DirectorySeparatorChar;
|
||||
|
||||
/// <summary>Combines path strings</summary>
|
||||
/// <param name="path1">First path</param>
|
||||
/// <param name="path2">Second path</param>
|
||||
/// <returns>Combined path</returns>
|
||||
public static string Combine(string path1, string path2)
|
||||
{
|
||||
return System.IO.Path.Combine(path1, path2);
|
||||
}
|
||||
|
||||
/// <summary>Gets the file name from a path</summary>
|
||||
/// <param name="path">Full path</param>
|
||||
/// <returns>File name</returns>
|
||||
public static string GetFileName(string path)
|
||||
{
|
||||
return System.IO.Path.GetFileName(path);
|
||||
}
|
||||
|
||||
/// <summary>Gets the directory name from a path</summary>
|
||||
/// <param name="path">Full path</param>
|
||||
/// <returns>Directory name</returns>
|
||||
public static string GetDirectoryName(string path)
|
||||
{
|
||||
return System.IO.Path.GetDirectoryName(path) ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Gets the file extension from a path</summary>
|
||||
/// <param name="path">Full path</param>
|
||||
/// <returns>File extension</returns>
|
||||
public static string GetExtension(string path)
|
||||
{
|
||||
return System.IO.Path.GetExtension(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/CrestronMock/CrestronLogger.cs
Normal file
61
src/CrestronMock/CrestronLogger.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronLogger
|
||||
{
|
||||
/// <summary>Mock CrestronLogger for .NET 8 compatibility</summary>
|
||||
public static class CrestronLogger
|
||||
{
|
||||
/// <summary>Write to log</summary>
|
||||
/// <param name="logName">Log name</param>
|
||||
/// <param name="message">Message</param>
|
||||
/// <param name="mode">Logger mode</param>
|
||||
public static void WriteToLog(string logName, string message, LoggerModeEnum mode)
|
||||
{
|
||||
Console.WriteLine($"[{logName}] {message}");
|
||||
}
|
||||
|
||||
/// <summary>Write to log with level</summary>
|
||||
/// <param name="message">Message</param>
|
||||
/// <param name="level">Log level</param>
|
||||
public static void WriteToLog(string message, uint level)
|
||||
{
|
||||
Console.WriteLine($"[Level {level}] {message}");
|
||||
}
|
||||
|
||||
/// <summary>Initialize logger</summary>
|
||||
/// <param name="bufferSize">Buffer size</param>
|
||||
/// <param name="mode">Logger mode</param>
|
||||
public static void Initialize(int bufferSize, LoggerModeEnum mode)
|
||||
{
|
||||
// Mock implementation
|
||||
}
|
||||
|
||||
/// <summary>Print the log</summary>
|
||||
/// <param name="includeAll">Include all log entries</param>
|
||||
/// <returns>Log entries as string list</returns>
|
||||
public static List<string> PrintTheLog(bool includeAll = false)
|
||||
{
|
||||
return new List<string> { "Mock log entry" };
|
||||
}
|
||||
|
||||
/// <summary>Clear the log</summary>
|
||||
/// <param name="clearAll">Clear all entries</param>
|
||||
/// <returns>Success message</returns>
|
||||
public static string Clear(bool clearAll)
|
||||
{
|
||||
return "Log cleared (mock)";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Logger mode enumeration</summary>
|
||||
public enum LoggerModeEnum
|
||||
{
|
||||
/// <summary>Append mode</summary>
|
||||
LoggingModeAppend = 0,
|
||||
/// <summary>Overwrite mode</summary>
|
||||
LoggingModeOverwrite = 1,
|
||||
/// <summary>RM mode</summary>
|
||||
RM = 2
|
||||
}
|
||||
}
|
||||
10
src/CrestronMock/CrestronMock.csproj
Normal file
10
src/CrestronMock/CrestronMock.csproj
Normal file
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Configurations>Debug;Release;Test</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
245
src/CrestronMock/CrestronQueue.cs
Normal file
245
src/CrestronMock/CrestronQueue.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
/// <summary>
|
||||
/// Mock implementation of Crestron CrestronQueue for testing purposes
|
||||
/// Provides the same public API surface as the real CrestronQueue
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of items in the queue</typeparam>
|
||||
public class CrestronQueue<T> : IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly Queue<T> _queue = new Queue<T>();
|
||||
private readonly object _lockObject = new object();
|
||||
private readonly ManualResetEventSlim _dataAvailableEvent = new ManualResetEventSlim(false);
|
||||
private bool _disposed = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>Gets the number of items in the queue</summary>
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _queue.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets whether the queue is empty</summary>
|
||||
public bool IsEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _queue.Count == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>Initializes a new instance of the CrestronQueue class</summary>
|
||||
public CrestronQueue()
|
||||
{
|
||||
// Mock implementation
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the CrestronQueue class with specified capacity</summary>
|
||||
/// <param name="capacity">The initial capacity of the queue</param>
|
||||
public CrestronQueue(int capacity)
|
||||
{
|
||||
// Mock implementation - capacity is ignored in this mock
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>Adds an item to the end of the queue</summary>
|
||||
/// <param name="item">Item to add</param>
|
||||
public void Enqueue(T item)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(CrestronQueue<T>));
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_queue.Enqueue(item);
|
||||
_dataAvailableEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the item at the beginning of the queue</summary>
|
||||
/// <returns>The item that was removed from the queue</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the queue is empty</exception>
|
||||
public T Dequeue()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(CrestronQueue<T>));
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_queue.Count == 0)
|
||||
throw new InvalidOperationException("Queue is empty");
|
||||
|
||||
var item = _queue.Dequeue();
|
||||
|
||||
if (_queue.Count == 0)
|
||||
_dataAvailableEvent.Reset();
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tries to remove and return the item at the beginning of the queue</summary>
|
||||
/// <param name="item">When successful, contains the dequeued item</param>
|
||||
/// <returns>True if an item was successfully dequeued</returns>
|
||||
public bool TryDequeue(out T item)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
item = default(T)!;
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_queue.Count == 0)
|
||||
{
|
||||
item = default(T)!;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = _queue.Dequeue();
|
||||
|
||||
if (_queue.Count == 0)
|
||||
_dataAvailableEvent.Reset();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the item at the beginning of the queue without removing it</summary>
|
||||
/// <returns>The item at the beginning of the queue</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the queue is empty</exception>
|
||||
public T Peek()
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(CrestronQueue<T>));
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_queue.Count == 0)
|
||||
throw new InvalidOperationException("Queue is empty");
|
||||
|
||||
return _queue.Peek();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tries to return the item at the beginning of the queue without removing it</summary>
|
||||
/// <param name="item">When successful, contains the item at the beginning of the queue</param>
|
||||
/// <returns>True if an item was found</returns>
|
||||
public bool TryPeek(out T item)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
item = default(T)!;
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_queue.Count == 0)
|
||||
{
|
||||
item = default(T)!;
|
||||
return false;
|
||||
}
|
||||
|
||||
item = _queue.Peek();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes all items from the queue</summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
_queue.Clear();
|
||||
_dataAvailableEvent.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Waits for data to become available in the queue</summary>
|
||||
/// <param name="timeout">Timeout in milliseconds</param>
|
||||
/// <returns>True if data became available within the timeout</returns>
|
||||
public bool WaitForData(int timeout)
|
||||
{
|
||||
if (_disposed) return false;
|
||||
|
||||
return _dataAvailableEvent.Wait(timeout);
|
||||
}
|
||||
|
||||
/// <summary>Waits for data to become available in the queue</summary>
|
||||
/// <returns>True when data becomes available</returns>
|
||||
public bool WaitForData()
|
||||
{
|
||||
if (_disposed) return false;
|
||||
|
||||
_dataAvailableEvent.Wait();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Copies the queue elements to an array</summary>
|
||||
/// <returns>Array containing the queue elements</returns>
|
||||
public T[] ToArray()
|
||||
{
|
||||
if (_disposed) return new T[0];
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _queue.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
/// <summary>Disposes the queue and releases resources</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Protected dispose method</summary>
|
||||
/// <param name="disposing">True if disposing managed resources</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_dataAvailableEvent?.Dispose();
|
||||
Clear();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
131
src/CrestronMock/EventTypes.cs
Normal file
131
src/CrestronMock/EventTypes.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
/// <summary>Mock eProgramStatusEventType enumeration</summary>
|
||||
public enum eProgramStatusEventType
|
||||
{
|
||||
/// <summary>Program stopping</summary>
|
||||
Stopping = 0,
|
||||
/// <summary>Program started</summary>
|
||||
Starting = 1,
|
||||
/// <summary>Program running</summary>
|
||||
Running = 2,
|
||||
/// <summary>Program paused</summary>
|
||||
Paused = 3
|
||||
}
|
||||
|
||||
/// <summary>Mock EthernetEventArgs class</summary>
|
||||
public class EthernetEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Gets the Ethernet adapter that triggered the event</summary>
|
||||
public int EthernetAdapter { get; private set; }
|
||||
|
||||
/// <summary>Gets the link status</summary>
|
||||
public bool LinkUp { get; private set; }
|
||||
|
||||
/// <summary>Gets the speed in Mbps</summary>
|
||||
public int Speed { get; private set; }
|
||||
|
||||
/// <summary>Gets whether it's full duplex</summary>
|
||||
public bool FullDuplex { get; private set; }
|
||||
|
||||
/// <summary>Gets the ethernet event type</summary>
|
||||
public eEthernetEventType EthernetEventType { get; private set; }
|
||||
|
||||
/// <summary>Initializes a new instance of EthernetEventArgs</summary>
|
||||
/// <param name="adapter">Ethernet adapter number</param>
|
||||
/// <param name="linkUp">Link status</param>
|
||||
/// <param name="speed">Speed in Mbps</param>
|
||||
/// <param name="fullDuplex">Full duplex status</param>
|
||||
public EthernetEventArgs(int adapter, bool linkUp, int speed, bool fullDuplex)
|
||||
{
|
||||
EthernetAdapter = adapter;
|
||||
LinkUp = linkUp;
|
||||
Speed = speed;
|
||||
FullDuplex = fullDuplex;
|
||||
EthernetEventType = linkUp ? eEthernetEventType.LinkUp : eEthernetEventType.LinkDown;
|
||||
}
|
||||
|
||||
/// <summary>Default constructor</summary>
|
||||
public EthernetEventArgs() : this(0, false, 0, false)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronIO
|
||||
{
|
||||
/// <summary>Mock FileInfo class for basic file operations</summary>
|
||||
public class FileInfo
|
||||
{
|
||||
/// <summary>Gets the full path of the file</summary>
|
||||
public string FullName { get; private set; }
|
||||
|
||||
/// <summary>Gets the name of the file</summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>Gets the directory name</summary>
|
||||
public string? DirectoryName { get; private set; }
|
||||
|
||||
/// <summary>Gets whether the file exists</summary>
|
||||
public bool Exists { get; private set; }
|
||||
|
||||
/// <summary>Gets the length of the file in bytes</summary>
|
||||
public long Length { get; private set; }
|
||||
|
||||
/// <summary>Gets the creation time</summary>
|
||||
public DateTime CreationTime { get; private set; }
|
||||
|
||||
/// <summary>Gets the last write time</summary>
|
||||
public DateTime LastWriteTime { get; private set; }
|
||||
|
||||
/// <summary>Gets the last access time</summary>
|
||||
public DateTime LastAccessTime { get; private set; }
|
||||
|
||||
/// <summary>Initializes a new instance of FileInfo</summary>
|
||||
/// <param name="fileName">Path to the file</param>
|
||||
public FileInfo(string fileName)
|
||||
{
|
||||
FullName = fileName ?? string.Empty;
|
||||
Name = System.IO.Path.GetFileName(fileName) ?? string.Empty;
|
||||
DirectoryName = System.IO.Path.GetDirectoryName(fileName);
|
||||
|
||||
// Mock file properties
|
||||
Exists = !string.IsNullOrEmpty(fileName);
|
||||
Length = 0;
|
||||
CreationTime = DateTime.Now;
|
||||
LastWriteTime = DateTime.Now;
|
||||
LastAccessTime = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <summary>Deletes the file</summary>
|
||||
public void Delete()
|
||||
{
|
||||
// Mock implementation - just mark as not existing
|
||||
Exists = false;
|
||||
}
|
||||
|
||||
/// <summary>Creates a text file or opens an existing one for writing</summary>
|
||||
/// <returns>A mock StreamWriter</returns>
|
||||
public System.IO.StreamWriter CreateText()
|
||||
{
|
||||
var stream = new System.IO.MemoryStream();
|
||||
return new System.IO.StreamWriter(stream);
|
||||
}
|
||||
|
||||
/// <summary>Opens an existing file for reading</summary>
|
||||
/// <returns>A mock FileStream</returns>
|
||||
public System.IO.FileStream OpenRead()
|
||||
{
|
||||
// Mock implementation - return a memory stream wrapped as FileStream
|
||||
return new System.IO.FileStream(FullName, System.IO.FileMode.Open, System.IO.FileAccess.Read);
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler delegates
|
||||
/// <summary>Ethernet event handler delegate</summary>
|
||||
public delegate void EthernetEventHandler(EthernetEventArgs args);
|
||||
}
|
||||
|
||||
|
||||
112
src/CrestronMock/Extensions.cs
Normal file
112
src/CrestronMock/Extensions.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threa case ETHERNET_PARAMETER_TO_GET.ETHERNET_HOSTNAME:
|
||||
return "mock-hostname";
|
||||
case ETHERNET_PARAMETER_TO_GET.ETHERNET_MAC_ADDRESS:
|
||||
return "00:11:22:33:44:55";
|
||||
case ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME:
|
||||
return "mock-domain.local";
|
||||
default:
|
||||
return string.Empty; asks;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
public static class CrestronInvoke
|
||||
{
|
||||
public static void BeginInvoke(Func<object> func, object? state = null)
|
||||
{
|
||||
Task.Run(func);
|
||||
}
|
||||
|
||||
public static void BeginInvoke(Action action)
|
||||
{
|
||||
Task.Run(action);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CrestronEthernetHelper
|
||||
{
|
||||
/// <summary>Ethernet parameter enumeration</summary>
|
||||
public enum ETHERNET_PARAMETER_TO_GET
|
||||
{
|
||||
ETHERNET_HOSTNAME = 0,
|
||||
ETHERNET_DOMAIN_NAME = 1,
|
||||
ETHERNET_IP_ADDRESS = 2,
|
||||
ETHERNET_SUBNET_MASK = 3,
|
||||
ETHERNET_GATEWAY = 4,
|
||||
ETHERNET_DNS_SERVER = 5,
|
||||
ETHERNET_MAC_ADDRESS = 6,
|
||||
ETHERNET_DHCP_STATUS = 7,
|
||||
GET_CURRENT_DHCP_STATE = 8,
|
||||
GET_CURRENT_IP_ADDRESS = 9,
|
||||
GET_CURRENT_IP_MASK = 10,
|
||||
GET_CURRENT_ROUTER = 11,
|
||||
GET_HOSTNAME = 12,
|
||||
GET_LINK_STATUS = 13,
|
||||
GET_DOMAIN_NAME = 14
|
||||
}
|
||||
|
||||
public static List<string> GetEthernetAdaptersInfo()
|
||||
{
|
||||
return new List<string> { "MockAdapter" };
|
||||
}
|
||||
|
||||
public static string GetEthernetParameter(string adapter, string parameter)
|
||||
{
|
||||
return "MockValue";
|
||||
}
|
||||
|
||||
/// <summary>Get ethernet parameter as string</summary>
|
||||
/// <param name="parameter">The parameter to get</param>
|
||||
/// <param name="adapterType">The adapter type</param>
|
||||
/// <returns>The parameter value as string</returns>
|
||||
public static string GetEthernetParameter(ETHERNET_PARAMETER_TO_GET parameter, EthernetAdapterType adapterType)
|
||||
{
|
||||
// Mock implementation
|
||||
switch (parameter)
|
||||
{
|
||||
case ETHERNET_PARAMETER_TO_GET.ETHERNET_IP_ADDRESS:
|
||||
return "192.168.1.100";
|
||||
case ETHERNET_PARAMETER_TO_GET.ETHERNET_SUBNET_MASK:
|
||||
return "255.255.255.0";
|
||||
case ETHERNET_PARAMETER_TO_GET.ETHERNET_GATEWAY:
|
||||
return "192.168.1.1";
|
||||
case ETHERNET_PARAMETER_TO_GET.ETHERNET_HOSTNAME:
|
||||
return "MockHost";
|
||||
case ETHERNET_PARAMETER_TO_GET.ETHERNET_MAC_ADDRESS:
|
||||
return "00:11:22:33:44:55";
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get adapter ID for specified adapter type</summary>
|
||||
/// <param name="adapterType">The adapter type</param>
|
||||
/// <returns>The adapter ID</returns>
|
||||
public static int GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType adapterType)
|
||||
{
|
||||
// Mock implementation
|
||||
return (int)adapterType;
|
||||
}
|
||||
|
||||
/// <summary>Check if control subnet is in automatic mode</summary>
|
||||
/// <param name="adapterId">The adapter ID</param>
|
||||
/// <returns>True if in automatic mode</returns>
|
||||
public static bool IsControlSubnetInAutomaticMode(int adapterId)
|
||||
{
|
||||
// Mock implementation
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock EthernetAdapterType enumeration</summary>
|
||||
public enum EthernetAdapterType
|
||||
{
|
||||
/// <summary>Ethernet LAN adapter</summary>
|
||||
EthernetLANAdapter = 0,
|
||||
/// <summary>Control subnet adapter</summary>
|
||||
ControlSubnet = 1,
|
||||
/// <summary>Auto-detect adapter</summary>
|
||||
EthernetAdapterAuto = 2
|
||||
}
|
||||
}
|
||||
407
src/CrestronMock/HttpCwsServer.cs
Normal file
407
src/CrestronMock/HttpCwsServer.cs
Normal file
@@ -0,0 +1,407 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Crestron.SimplSharp.WebScripting
|
||||
{
|
||||
/// <summary>Mock HttpCwsServer class for HTTP web server functionality</summary>
|
||||
public class HttpCwsServer : IDisposable
|
||||
{
|
||||
private HttpListener? _httpListener;
|
||||
private bool _listening;
|
||||
private readonly Dictionary<string, IHttpCwsHandler> _routes = new Dictionary<string, IHttpCwsHandler>();
|
||||
|
||||
/// <summary>Gets or sets the port number</summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>Gets whether the server is listening</summary>
|
||||
public bool Listening => _listening;
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsServer</summary>
|
||||
public HttpCwsServer()
|
||||
{
|
||||
Port = 80;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsServer</summary>
|
||||
/// <param name="port">Port number to listen on</param>
|
||||
public HttpCwsServer(int port)
|
||||
{
|
||||
Port = port;
|
||||
}
|
||||
|
||||
/// <summary>Starts the HTTP server</summary>
|
||||
/// <returns>True if started successfully</returns>
|
||||
public bool Start()
|
||||
{
|
||||
if (_listening)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
_httpListener = new HttpListener();
|
||||
_httpListener.Prefixes.Add($"http://+:{Port}/");
|
||||
_httpListener.Start();
|
||||
_listening = true;
|
||||
|
||||
_ = Task.Run(ProcessRequestsAsync);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stops the HTTP server</summary>
|
||||
/// <returns>True if stopped successfully</returns>
|
||||
public bool Stop()
|
||||
{
|
||||
if (!_listening)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
_listening = false;
|
||||
_httpListener?.Stop();
|
||||
_httpListener?.Close();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Adds a route handler</summary>
|
||||
/// <param name="route">Route path</param>
|
||||
/// <param name="handler">Handler for the route</param>
|
||||
public void AddRoute(string route, IHttpCwsHandler handler)
|
||||
{
|
||||
_routes[route.ToLowerInvariant()] = handler;
|
||||
}
|
||||
|
||||
/// <summary>Removes a route handler</summary>
|
||||
/// <param name="route">Route path to remove</param>
|
||||
public void RemoveRoute(string route)
|
||||
{
|
||||
_routes.Remove(route.ToLowerInvariant());
|
||||
}
|
||||
|
||||
/// <summary>Unregisters a route handler</summary>
|
||||
/// <param name="route">Route path to unregister</param>
|
||||
public void Unregister(string route)
|
||||
{
|
||||
RemoveRoute(route);
|
||||
}
|
||||
|
||||
private async Task ProcessRequestsAsync()
|
||||
{
|
||||
while (_listening && _httpListener != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var context = await _httpListener.GetContextAsync();
|
||||
_ = Task.Run(() => HandleRequest(context));
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
// Listener was stopped
|
||||
break;
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Listener was disposed
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRequest(HttpListenerContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
|
||||
var path = request.Url?.AbsolutePath?.ToLowerInvariant() ?? "/";
|
||||
|
||||
var cwsContext = new HttpCwsContext(context);
|
||||
|
||||
if (_routes.TryGetValue(path, out var handler))
|
||||
{
|
||||
handler.ProcessRequest(cwsContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default 404 response
|
||||
response.StatusCode = 404;
|
||||
var buffer = Encoding.UTF8.GetBytes("Not Found");
|
||||
response.ContentLength64 = buffer.Length;
|
||||
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
response.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle request processing errors
|
||||
try
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
context.Response.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors when closing response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disposes the HttpCwsServer</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_httpListener?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HttpCwsContext class representing an HTTP request/response context</summary>
|
||||
public class HttpCwsContext
|
||||
{
|
||||
private readonly HttpListenerContext _context;
|
||||
|
||||
/// <summary>Gets the HTTP request</summary>
|
||||
public HttpCwsRequest Request { get; }
|
||||
|
||||
/// <summary>Gets the HTTP response</summary>
|
||||
public HttpCwsResponse Response { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsContext</summary>
|
||||
/// <param name="context">Underlying HttpListenerContext</param>
|
||||
public HttpCwsContext(HttpListenerContext context)
|
||||
{
|
||||
_context = context;
|
||||
Request = new HttpCwsRequest(context.Request);
|
||||
Response = new HttpCwsResponse(context.Response);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HttpCwsRequest class representing an HTTP request</summary>
|
||||
public class HttpCwsRequest
|
||||
{
|
||||
private readonly HttpListenerRequest _request;
|
||||
|
||||
/// <summary>Gets the HTTP method</summary>
|
||||
public string HttpMethod => _request.HttpMethod;
|
||||
|
||||
/// <summary>Gets the request URL</summary>
|
||||
public Uri? Url => _request.Url;
|
||||
|
||||
/// <summary>Gets the request headers</summary>
|
||||
public System.Collections.Specialized.NameValueCollection Headers => _request.Headers;
|
||||
|
||||
/// <summary>Gets the query string</summary>
|
||||
public System.Collections.Specialized.NameValueCollection QueryString => _request.QueryString;
|
||||
|
||||
/// <summary>Gets the content type</summary>
|
||||
public string? ContentType => _request.ContentType;
|
||||
|
||||
/// <summary>Gets the content length</summary>
|
||||
public long ContentLength => _request.ContentLength64;
|
||||
|
||||
/// <summary>Gets the input stream</summary>
|
||||
public Stream InputStream => _request.InputStream;
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsRequest</summary>
|
||||
/// <param name="request">Underlying HttpListenerRequest</param>
|
||||
public HttpCwsRequest(HttpListenerRequest request)
|
||||
{
|
||||
_request = request;
|
||||
}
|
||||
|
||||
/// <summary>Gets the request body as a string</summary>
|
||||
/// <returns>Request body content</returns>
|
||||
public string GetRequestBodyAsString()
|
||||
{
|
||||
using var reader = new StreamReader(InputStream, Encoding.UTF8);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HttpCwsResponse class representing an HTTP response</summary>
|
||||
public class HttpCwsResponse
|
||||
{
|
||||
private readonly HttpListenerResponse _response;
|
||||
|
||||
/// <summary>Gets or sets the status code</summary>
|
||||
public int StatusCode
|
||||
{
|
||||
get => _response.StatusCode;
|
||||
set => _response.StatusCode = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the status description</summary>
|
||||
public string StatusDescription
|
||||
{
|
||||
get => _response.StatusDescription;
|
||||
set => _response.StatusDescription = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the content type</summary>
|
||||
public string? ContentType
|
||||
{
|
||||
get => _response.ContentType;
|
||||
set => _response.ContentType = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the content length</summary>
|
||||
public long ContentLength
|
||||
{
|
||||
get => _response.ContentLength64;
|
||||
set => _response.ContentLength64 = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets the response headers</summary>
|
||||
public WebHeaderCollection Headers => _response.Headers;
|
||||
|
||||
/// <summary>Gets the output stream</summary>
|
||||
public Stream OutputStream => _response.OutputStream;
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsResponse</summary>
|
||||
/// <param name="response">Underlying HttpListenerResponse</param>
|
||||
public HttpCwsResponse(HttpListenerResponse response)
|
||||
{
|
||||
_response = response;
|
||||
}
|
||||
|
||||
/// <summary>Writes a string to the response</summary>
|
||||
/// <param name="content">Content to write</param>
|
||||
public void Write(string content)
|
||||
{
|
||||
var buffer = Encoding.UTF8.GetBytes(content);
|
||||
ContentLength = buffer.Length;
|
||||
OutputStream.Write(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
/// <summary>Writes bytes to the response</summary>
|
||||
/// <param name="buffer">Buffer to write</param>
|
||||
/// <param name="offset">Offset in buffer</param>
|
||||
/// <param name="count">Number of bytes to write</param>
|
||||
public void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
OutputStream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <summary>Ends the response</summary>
|
||||
public void End()
|
||||
{
|
||||
try
|
||||
{
|
||||
_response.Close();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore exceptions during close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Interface for HTTP request handlers</summary>
|
||||
public interface IHttpCwsHandler
|
||||
{
|
||||
/// <summary>Processes an HTTP request</summary>
|
||||
/// <param name="context">HTTP context</param>
|
||||
void ProcessRequest(HttpCwsContext context);
|
||||
}
|
||||
|
||||
/// <summary>Mock HttpCwsRoute class for route management</summary>
|
||||
public class HttpCwsRoute
|
||||
{
|
||||
/// <summary>Gets or sets the route path</summary>
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the HTTP method</summary>
|
||||
public string Method { get; set; } = "GET";
|
||||
|
||||
/// <summary>Gets or sets the route handler</summary>
|
||||
public IHttpCwsHandler? Handler { get; set; }
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsRoute</summary>
|
||||
public HttpCwsRoute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsRoute</summary>
|
||||
/// <param name="path">Route path</param>
|
||||
/// <param name="handler">Route handler</param>
|
||||
public HttpCwsRoute(string path, IHttpCwsHandler handler)
|
||||
{
|
||||
Path = path;
|
||||
Handler = handler;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of HttpCwsRoute</summary>
|
||||
/// <param name="path">Route path</param>
|
||||
/// <param name="method">HTTP method</param>
|
||||
/// <param name="handler">Route handler</param>
|
||||
public HttpCwsRoute(string path, string method, IHttpCwsHandler handler)
|
||||
{
|
||||
Path = path;
|
||||
Method = method;
|
||||
Handler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP CWS route collection</summary>
|
||||
public class HttpCwsRouteCollection
|
||||
{
|
||||
private readonly List<HttpCwsRoute> _routes = new List<HttpCwsRoute>();
|
||||
|
||||
/// <summary>Adds a route</summary>
|
||||
/// <param name="route">Route to add</param>
|
||||
public void Add(HttpCwsRoute route)
|
||||
{
|
||||
_routes.Add(route);
|
||||
}
|
||||
|
||||
/// <summary>Removes a route</summary>
|
||||
/// <param name="route">Route to remove</param>
|
||||
public void Remove(HttpCwsRoute route)
|
||||
{
|
||||
_routes.Remove(route);
|
||||
}
|
||||
|
||||
/// <summary>Clears all routes</summary>
|
||||
public void Clear()
|
||||
{
|
||||
_routes.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Gets route count</summary>
|
||||
public int Count => _routes.Count;
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP CWS request event args</summary>
|
||||
public class HttpCwsRequestEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Gets the HTTP context</summary>
|
||||
public HttpCwsContext Context { get; private set; }
|
||||
|
||||
/// <summary>Initializes new instance</summary>
|
||||
/// <param name="context">HTTP context</param>
|
||||
public HttpCwsRequestEventArgs(HttpCwsContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/CrestronMock/NetworkingExtensions.cs
Normal file
41
src/CrestronMock/NetworkingExtensions.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronSockets
|
||||
{
|
||||
// Additional types needed for networking compatibility
|
||||
|
||||
/// <summary>IP address extensions and utilities</summary>
|
||||
public static class IPAddress
|
||||
{
|
||||
/// <summary>Parse IP address string</summary>
|
||||
public static System.Net.IPAddress Parse(string ipString)
|
||||
{
|
||||
return System.Net.IPAddress.Parse(ipString);
|
||||
}
|
||||
|
||||
/// <summary>Any IP address</summary>
|
||||
public static System.Net.IPAddress Any => System.Net.IPAddress.Any;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
/// <summary>Extensions for CrestronQueue</summary>
|
||||
public static class CrestronQueueExtensions
|
||||
{
|
||||
/// <summary>Try to enqueue item</summary>
|
||||
public static bool TryToEnqueue<T>(this CrestronQueue<T> queue, T item)
|
||||
{
|
||||
try
|
||||
{
|
||||
queue.Enqueue(item);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/CrestronMock/SecureTCPClient.cs
Normal file
31
src/CrestronMock/SecureTCPClient.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronSockets
|
||||
{
|
||||
/// <summary>Mock implementation of Crestron SecureTCPClient for testing purposes</summary>
|
||||
public class SecureTCPClient : TCPClient
|
||||
{
|
||||
/// <summary>Initializes a new instance of the SecureTCPClient class</summary>
|
||||
/// <param name="addressToConnectTo">IP address to connect to</param>
|
||||
/// <param name="portNumber">Port number to connect to</param>
|
||||
/// <param name="bufferSize">Size of the receive buffer</param>
|
||||
public SecureTCPClient(string addressToConnectTo, int portNumber, int bufferSize)
|
||||
: base(addressToConnectTo, portNumber, bufferSize)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets whether to verify the host certificate</summary>
|
||||
public bool HostVerification { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets whether to verify the peer certificate</summary>
|
||||
public bool PeerVerification { get; set; } = true;
|
||||
|
||||
/// <summary>Resets the client connection</summary>
|
||||
/// <param name="connectionFlag">Connection flag</param>
|
||||
public void Reset(int connectionFlag)
|
||||
{
|
||||
// Mock implementation
|
||||
DisconnectFromServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/CrestronMock/SocketTypes.cs
Normal file
74
src/CrestronMock/SocketTypes.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronSockets
|
||||
{
|
||||
/// <summary>Mock EthernetAdapterType enumeration</summary>
|
||||
public enum EthernetAdapterType
|
||||
{
|
||||
/// <summary>Ethernet adapter 1</summary>
|
||||
EthernetLANAdapter = 0,
|
||||
/// <summary>Ethernet adapter 2</summary>
|
||||
EthernetAdapter2 = 1,
|
||||
/// <summary>Auto-detect adapter</summary>
|
||||
EthernetAdapterAuto = 2
|
||||
}
|
||||
|
||||
/// <summary>Mock SocketErrorCodes enumeration</summary>
|
||||
public enum SocketErrorCodes
|
||||
{
|
||||
/// <summary>Operation completed successfully</summary>
|
||||
SOCKET_OK = 0,
|
||||
/// <summary>Socket operation pending</summary>
|
||||
SOCKET_OPERATION_PENDING = 1,
|
||||
/// <summary>Socket not connected</summary>
|
||||
SOCKET_NOT_CONNECTED = 2,
|
||||
/// <summary>Connection failed</summary>
|
||||
SOCKET_CONNECTION_FAILED = 3,
|
||||
/// <summary>Invalid client index</summary>
|
||||
SOCKET_INVALID_CLIENT_INDEX = 4,
|
||||
/// <summary>DNS lookup failed</summary>
|
||||
SOCKET_DNS_LOOKUP_FAILED = 5,
|
||||
/// <summary>Invalid address</summary>
|
||||
SOCKET_INVALID_ADDRESS = 6,
|
||||
/// <summary>Connection timed out</summary>
|
||||
SOCKET_CONNECTION_TIMEOUT = 7,
|
||||
/// <summary>Send data failed</summary>
|
||||
SOCKET_SEND_DATA_FAILED = 8,
|
||||
/// <summary>Receive data failed</summary>
|
||||
SOCKET_RECEIVE_DATA_FAILED = 9,
|
||||
/// <summary>Socket closed</summary>
|
||||
SOCKET_CLOSED = 10,
|
||||
/// <summary>Socket disconnected</summary>
|
||||
SOCKET_DISCONNECTED = 11,
|
||||
/// <summary>Max connections reached</summary>
|
||||
SOCKET_MAX_CONNECTIONS_REACHED = 12,
|
||||
/// <summary>Permission denied</summary>
|
||||
SOCKET_PERMISSION_DENIED = 13,
|
||||
/// <summary>Address already in use</summary>
|
||||
SOCKET_ADDRESS_IN_USE = 14,
|
||||
/// <summary>Invalid parameter</summary>
|
||||
SOCKET_INVALID_PARAMETER = 15,
|
||||
/// <summary>Connection in progress</summary>
|
||||
SOCKET_CONNECTION_IN_PROGRESS = 16
|
||||
}
|
||||
|
||||
/// <summary>Mock socket exception</summary>
|
||||
public class SocketException : Exception
|
||||
{
|
||||
/// <summary>Error code</summary>
|
||||
public int ErrorCode { get; }
|
||||
|
||||
/// <summary>Constructor with error code</summary>
|
||||
public SocketException(int errorCode, string message) : base(message)
|
||||
{
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
|
||||
/// <summary>Constructor with message only</summary>
|
||||
public SocketException(string message) : base(message)
|
||||
{
|
||||
ErrorCode = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/CrestronMock/SystemTypes.cs
Normal file
61
src/CrestronMock/SystemTypes.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Crestron.SimplSharp
|
||||
{
|
||||
public delegate void ProgramStatusEventHandler(eProgramStatusEventType eventType);
|
||||
|
||||
public class InitialParametersClass
|
||||
{
|
||||
public static string ApplicationDirectory { get; set; } = "/User/";
|
||||
public static string ProgramIDTag { get; set; } = "MockProgram";
|
||||
public static string ApplicationName { get; set; } = "MockApplication";
|
||||
public static string FirmwareVersion { get; set; } = "1.0.0.0";
|
||||
public static uint ProgramNumber { get; set; } = 1;
|
||||
public static eDevicePlatform DevicePlatform { get; set; } = eDevicePlatform.Appliance;
|
||||
public static eCrestronSeries ControllerSeries { get; set; } = eCrestronSeries.FourSeries;
|
||||
|
||||
// Additional properties needed by PepperDash.Core
|
||||
public static string RoomId { get; set; } = "Room001";
|
||||
public static string RoomName { get; set; } = "Conference Room";
|
||||
public static uint ApplicationNumber { get; set; } = 1;
|
||||
public static string ControllerPromptName { get; set; } = "TestController";
|
||||
public static string ProgramDirectory { get; set; } = "/User/";
|
||||
}
|
||||
|
||||
public enum eDevicePlatform
|
||||
{
|
||||
Appliance = 0,
|
||||
Server = 1,
|
||||
ControlSystem = 2
|
||||
}
|
||||
|
||||
public enum eCrestronSeries
|
||||
{
|
||||
TwoSeries = 2,
|
||||
ThreeSeries = 3,
|
||||
FourSeries = 4,
|
||||
// Alias names used in some contexts
|
||||
Series2 = 2,
|
||||
Series3 = 3,
|
||||
Series4 = 4
|
||||
}
|
||||
|
||||
public enum eRuntimeEnvironment
|
||||
{
|
||||
SimplSharpPro = 0,
|
||||
SimplSharp = 1
|
||||
}
|
||||
|
||||
public enum eProgramCompatibility
|
||||
{
|
||||
Series3And4 = 0,
|
||||
Series3Only = 1,
|
||||
Series4Only = 2
|
||||
}
|
||||
|
||||
public static class Timeout
|
||||
{
|
||||
public const int Infinite = -1;
|
||||
}
|
||||
}
|
||||
360
src/CrestronMock/TCPClient.cs
Normal file
360
src/CrestronMock/TCPClient.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using System;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronSockets
|
||||
{
|
||||
/// <summary>Mock SocketStatus enumeration</summary>
|
||||
public enum SocketStatus
|
||||
{
|
||||
/// <summary>Socket is connecting</summary>
|
||||
SOCKET_STATUS_WAITING = 1,
|
||||
/// <summary>Socket is connected</summary>
|
||||
SOCKET_STATUS_CONNECTED = 2,
|
||||
/// <summary>Socket is not connected</summary>
|
||||
SOCKET_STATUS_NOT_CONNECTED = 3,
|
||||
/// <summary>Connection broken</summary>
|
||||
SOCKET_STATUS_BROKEN_REMOTELY = 4,
|
||||
/// <summary>Connection broken locally</summary>
|
||||
SOCKET_STATUS_BROKEN_LOCALLY = 5,
|
||||
/// <summary>DNS resolution failed</summary>
|
||||
SOCKET_STATUS_DNS_RESOLUTION_FAILED = 6,
|
||||
/// <summary>Connection failed</summary>
|
||||
SOCKET_STATUS_CONNECT_FAILED = 7,
|
||||
/// <summary>Socket error</summary>
|
||||
SOCKET_STATUS_SOCKET_ERROR = 8,
|
||||
/// <summary>Secure connection failed</summary>
|
||||
SOCKET_STATUS_SSL_FAILED = 9,
|
||||
/// <summary>No connection available</summary>
|
||||
SOCKET_STATUS_NO_CONNECT = 10
|
||||
}
|
||||
|
||||
/// <summary>Mock ServerState enumeration</summary>
|
||||
public enum ServerState
|
||||
{
|
||||
/// <summary>Server is not listening</summary>
|
||||
SERVER_NOT_LISTENING = 0,
|
||||
/// <summary>Server is listening</summary>
|
||||
SERVER_LISTENING = 1,
|
||||
/// <summary>Server is connected</summary>
|
||||
SERVER_CONNECTED = 2
|
||||
}
|
||||
|
||||
/// <summary>Mock event handler for TCP client status changes</summary>
|
||||
/// <param name="client">The TCP client</param>
|
||||
/// <param name="clientSocketStatus">The socket status</param>
|
||||
public delegate void TCPClientSocketStatusChangeEventHandler(TCPClient client, SocketStatus clientSocketStatus);
|
||||
|
||||
/// <summary>Delegate for TCP client connect callback</summary>
|
||||
/// <param name="client">TCP client instance</param>
|
||||
public delegate void TCPClientConnectCallback(TCPClient client);
|
||||
|
||||
/// <summary>Delegate for TCP client send callback</summary>
|
||||
/// <param name="client">TCP client instance</param>
|
||||
/// <param name="numberOfBytesSent">Number of bytes sent</param>
|
||||
public delegate void TCPClientSendCallback(TCPClient client, int numberOfBytesSent);
|
||||
|
||||
/// <summary>Delegate for TCP client receive callback</summary>
|
||||
/// <param name="client">TCP client instance</param>
|
||||
/// <param name="numberOfBytesReceived">Number of bytes received</param>
|
||||
public delegate void TCPClientReceiveCallback(TCPClient client, int numberOfBytesReceived);
|
||||
|
||||
/// <summary>Mock event handler for receiving TCP client data</summary>
|
||||
/// <param name="client">The TCP client</param>
|
||||
/// <param name="numberOfBytesReceived">Number of bytes received</param>
|
||||
public delegate void TCPClientReceiveEventHandler(TCPClient client, int numberOfBytesReceived);
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of Crestron TCPClient for testing purposes
|
||||
/// Provides the same public API surface as the real TCPClient
|
||||
/// </summary>
|
||||
public class TCPClient : IDisposable
|
||||
{
|
||||
#region Events
|
||||
|
||||
/// <summary>Event fired when socket status changes</summary>
|
||||
public event TCPClientSocketStatusChangeEventHandler? SocketStatusChange;
|
||||
|
||||
/// <summary>Event fired when data is received</summary>
|
||||
public event TCPClientReceiveEventHandler? DataReceived;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>Gets the client socket status</summary>
|
||||
public SocketStatus ClientStatus { get; private set; } = SocketStatus.SOCKET_STATUS_NOT_CONNECTED;
|
||||
|
||||
/// <summary>Gets or sets the address to connect to</summary>
|
||||
public string AddressToConnectTo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the port number to connect to</summary>
|
||||
public int PortNumber { get; set; } = 0;
|
||||
|
||||
/// <summary>Gets the number of bytes received in the incoming data buffer</summary>
|
||||
public int IncomingDataBufferSize { get; private set; } = 0;
|
||||
|
||||
/// <summary>Gets or sets the socket send timeout in milliseconds</summary>
|
||||
public int SocketSendTimeout { get; set; } = 30000;
|
||||
|
||||
/// <summary>Gets or sets the socket receive timeout in milliseconds</summary>
|
||||
public int SocketReceiveTimeout { get; set; } = 30000;
|
||||
|
||||
/// <summary>Gets or sets whether to keep the connection alive</summary>
|
||||
public bool KeepAlive { get; set; } = false;
|
||||
|
||||
/// <summary>Gets or sets whether Nagle algorithm is enabled</summary>
|
||||
public bool EnableNagle { get; set; } = true;
|
||||
|
||||
/// <summary>Gets the number of bytes available to read</summary>
|
||||
public int BytesAvailable => IncomingDataBufferSize;
|
||||
|
||||
/// <summary>Gets or sets the socket send or receive timeout in milliseconds</summary>
|
||||
public int SocketSendOrReceiveTimeOutInMs
|
||||
{
|
||||
get => SocketSendTimeout;
|
||||
set => SocketSendTimeout = SocketReceiveTimeout = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets the address the client is connected to</summary>
|
||||
public string AddressClientConnectedTo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets the local port number of the client</summary>
|
||||
public uint LocalPortNumberOfClient { get; private set; } = 0;
|
||||
|
||||
/// <summary>Gets the incoming data buffer</summary>
|
||||
public byte[] IncomingDataBuffer { get; private set; } = new byte[0];
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
|
||||
/// <summary>Initializes a new instance of the TCPClient class</summary>
|
||||
/// <param name="addressToConnectTo">IP address to connect to</param>
|
||||
/// <param name="portNumber">Port number to connect to</param>
|
||||
/// <param name="bufferSize">Size of the receive buffer</param>
|
||||
public TCPClient(string addressToConnectTo, int portNumber, int bufferSize)
|
||||
{
|
||||
AddressToConnectTo = addressToConnectTo;
|
||||
PortNumber = portNumber;
|
||||
_bufferSize = bufferSize;
|
||||
_receiveBuffer = new byte[bufferSize];
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the TCPClient class</summary>
|
||||
/// <param name="addressToConnectTo">IP address to connect to</param>
|
||||
/// <param name="portNumber">Port number to connect to</param>
|
||||
/// <param name="bufferSize">Size of the receive buffer</param>
|
||||
/// <param name="ethernetAdapterToBindTo">Ethernet adapter to bind to</param>
|
||||
public TCPClient(string addressToConnectTo, int portNumber, int bufferSize, EthernetAdapterType ethernetAdapterToBindTo)
|
||||
{
|
||||
AddressToConnectTo = addressToConnectTo;
|
||||
PortNumber = portNumber;
|
||||
_bufferSize = bufferSize;
|
||||
_receiveBuffer = new byte[bufferSize];
|
||||
// Note: EthernetAdapterType is ignored in mock implementation
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private readonly int _bufferSize;
|
||||
private readonly byte[] _receiveBuffer;
|
||||
private bool _disposed = false;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>Connects to the remote endpoint asynchronously</summary>
|
||||
/// <returns>Status of the connection attempt</returns>
|
||||
public SocketStatus ConnectToServerAsync()
|
||||
{
|
||||
if (_disposed) return SocketStatus.SOCKET_STATUS_SOCKET_ERROR;
|
||||
|
||||
// Mock connection - simulate successful connection
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
|
||||
AddressClientConnectedTo = AddressToConnectTo;
|
||||
SocketStatusChange?.Invoke(this, ClientStatus);
|
||||
return ClientStatus;
|
||||
}
|
||||
|
||||
/// <summary>Connects to the remote endpoint asynchronously with callback</summary>
|
||||
/// <param name="callback">Callback to invoke when connection completes</param>
|
||||
/// <returns>Status of the connection attempt</returns>
|
||||
public SocketStatus ConnectToServerAsync(TCPClientConnectCallback callback)
|
||||
{
|
||||
var status = ConnectToServerAsync();
|
||||
callback?.Invoke(this);
|
||||
return status;
|
||||
}
|
||||
|
||||
/// <summary>Connects to the remote endpoint</summary>
|
||||
/// <returns>Status of the connection attempt</returns>
|
||||
public SocketStatus ConnectToServer()
|
||||
{
|
||||
return ConnectToServerAsync();
|
||||
}
|
||||
|
||||
/// <summary>Disconnects from the remote endpoint</summary>
|
||||
/// <returns>Status of the disconnection</returns>
|
||||
public SocketStatus DisconnectFromServer()
|
||||
{
|
||||
if (_disposed) return SocketStatus.SOCKET_STATUS_SOCKET_ERROR;
|
||||
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_NOT_CONNECTED;
|
||||
SocketStatusChange?.Invoke(this, ClientStatus);
|
||||
return ClientStatus;
|
||||
}
|
||||
|
||||
/// <summary>Sends data to the connected server</summary>
|
||||
/// <param name="dataToSend">Data to send as a string</param>
|
||||
/// <returns>Number of bytes sent, or -1 on error</returns>
|
||||
public int SendData(string dataToSend)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(dataToSend)) return -1;
|
||||
if (ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) return -1;
|
||||
|
||||
// Mock send - return the length of the string as bytes sent
|
||||
return System.Text.Encoding.UTF8.GetByteCount(dataToSend);
|
||||
}
|
||||
|
||||
/// <summary>Sends data to the connected server</summary>
|
||||
/// <param name="dataToSend">Data to send as byte array</param>
|
||||
/// <param name="lengthToSend">Number of bytes to send</param>
|
||||
/// <returns>Number of bytes sent, or -1 on error</returns>
|
||||
public int SendData(byte[] dataToSend, int lengthToSend)
|
||||
{
|
||||
if (_disposed || dataToSend == null || lengthToSend <= 0) return -1;
|
||||
if (ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) return -1;
|
||||
if (lengthToSend > dataToSend.Length) return -1;
|
||||
|
||||
// Mock send - return the requested length
|
||||
return lengthToSend;
|
||||
}
|
||||
|
||||
/// <summary>Receives data from the server</summary>
|
||||
/// <param name="buffer">Buffer to receive data into</param>
|
||||
/// <param name="bufferIndex">Starting index in the buffer</param>
|
||||
/// <param name="lengthToReceive">Maximum number of bytes to receive</param>
|
||||
/// <returns>Number of bytes received, or -1 on error</returns>
|
||||
public int ReceiveData(byte[] buffer, int bufferIndex, int lengthToReceive)
|
||||
{
|
||||
if (_disposed || buffer == null || bufferIndex < 0 || lengthToReceive <= 0) return -1;
|
||||
if (ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) return -1;
|
||||
if (bufferIndex + lengthToReceive > buffer.Length) return -1;
|
||||
|
||||
// Mock receive - simulate no data available for now
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Receives data from the server as a string</summary>
|
||||
/// <param name="numberOfBytesToReceive">Maximum number of bytes to receive</param>
|
||||
/// <returns>Received data as string, or empty string on error</returns>
|
||||
public string ReceiveData(int numberOfBytesToReceive)
|
||||
{
|
||||
if (_disposed || numberOfBytesToReceive <= 0) return string.Empty;
|
||||
if (ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) return string.Empty;
|
||||
|
||||
// Mock receive - return empty string (no data available)
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>Sends data to the connected server asynchronously</summary>
|
||||
/// <param name="dataToSend">Data to send as byte array</param>
|
||||
/// <param name="lengthToSend">Number of bytes to send</param>
|
||||
/// <returns>Number of bytes sent, or -1 on error</returns>
|
||||
public int SendDataAsync(byte[] dataToSend, int lengthToSend)
|
||||
{
|
||||
return SendData(dataToSend, lengthToSend);
|
||||
}
|
||||
|
||||
/// <summary>Sends data to the connected server asynchronously with callback</summary>
|
||||
/// <param name="dataToSend">Data to send as byte array</param>
|
||||
/// <param name="lengthToSend">Number of bytes to send</param>
|
||||
/// <param name="callback">Callback to invoke when send completes</param>
|
||||
/// <returns>Number of bytes sent, or -1 on error</returns>
|
||||
public int SendDataAsync(byte[] dataToSend, int lengthToSend, TCPClientSendCallback callback)
|
||||
{
|
||||
var result = SendData(dataToSend, lengthToSend);
|
||||
callback?.Invoke(this, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Receives data from the server asynchronously</summary>
|
||||
/// <returns>Number of bytes received, or -1 on error</returns>
|
||||
public int ReceiveDataAsync()
|
||||
{
|
||||
if (_disposed) return -1;
|
||||
if (ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) return -1;
|
||||
|
||||
// Mock receive - simulate no data available
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>Receives data from the server asynchronously with callback</summary>
|
||||
/// <param name="callback">Callback to invoke when data is received</param>
|
||||
/// <returns>Number of bytes received, or -1 on error</returns>
|
||||
public int ReceiveDataAsync(TCPClientReceiveCallback callback)
|
||||
{
|
||||
var result = ReceiveDataAsync();
|
||||
callback?.Invoke(this, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Simulates receiving data (for testing purposes)</summary>
|
||||
/// <param name="data">Data to simulate receiving</param>
|
||||
public void SimulateDataReceived(string data)
|
||||
{
|
||||
if (_disposed || string.IsNullOrEmpty(data)) return;
|
||||
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(data);
|
||||
var bytesToCopy = Math.Min(bytes.Length, _receiveBuffer.Length);
|
||||
Array.Copy(bytes, _receiveBuffer, bytesToCopy);
|
||||
IncomingDataBufferSize = bytesToCopy;
|
||||
|
||||
DataReceived?.Invoke(this, bytesToCopy);
|
||||
}
|
||||
|
||||
/// <summary>Simulates a socket status change (for testing purposes)</summary>
|
||||
/// <param name="newStatus">New socket status</param>
|
||||
public void SimulateStatusChange(SocketStatus newStatus)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
ClientStatus = newStatus;
|
||||
SocketStatusChange?.Invoke(this, newStatus);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable Implementation
|
||||
|
||||
/// <summary>Disposes the TCP client and releases resources</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Protected dispose method</summary>
|
||||
/// <param name="disposing">True if disposing managed resources</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Disconnect if still connected
|
||||
if (ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
DisconnectFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
552
src/CrestronMock/TCPServer.cs
Normal file
552
src/CrestronMock/TCPServer.cs
Normal file
@@ -0,0 +1,552 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronSockets
|
||||
{
|
||||
/// <summary>Mock TCPServer class for server-side TCP operations</summary>
|
||||
public class TCPServer : IDisposable
|
||||
{
|
||||
private TcpListener? _listener;
|
||||
private readonly List<TCPClientConnection> _clients = new List<TCPClientConnection>();
|
||||
private bool _listening;
|
||||
private readonly object _lockObject = new object();
|
||||
private int _bufferSize = 4096;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
/// <summary>Event fired when waiting for connections</summary>
|
||||
public event TCPServerWaitingForConnectionsEventHandler? WaitingForConnections;
|
||||
|
||||
/// <summary>Event fired when a client connects</summary>
|
||||
public event TCPServerClientConnectEventHandler? ClientConnected;
|
||||
|
||||
/// <summary>Event fired when a client disconnects</summary>
|
||||
public event TCPServerClientDisconnectEventHandler? ClientDisconnected;
|
||||
|
||||
/// <summary>Event fired when data is received from a client</summary>
|
||||
public event TCPServerReceiveDataEventHandler? ReceivedData;
|
||||
|
||||
/// <summary>Event fired when socket status changes</summary>
|
||||
public event TCPServerWaitingForConnectionsEventHandler? SocketStatusChange;
|
||||
|
||||
/// <summary>Gets the server state</summary>
|
||||
public ServerState State { get; private set; } = ServerState.SERVER_NOT_LISTENING;
|
||||
|
||||
/// <summary>Gets or sets the port number</summary>
|
||||
public int PortNumber { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the socket send or receive timeout in milliseconds</summary>
|
||||
public int SocketSendOrReceiveTimeOutInMs { get; set; } = 30000;
|
||||
|
||||
/// <summary>Gets the server socket status based on current state</summary>
|
||||
public SocketStatus ServerSocketStatus => State == ServerState.SERVER_LISTENING ? SocketStatus.SOCKET_STATUS_CONNECTED : SocketStatus.SOCKET_STATUS_NOT_CONNECTED; /// <summary>Gets the maximum number of clients</summary>
|
||||
public int MaxNumberOfClientSupported { get; private set; }
|
||||
|
||||
/// <summary>Gets the number of connected clients</summary>
|
||||
public int NumberOfClientsConnected
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
return _clients.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a TCP server with IP address binding</summary>
|
||||
/// <param name="ipAddress">IP address to bind to</param>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="bufferSize">Buffer size for incoming data</param>
|
||||
/// <param name="ethernetAdapterToBindTo">Ethernet adapter to bind to</param>
|
||||
/// <param name="maxNumberOfClientSupported">Maximum number of clients</param>
|
||||
public TCPServer(string ipAddress, int portNumber, int bufferSize, EthernetAdapterType ethernetAdapterToBindTo, int maxNumberOfClientSupported)
|
||||
{
|
||||
PortNumber = portNumber;
|
||||
MaxNumberOfClientSupported = maxNumberOfClientSupported;
|
||||
_bufferSize = bufferSize;
|
||||
}
|
||||
|
||||
/// <summary>Creates a TCP server</summary>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="bufferSize">Buffer size for incoming data</param>
|
||||
/// <param name="maxNumberOfClientSupported">Maximum number of clients</param>
|
||||
public TCPServer(int portNumber, int bufferSize, int maxNumberOfClientSupported)
|
||||
{
|
||||
PortNumber = portNumber;
|
||||
MaxNumberOfClientSupported = maxNumberOfClientSupported;
|
||||
_bufferSize = bufferSize;
|
||||
}
|
||||
|
||||
/// <summary>Creates a TCP server with just port and max clients</summary>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="maxNumberOfClientSupported">Maximum number of clients</param>
|
||||
public TCPServer(int portNumber, int maxNumberOfClientSupported)
|
||||
{
|
||||
PortNumber = portNumber;
|
||||
MaxNumberOfClientSupported = maxNumberOfClientSupported;
|
||||
_bufferSize = 4096; // Default buffer size
|
||||
}
|
||||
|
||||
/// <summary>Starts listening for client connections</summary>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes WaitForConnectionAsync()
|
||||
{
|
||||
if (_listening)
|
||||
return SocketErrorCodes.SOCKET_OPERATION_PENDING;
|
||||
|
||||
try
|
||||
{
|
||||
_listener = new TcpListener(IPAddress.Any, PortNumber);
|
||||
_listener.Start();
|
||||
_listening = true;
|
||||
State = ServerState.SERVER_LISTENING;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
// Start accepting clients in background
|
||||
_ = Task.Run(() => AcceptClientsAsync());
|
||||
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
State = ServerState.SERVER_NOT_LISTENING;
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts listening for connections asynchronously with callback</summary>
|
||||
/// <param name="ipAddress">IP address to listen on</param>
|
||||
/// <param name="callback">Callback for connection events</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes WaitForConnectionAsync(string ipAddress, TCPServerWaitingForConnectionsEventHandler callback)
|
||||
{
|
||||
SocketStatusChange += callback;
|
||||
return WaitForConnectionAsync();
|
||||
}
|
||||
|
||||
/// <summary>Stops listening for connections</summary>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes Stop()
|
||||
{
|
||||
if (!_listening)
|
||||
return SocketErrorCodes.SOCKET_NOT_CONNECTED;
|
||||
|
||||
try
|
||||
{
|
||||
_listening = false;
|
||||
_listener?.Stop();
|
||||
State = ServerState.SERVER_NOT_LISTENING;
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
client.Disconnect();
|
||||
}
|
||||
_clients.Clear();
|
||||
}
|
||||
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sends data to a specific client</summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <param name="dataLength">Length of data</param>
|
||||
/// <param name="clientIndex">Index of client to send to</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes SendData(byte[] data, int dataLength, uint clientIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (clientIndex >= _clients.Count)
|
||||
return SocketErrorCodes.SOCKET_INVALID_CLIENT_INDEX;
|
||||
|
||||
return _clients[(int)clientIndex].SendData(data, dataLength);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sends data to all connected clients</summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <param name="dataLength">Length of data</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes SendDataToAll(byte[] data, int dataLength)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
var result = SocketErrorCodes.SOCKET_OK;
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
var sendResult = client.SendData(data, dataLength);
|
||||
if (sendResult != SocketErrorCodes.SOCKET_OK)
|
||||
result = sendResult;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disconnects a specific client</summary>
|
||||
/// <param name="clientIndex">Index of client to disconnect</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes Disconnect(uint clientIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (clientIndex >= _clients.Count)
|
||||
return SocketErrorCodes.SOCKET_INVALID_CLIENT_INDEX;
|
||||
|
||||
var client = _clients[(int)clientIndex];
|
||||
client.Disconnect();
|
||||
_clients.RemoveAt((int)clientIndex);
|
||||
|
||||
ClientDisconnected?.Invoke(this, new TCPServerClientDisconnectEventArgs((uint)clientIndex));
|
||||
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the IP address of a connected client</summary>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>IP address as string</returns>
|
||||
public string GetAddressServerAcceptedConnectionFromForSpecificClient(uint clientIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (clientIndex >= _clients.Count)
|
||||
return string.Empty;
|
||||
|
||||
return _clients[(int)clientIndex].ClientIPAddress;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the socket status for a specific client</summary>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>Socket status</returns>
|
||||
public SocketStatus GetServerSocketStatusForSpecificClient(uint clientIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (clientIndex >= _clients.Count)
|
||||
return SocketStatus.SOCKET_STATUS_NOT_CONNECTED;
|
||||
|
||||
return _clients[(int)clientIndex].IsConnected ?
|
||||
SocketStatus.SOCKET_STATUS_CONNECTED :
|
||||
SocketStatus.SOCKET_STATUS_NOT_CONNECTED;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the port number the server accepted connection from for a specific client</summary>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>Port number</returns>
|
||||
public int GetPortNumberServerAcceptedConnectionFromForSpecificClient(uint clientIndex)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (clientIndex >= _clients.Count)
|
||||
return 0;
|
||||
|
||||
return _clients[(int)clientIndex].ClientPort;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the local address the server accepted connection from for a specific client</summary>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>Local address</returns>
|
||||
public string GetLocalAddressServerAcceptedConnectionFromForSpecificClient(uint clientIndex)
|
||||
{
|
||||
return "127.0.0.1"; // Mock local address
|
||||
}
|
||||
|
||||
/// <summary>Gets the incoming data buffer for a specific client</summary>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>Incoming data buffer</returns>
|
||||
public byte[] GetIncomingDataBufferForSpecificClient(uint clientIndex)
|
||||
{
|
||||
return new byte[0]; // Mock empty buffer
|
||||
}
|
||||
|
||||
/// <summary>Sends data to a specific client asynchronously</summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <param name="dataLength">Length of data to send</param>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes SendDataAsync(byte[] data, int dataLength, uint clientIndex)
|
||||
{
|
||||
return SendData(data, dataLength, clientIndex);
|
||||
}
|
||||
|
||||
/// <summary>Receives data from a specific client asynchronously</summary>
|
||||
/// <param name="clientIndex">Index of client</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes ReceiveDataAsync(uint clientIndex)
|
||||
{
|
||||
// Mock implementation - no data to receive
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
|
||||
private async Task AcceptClientsAsync()
|
||||
{
|
||||
while (_listening && _listener != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tcpClient = await _listener.AcceptTcpClientAsync();
|
||||
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (_clients.Count >= MaxNumberOfClientSupported)
|
||||
{
|
||||
tcpClient.Close();
|
||||
continue;
|
||||
}
|
||||
|
||||
var clientConnection = new TCPClientConnection(tcpClient, (uint)_clients.Count);
|
||||
clientConnection.DataReceived += OnClientDataReceived;
|
||||
clientConnection.Disconnected += OnClientDisconnected;
|
||||
_clients.Add(clientConnection);
|
||||
|
||||
ClientConnected?.Invoke(this, new TCPServerClientConnectEventArgs((uint)(_clients.Count - 1)));
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Server was stopped
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClientDataReceived(object? sender, TCPClientDataEventArgs e)
|
||||
{
|
||||
if (sender is TCPClientConnection client)
|
||||
{
|
||||
var args = new TCPServerReceiveDataEventArgs(e.Data, e.DataLength, client.ClientIndex);
|
||||
ReceivedData?.Invoke(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClientDisconnected(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is TCPClientConnection client)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
var index = _clients.IndexOf(client);
|
||||
if (index >= 0)
|
||||
{
|
||||
_clients.RemoveAt(index);
|
||||
ClientDisconnected?.Invoke(this, new TCPServerClientDisconnectEventArgs(client.ClientIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disposes the TCPServer</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_listener?.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock SecureTCPServer class for secure server-side TCP operations</summary>
|
||||
public class SecureTCPServer : TCPServer
|
||||
{
|
||||
/// <summary>Initializes a new instance of SecureTCPServer</summary>
|
||||
/// <param name="ipAddress">IP address to bind to</param>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="bufferSize">Buffer size for data reception</param>
|
||||
/// <param name="ethernetAdapterToBindTo">Ethernet adapter to bind to</param>
|
||||
/// <param name="maxNumberOfClientSupported">Maximum number of clients</param>
|
||||
public SecureTCPServer(string ipAddress, int portNumber, int bufferSize, EthernetAdapterType ethernetAdapterToBindTo, int maxNumberOfClientSupported)
|
||||
: base(ipAddress, portNumber, bufferSize, ethernetAdapterToBindTo, maxNumberOfClientSupported)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of SecureTCPServer</summary>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="bufferSize">Buffer size for data reception</param>
|
||||
/// <param name="maxNumberOfClientSupported">Maximum number of clients</param>
|
||||
public SecureTCPServer(int portNumber, int bufferSize, int maxNumberOfClientSupported)
|
||||
: base(portNumber, bufferSize, maxNumberOfClientSupported)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of SecureTCPServer</summary>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="maxNumberOfClientSupported">Maximum number of clients</param>
|
||||
public SecureTCPServer(int portNumber, int maxNumberOfClientSupported)
|
||||
: base(portNumber, 4096, maxNumberOfClientSupported) // Default buffer size
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the handshake timeout in seconds</summary>
|
||||
public int HandshakeTimeout { get; set; } = 30;
|
||||
|
||||
/// <summary>Event raised when socket status changes with client details</summary>
|
||||
public event SecureTCPServerSocketStatusEventHandler? SocketStatusChangeWithClientDetails;
|
||||
}
|
||||
|
||||
/// <summary>Internal class representing a client connection</summary>
|
||||
internal class TCPClientConnection
|
||||
{
|
||||
private readonly TcpClient _tcpClient;
|
||||
private readonly NetworkStream _stream;
|
||||
private readonly byte[] _buffer = new byte[4096];
|
||||
private bool _connected = true;
|
||||
|
||||
public uint ClientIndex { get; }
|
||||
public string ClientIPAddress { get; }
|
||||
public bool IsConnected => _connected;
|
||||
public int ClientPort { get; }
|
||||
|
||||
public event EventHandler<TCPClientDataEventArgs>? DataReceived;
|
||||
public event EventHandler? Disconnected;
|
||||
|
||||
public TCPClientConnection(TcpClient tcpClient, uint clientIndex)
|
||||
{
|
||||
_tcpClient = tcpClient;
|
||||
ClientIndex = clientIndex;
|
||||
_stream = tcpClient.GetStream();
|
||||
|
||||
var endpoint = tcpClient.Client.RemoteEndPoint as IPEndPoint;
|
||||
ClientIPAddress = endpoint?.Address.ToString() ?? "Unknown";
|
||||
ClientPort = endpoint?.Port ?? 0;
|
||||
|
||||
_ = Task.Run(ReceiveDataAsync);
|
||||
}
|
||||
|
||||
public SocketErrorCodes SendData(byte[] data, int dataLength)
|
||||
{
|
||||
if (!_connected)
|
||||
return SocketErrorCodes.SOCKET_NOT_CONNECTED;
|
||||
|
||||
try
|
||||
{
|
||||
_stream.Write(data, 0, dataLength);
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Disconnect();
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
|
||||
_connected = false;
|
||||
_stream?.Close();
|
||||
_tcpClient?.Close();
|
||||
Disconnected?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private async Task ReceiveDataAsync()
|
||||
{
|
||||
while (_connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytesRead = await _stream.ReadAsync(_buffer, 0, _buffer.Length);
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
|
||||
var data = new byte[bytesRead];
|
||||
Array.Copy(_buffer, data, bytesRead);
|
||||
DataReceived?.Invoke(this, new TCPClientDataEventArgs(data, bytesRead));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Event args for TCP client data</summary>
|
||||
internal class TCPClientDataEventArgs : EventArgs
|
||||
{
|
||||
public byte[] Data { get; }
|
||||
public int DataLength { get; }
|
||||
|
||||
public TCPClientDataEventArgs(byte[] data, int dataLength)
|
||||
{
|
||||
Data = data;
|
||||
DataLength = dataLength;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Server state enumeration</summary>
|
||||
public enum SocketServerState
|
||||
{
|
||||
/// <summary>Server is not listening</summary>
|
||||
SERVER_NOT_LISTENING = 0,
|
||||
/// <summary>Server is listening for connections</summary>
|
||||
SERVER_LISTENING = 1,
|
||||
/// <summary>Server is connected</summary>
|
||||
SERVER_CONNECTED = 2
|
||||
}
|
||||
|
||||
// Event handler delegates
|
||||
public delegate void TCPServerWaitingForConnectionsEventHandler(TCPServer server, TCPServerWaitingForConnectionsEventArgs args);
|
||||
public delegate void TCPServerClientConnectEventHandler(TCPServer server, TCPServerClientConnectEventArgs args);
|
||||
public delegate void TCPServerClientDisconnectEventHandler(TCPServer server, TCPServerClientDisconnectEventArgs args);
|
||||
public delegate void TCPServerReceiveDataEventHandler(TCPServer server, TCPServerReceiveDataEventArgs args);
|
||||
public delegate void SecureTCPServerSocketStatusChangeEventHandler(SecureTCPServer server, TCPServerWaitingForConnectionsEventArgs args);
|
||||
/// <summary>Delegate for secure TCP server socket status changes with client details</summary>
|
||||
public delegate void SecureTCPServerSocketStatusEventHandler(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus);
|
||||
|
||||
// Event argument classes
|
||||
public class TCPServerWaitingForConnectionsEventArgs : EventArgs
|
||||
{
|
||||
public int ErrorCode { get; }
|
||||
public TCPServerWaitingForConnectionsEventArgs(int errorCode) { ErrorCode = errorCode; }
|
||||
}
|
||||
|
||||
public class TCPServerClientConnectEventArgs : EventArgs
|
||||
{
|
||||
public uint ClientIndex { get; }
|
||||
public TCPServerClientConnectEventArgs(uint clientIndex) { ClientIndex = clientIndex; }
|
||||
}
|
||||
|
||||
public class TCPServerClientDisconnectEventArgs : EventArgs
|
||||
{
|
||||
public uint ClientIndex { get; }
|
||||
public TCPServerClientDisconnectEventArgs(uint clientIndex) { ClientIndex = clientIndex; }
|
||||
}
|
||||
|
||||
public class TCPServerReceiveDataEventArgs : EventArgs
|
||||
{
|
||||
public byte[] Data { get; }
|
||||
public int DataLength { get; }
|
||||
public uint ClientIndex { get; }
|
||||
|
||||
public TCPServerReceiveDataEventArgs(byte[] data, int dataLength, uint clientIndex)
|
||||
{
|
||||
Data = data;
|
||||
DataLength = dataLength;
|
||||
ClientIndex = clientIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
270
src/CrestronMock/UDPServer.cs
Normal file
270
src/CrestronMock/UDPServer.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Crestron.SimplSharp.CrestronSockets
|
||||
{
|
||||
/// <summary>Mock UDPServer class for UDP communication</summary>
|
||||
public class UDPServer : IDisposable
|
||||
{
|
||||
private UdpClient? _udpClient;
|
||||
private bool _listening;
|
||||
private readonly object _lockObject = new object();
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
/// <summary>Event fired when data is received</summary>
|
||||
public event UDPServerReceiveDataEventHandler? ReceivedData;
|
||||
|
||||
/// <summary>Gets the server state</summary>
|
||||
public SocketServerState State { get; private set; } = SocketServerState.SERVER_NOT_LISTENING;
|
||||
|
||||
/// <summary>Gets the server status (alias for State)</summary>
|
||||
public SocketServerState ServerStatus => State;
|
||||
|
||||
/// <summary>Gets the client status as SocketStatus</summary>
|
||||
public SocketStatus ClientStatus => State == SocketServerState.SERVER_LISTENING ? SocketStatus.SOCKET_STATUS_CONNECTED : SocketStatus.SOCKET_STATUS_NOT_CONNECTED;
|
||||
|
||||
/// <summary>Gets or sets the port number</summary>
|
||||
public int PortNumber { get; set; }
|
||||
|
||||
/// <summary>Gets the buffer size</summary>
|
||||
public int BufferSize { get; private set; }
|
||||
|
||||
/// <summary>Gets the IP address of the last message received from</summary>
|
||||
public string IPAddressLastMessageReceivedFrom { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets the IP port of the last message received from</summary>
|
||||
public int IPPortLastMessageReceivedFrom { get; private set; }
|
||||
|
||||
/// <summary>Gets the incoming data buffer</summary>
|
||||
public byte[] IncomingDataBuffer { get; private set; } = new byte[0];
|
||||
|
||||
/// <summary>Initializes a new instance of UDPServer</summary>
|
||||
public UDPServer()
|
||||
{
|
||||
PortNumber = 0;
|
||||
BufferSize = 1024;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of UDPServer</summary>
|
||||
/// <param name="ipAddress">IP address to bind to</param>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="bufferSize">Buffer size for data reception</param>
|
||||
/// <param name="ethernetAdapterToBindTo">Ethernet adapter to bind to</param>
|
||||
public UDPServer(string ipAddress, int portNumber, int bufferSize, EthernetAdapterType ethernetAdapterToBindTo)
|
||||
{
|
||||
PortNumber = portNumber;
|
||||
BufferSize = bufferSize;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of UDPServer</summary>
|
||||
/// <param name="portNumber">Port number to listen on</param>
|
||||
/// <param name="bufferSize">Buffer size for data reception</param>
|
||||
public UDPServer(int portNumber, int bufferSize)
|
||||
{
|
||||
PortNumber = portNumber;
|
||||
BufferSize = bufferSize;
|
||||
}
|
||||
|
||||
/// <summary>Starts listening for UDP packets</summary>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes EnableUDPServer()
|
||||
{
|
||||
if (_listening)
|
||||
return SocketErrorCodes.SOCKET_OPERATION_PENDING;
|
||||
|
||||
try
|
||||
{
|
||||
_udpClient = new UdpClient(PortNumber);
|
||||
_listening = true;
|
||||
State = SocketServerState.SERVER_LISTENING;
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_ = Task.Run(() => ReceiveDataAsync(_cancellationTokenSource.Token));
|
||||
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
State = SocketServerState.SERVER_NOT_LISTENING;
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Starts listening for UDP packets on specified hostname and port</summary>
|
||||
/// <param name="hostname">Hostname to bind to</param>
|
||||
/// <param name="port">Port number to listen on</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes EnableUDPServer(string hostname, int port)
|
||||
{
|
||||
PortNumber = port;
|
||||
return EnableUDPServer();
|
||||
}
|
||||
|
||||
/// <summary>Stops listening for UDP packets</summary>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes DisableUDPServer()
|
||||
{
|
||||
if (!_listening)
|
||||
return SocketErrorCodes.SOCKET_NOT_CONNECTED;
|
||||
|
||||
try
|
||||
{
|
||||
_listening = false;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_udpClient?.Close();
|
||||
State = SocketServerState.SERVER_NOT_LISTENING;
|
||||
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sends data to a specific endpoint</summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <param name="dataLength">Length of data</param>
|
||||
/// <param name="ipAddress">Target IP address</param>
|
||||
/// <param name="portNumber">Target port number</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes SendData(byte[] data, int dataLength, string ipAddress, int portNumber)
|
||||
{
|
||||
if (!_listening || _udpClient == null)
|
||||
return SocketErrorCodes.SOCKET_NOT_CONNECTED;
|
||||
|
||||
try
|
||||
{
|
||||
var endpoint = new IPEndPoint(IPAddress.Parse(ipAddress), portNumber);
|
||||
_udpClient.Send(data, dataLength, endpoint);
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sends data to the last received endpoint</summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <param name="dataLength">Length of data</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes SendData(byte[] data, int dataLength)
|
||||
{
|
||||
return SendData(data, dataLength, IPAddressLastMessageReceivedFrom, IPPortLastMessageReceivedFrom);
|
||||
}
|
||||
|
||||
/// <summary>Receives data asynchronously</summary>
|
||||
/// <param name="callback">Callback to invoke when data is received</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes ReceiveDataAsync(UDPServerReceiveDataEventHandler callback)
|
||||
{
|
||||
ReceivedData += callback;
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
|
||||
/// <summary>Receives data asynchronously with simple callback</summary>
|
||||
/// <param name="callback">Simple callback to invoke when data is received</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes ReceiveDataAsync(UDPServerReceiveDataSimpleEventHandler callback)
|
||||
{
|
||||
// Convert simple callback to full event handler and subscribe
|
||||
ReceivedData += (server, args) => callback(server, args.DataLength);
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
|
||||
/// <summary>Sends data to a specific endpoint</summary>
|
||||
/// <param name="data">Data to send</param>
|
||||
/// <param name="dataLength">Length of data</param>
|
||||
/// <param name="endpoint">Target endpoint</param>
|
||||
/// <returns>SocketErrorCodes indicating success or failure</returns>
|
||||
public SocketErrorCodes SendData(byte[] data, int dataLength, IPEndPoint endpoint)
|
||||
{
|
||||
if (!_listening || _udpClient == null)
|
||||
return SocketErrorCodes.SOCKET_NOT_CONNECTED;
|
||||
|
||||
try
|
||||
{
|
||||
_udpClient.Send(data, dataLength, endpoint);
|
||||
return SocketErrorCodes.SOCKET_OK;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return SocketErrorCodes.SOCKET_CONNECTION_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveDataAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
while (_listening && _udpClient != null && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _udpClient.ReceiveAsync();
|
||||
|
||||
var args = new UDPServerReceiveDataEventArgs(
|
||||
result.Buffer,
|
||||
result.Buffer.Length,
|
||||
result.RemoteEndPoint.Address.ToString(),
|
||||
result.RemoteEndPoint.Port);
|
||||
|
||||
ReceivedData?.Invoke(this, args);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// UDP client was disposed
|
||||
break;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle other exceptions
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Disposes the UDPServer</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
DisableUDPServer();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_udpClient?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler delegates for UDP
|
||||
public delegate void UDPServerReceiveDataEventHandler(UDPServer server, UDPServerReceiveDataEventArgs args);
|
||||
public delegate void UDPServerReceiveDataSimpleEventHandler(UDPServer server, int numBytes);
|
||||
|
||||
// Event argument classes for UDP
|
||||
public class UDPServerReceiveDataEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Gets the received data</summary>
|
||||
public byte[] Data { get; }
|
||||
|
||||
/// <summary>Gets the length of received data</summary>
|
||||
public int DataLength { get; }
|
||||
|
||||
/// <summary>Gets the sender's IP address</summary>
|
||||
public string IPAddress { get; }
|
||||
|
||||
/// <summary>Gets the sender's port number</summary>
|
||||
public int Port { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of UDPServerReceiveDataEventArgs</summary>
|
||||
/// <param name="data">Received data</param>
|
||||
/// <param name="dataLength">Length of received data</param>
|
||||
/// <param name="ipAddress">Sender's IP address</param>
|
||||
/// <param name="port">Sender's port number</param>
|
||||
public UDPServerReceiveDataEventArgs(byte[] data, int dataLength, string ipAddress, int port)
|
||||
{
|
||||
Data = data;
|
||||
DataLength = dataLength;
|
||||
IPAddress = ipAddress;
|
||||
Port = port;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/CrestronMock/UrlParserTypes.cs
Normal file
45
src/CrestronMock/UrlParserTypes.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
|
||||
namespace Crestron.SimplSharp.Net.Http
|
||||
{
|
||||
/// <summary>Mock UrlParser for HTTP</summary>
|
||||
public static class UrlParser
|
||||
{
|
||||
/// <summary>Parse a URL string</summary>
|
||||
/// <param name="url">URL to parse</param>
|
||||
/// <returns>Parsed URL components</returns>
|
||||
public static UrlParserResult Parse(string url)
|
||||
{
|
||||
return new UrlParserResult { Url = url };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>URL parser result</summary>
|
||||
public class UrlParserResult
|
||||
{
|
||||
/// <summary>Original URL</summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Crestron.SimplSharp.Net.Https
|
||||
{
|
||||
/// <summary>Mock UrlParser for HTTPS - different from HTTP version</summary>
|
||||
public static class UrlParser
|
||||
{
|
||||
/// <summary>Parse a URL string</summary>
|
||||
/// <param name="url">URL to parse</param>
|
||||
/// <returns>Parsed URL components</returns>
|
||||
public static UrlParserResult Parse(string url)
|
||||
{
|
||||
return new UrlParserResult { Url = url };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>HTTPS URL parser result</summary>
|
||||
public class UrlParserResult
|
||||
{
|
||||
/// <summary>Original URL</summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
295
src/CrestronMock/WebAndNetworking.cs
Normal file
295
src/CrestronMock/WebAndNetworking.cs
Normal file
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Crestron.SimplSharp.Net.Http
|
||||
{
|
||||
/// <summary>HTTP request types</summary>
|
||||
public enum RequestType
|
||||
{
|
||||
/// <summary>GET request</summary>
|
||||
Get = 0,
|
||||
/// <summary>POST request</summary>
|
||||
Post = 1,
|
||||
/// <summary>PUT request</summary>
|
||||
Put = 2,
|
||||
/// <summary>DELETE request</summary>
|
||||
Delete = 3,
|
||||
/// <summary>HEAD request</summary>
|
||||
Head = 4,
|
||||
/// <summary>OPTIONS request</summary>
|
||||
Options = 5,
|
||||
/// <summary>PATCH request</summary>
|
||||
Patch = 6
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP client</summary>
|
||||
public class HttpClient
|
||||
{
|
||||
/// <summary>Gets or sets the keep-alive setting</summary>
|
||||
public bool KeepAlive { get; set; } = false;
|
||||
|
||||
/// <summary>Gets or sets the port number</summary>
|
||||
public int Port { get; set; } = 80;
|
||||
|
||||
/// <summary>Dispatch HTTP request</summary>
|
||||
/// <param name="request">HTTP request</param>
|
||||
/// <param name="callback">Callback for response</param>
|
||||
public void Dispatch(HttpClientRequest request, Action<HttpClientResponse> callback)
|
||||
{
|
||||
// Mock implementation - invoke callback with empty response
|
||||
var response = new HttpClientResponse();
|
||||
callback?.Invoke(response);
|
||||
}
|
||||
|
||||
/// <summary>Dispatches HTTP request synchronously</summary>
|
||||
/// <param name="request">HTTP request</param>
|
||||
/// <returns>HTTP response</returns>
|
||||
public HttpClientResponse Dispatch(HttpClientRequest request)
|
||||
{
|
||||
// Mock implementation - return empty response
|
||||
return new HttpClientResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP client request</summary>
|
||||
public class HttpClientRequest
|
||||
{
|
||||
/// <summary>Gets or sets the URL parser</summary>
|
||||
public Crestron.SimplSharp.Net.Http.UrlParserResult Url { get; set; } = new Crestron.SimplSharp.Net.Http.UrlParserResult();
|
||||
|
||||
/// <summary>Gets or sets the HTTP method</summary>
|
||||
public RequestType RequestType { get; set; } = RequestType.Get;
|
||||
|
||||
/// <summary>Gets or sets the content data</summary>
|
||||
public string ContentString { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets the headers collection</summary>
|
||||
public HttpHeaderCollection Header { get; } = new HttpHeaderCollection();
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP client response</summary>
|
||||
public class HttpClientResponse
|
||||
{
|
||||
/// <summary>Gets the response code</summary>
|
||||
public int Code { get; set; } = 200;
|
||||
|
||||
/// <summary>Gets the response content</summary>
|
||||
public string ContentString { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets the response data as bytes</summary>
|
||||
public byte[] ContentBytes { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets the headers collection</summary>
|
||||
public HttpHeaderCollection Header { get; } = new HttpHeaderCollection();
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP header collection</summary>
|
||||
public class HttpHeaderCollection
|
||||
{
|
||||
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>Gets or sets the content type</summary>
|
||||
public string ContentType
|
||||
{
|
||||
get => _headers.TryGetValue("Content-Type", out var value) ? value : string.Empty;
|
||||
set => _headers["Content-Type"] = value;
|
||||
}
|
||||
|
||||
/// <summary>Sets a header value</summary>
|
||||
/// <param name="name">Header name</param>
|
||||
/// <param name="value">Header value</param>
|
||||
public void SetHeaderValue(string name, string value)
|
||||
{
|
||||
_headers[name] = value;
|
||||
}
|
||||
|
||||
/// <summary>Gets a header value</summary>
|
||||
/// <param name="name">Header name</param>
|
||||
/// <returns>Header value or empty string if not found</returns>
|
||||
public string GetHeaderValue(string name)
|
||||
{
|
||||
return _headers.TryGetValue(name, out var value) ? value : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
namespace Crestron.SimplSharp.Net.Https
|
||||
{
|
||||
/// <summary>HTTPS request types</summary>
|
||||
public enum RequestType
|
||||
{
|
||||
/// <summary>GET request</summary>
|
||||
Get = 0,
|
||||
/// <summary>POST request</summary>
|
||||
Post = 1,
|
||||
/// <summary>PUT request</summary>
|
||||
Put = 2,
|
||||
/// <summary>DELETE request</summary>
|
||||
Delete = 3,
|
||||
/// <summary>HEAD request</summary>
|
||||
Head = 4,
|
||||
/// <summary>OPTIONS request</summary>
|
||||
Options = 5,
|
||||
/// <summary>PATCH request</summary>
|
||||
Patch = 6
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTPS client</summary>
|
||||
public class HttpsClient
|
||||
{
|
||||
/// <summary>Gets or sets the keep-alive setting</summary>
|
||||
public bool KeepAlive { get; set; } = false;
|
||||
|
||||
/// <summary>Gets or sets the host verification setting</summary>
|
||||
public bool HostVerification { get; set; } = false;
|
||||
|
||||
/// <summary>Gets or sets the peer verification setting</summary>
|
||||
public bool PeerVerification { get; set; } = false;
|
||||
|
||||
/// <summary>Dispatch HTTPS request</summary>
|
||||
/// <param name="request">HTTPS request</param>
|
||||
/// <param name="callback">Callback for response</param>
|
||||
public void Dispatch(HttpsClientRequest request, Action<HttpsClientResponse> callback)
|
||||
{
|
||||
// Mock implementation - invoke callback with empty response
|
||||
var response = new HttpsClientResponse();
|
||||
callback?.Invoke(response);
|
||||
}
|
||||
|
||||
/// <summary>Dispatches HTTPS request synchronously</summary>
|
||||
/// <param name="request">HTTPS request</param>
|
||||
/// <returns>HTTPS response</returns>
|
||||
public HttpsClientResponse Dispatch(HttpsClientRequest request)
|
||||
{
|
||||
// Mock implementation - return empty response
|
||||
return new HttpsClientResponse();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTPS client request</summary>
|
||||
public class HttpsClientRequest
|
||||
{
|
||||
/// <summary>Gets or sets the URL parser</summary>
|
||||
public Crestron.SimplSharp.Net.Https.UrlParserResult Url { get; set; } = new Crestron.SimplSharp.Net.Https.UrlParserResult();
|
||||
|
||||
/// <summary>Gets or sets the HTTP method</summary>
|
||||
public RequestType RequestType { get; set; } = RequestType.Get;
|
||||
|
||||
/// <summary>Gets or sets the content data</summary>
|
||||
public string ContentString { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets the headers collection</summary>
|
||||
public HttpsHeaderCollection Header { get; } = new HttpsHeaderCollection();
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTPS client response</summary>
|
||||
public class HttpsClientResponse
|
||||
{
|
||||
/// <summary>Gets the response code</summary>
|
||||
public int Code { get; set; } = 200;
|
||||
|
||||
/// <summary>Gets the response content</summary>
|
||||
public string ContentString { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets the response data as bytes</summary>
|
||||
public byte[] ContentBytes { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>Gets the headers collection</summary>
|
||||
public HttpsHeaderCollection Header { get; } = new HttpsHeaderCollection();
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTPS header collection</summary>
|
||||
public class HttpsHeaderCollection
|
||||
{
|
||||
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
|
||||
|
||||
/// <summary>Gets or sets the content type</summary>
|
||||
public string ContentType
|
||||
{
|
||||
get => _headers.TryGetValue("Content-Type", out var value) ? value : string.Empty;
|
||||
set => _headers["Content-Type"] = value;
|
||||
}
|
||||
|
||||
/// <summary>Sets a header value</summary>
|
||||
/// <param name="name">Header name</param>
|
||||
/// <param name="value">Header value</param>
|
||||
public void SetHeaderValue(string name, string value)
|
||||
{
|
||||
_headers[name] = value;
|
||||
}
|
||||
|
||||
/// <summary>Adds a header</summary>
|
||||
/// <param name="header">Header to add</param>
|
||||
public void AddHeader(HttpsHeader header)
|
||||
{
|
||||
_headers[header.Name] = header.Value;
|
||||
}
|
||||
|
||||
/// <summary>Gets a header value</summary>
|
||||
/// <param name="name">Header name</param>
|
||||
/// <returns>Header value or empty string if not found</returns>
|
||||
public string GetHeaderValue(string name)
|
||||
{
|
||||
return _headers.TryGetValue(name, out var value) ? value : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTPS header</summary>
|
||||
public class HttpsHeader
|
||||
{
|
||||
/// <summary>Gets the header name</summary>
|
||||
public string Name { get; private set; }
|
||||
|
||||
/// <summary>Gets the header value</summary>
|
||||
public string Value { get; private set; }
|
||||
|
||||
/// <summary>Initializes a new instance of HttpsHeader</summary>
|
||||
/// <param name="name">Header name</param>
|
||||
/// <param name="value">Header value</param>
|
||||
public HttpsHeader(string name, string value)
|
||||
{
|
||||
Name = name ?? string.Empty;
|
||||
Value = value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Mock HTTP exception</summary>
|
||||
public class HttpException : Exception
|
||||
{
|
||||
/// <summary>Gets the HTTP response</summary>
|
||||
public HttpsClientResponse Response { get; }
|
||||
|
||||
/// <summary>Initializes a new instance of HttpException</summary>
|
||||
public HttpException() : base()
|
||||
{
|
||||
Response = new HttpsClientResponse();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of HttpException</summary>
|
||||
/// <param name="message">Exception message</param>
|
||||
public HttpException(string message) : base(message)
|
||||
{
|
||||
Response = new HttpsClientResponse();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of HttpException</summary>
|
||||
/// <param name="message">Exception message</param>
|
||||
/// <param name="innerException">Inner exception</param>
|
||||
public HttpException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
Response = new HttpsClientResponse();
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of HttpException</summary>
|
||||
/// <param name="message">Exception message</param>
|
||||
/// <param name="response">HTTP response</param>
|
||||
public HttpException(string message, HttpsClientResponse response) : base(message)
|
||||
{
|
||||
Response = response ?? new HttpsClientResponse();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
<Project>
|
||||
<ItemGroup>
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != ''">
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library'">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build;</PackagePath>
|
||||
</None>
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != '' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' )">
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program'">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build;</PackagePath>
|
||||
</None>
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != ''">
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary'">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build;</PackagePath>
|
||||
</None>
|
||||
@@ -23,32 +23,23 @@
|
||||
<FileName>$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz</FileName>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="DeleteCLZ" BeforeTargets="CoreBuild" Condition="$(ProjectType) == 'Library' And $(TargetDir) != ''">
|
||||
<ItemGroup>
|
||||
<OldCLZFiles Include="$(TargetDir)$(TargetName).*.$(TargetFramework).clz" />
|
||||
</ItemGroup>
|
||||
<Delete Files="@(OldCLZFiles)" Condition="@(OldCLZFiles) != ''">
|
||||
<Target Name="DeleteCLZ" BeforeTargets="PreBuildEvent" Condition="$(ProjectType) == 'Library' And $(TargetDir) != '' And Exists($(FileName))">
|
||||
<Delete Files="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz">
|
||||
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
|
||||
</Delete>
|
||||
<Message Text="Deleted old CLZ files: '@(DeletedList)'" Condition="@(DeletedList) != ''" />
|
||||
<Message Text="Deleted files: '@(DeletedList)'" />
|
||||
</Target>
|
||||
<Target Name="DeleteCPZ" BeforeTargets="CoreBuild" Condition="$(ProjectType) == 'Program' And $(TargetDir) != ''">
|
||||
<ItemGroup>
|
||||
<OldCPZFiles Include="$(TargetDir)$(TargetName).*.$(TargetFramework).cpz" />
|
||||
</ItemGroup>
|
||||
<Delete Files="@(OldCPZFiles)" Condition="@(OldCPZFiles) != ''">
|
||||
<Target Name="DeleteCPZ" BeforeTargets="PreBuildEvent" Condition="$(ProjectType) == 'Program' And $(TargetDir) != '' And Exists($(FileName))">
|
||||
<Delete Files="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz">
|
||||
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
|
||||
</Delete>
|
||||
<Message Text="Deleted old CPZ files: '@(DeletedList)'" Condition="@(DeletedList) != ''" />
|
||||
<Message Text="Deleted files: '@(DeletedList)'" />
|
||||
</Target>
|
||||
<Target Name="DeleteCPLZ" BeforeTargets="CoreBuild" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != ''">
|
||||
<ItemGroup>
|
||||
<OldCPLZFiles Include="$(TargetDir)$(TargetName).*.$(TargetFramework).cplz" />
|
||||
</ItemGroup>
|
||||
<Delete Files="@(OldCPLZFiles)" Condition="@(OldCPLZFiles) != ''">
|
||||
<Target Name="DeleteCPLZ" BeforeTargets="PreBuildEvent" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != '' And Exists($(FileName))">
|
||||
<Delete Files="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz">
|
||||
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
|
||||
</Delete>
|
||||
<Message Text="Deleted old CPLZ files: '@(DeletedList)'" Condition="@(DeletedList) != ''" />
|
||||
<Message Text="Deleted files: '@(DeletedList)'" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CreateCPLZ" AfterTargets="Build" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != ''" DependsOnTargets="DeleteCPLZ">
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<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>
|
||||
@@ -1,323 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
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 ____) { }
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<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>
|
||||
@@ -1,29 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -84,9 +84,10 @@ namespace PepperDash.Core;
|
||||
port.TextReceived += Port_TextReceivedStringDelimiter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop method
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Disconnects this gather from the Port's TextReceived event. This will not fire LineReceived
|
||||
/// after the this call.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
Port.TextReceived -= Port_TextReceived;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
@@ -20,7 +20,7 @@ public class CommunicationStreamDebugging
|
||||
/// <summary>
|
||||
/// Timer to disable automatically if not manually disabled
|
||||
/// </summary>
|
||||
private Timer DebugExpiryPeriod;
|
||||
private CTimer DebugExpiryPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// The current debug setting
|
||||
@@ -93,9 +93,7 @@ public class CommunicationStreamDebugging
|
||||
|
||||
StopDebugTimer();
|
||||
|
||||
DebugExpiryPeriod = new Timer(_DebugTimeoutInMs) { AutoReset = false };
|
||||
DebugExpiryPeriod.Elapsed += (s, e) => DisableDebugging();
|
||||
DebugExpiryPeriod.Start();
|
||||
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs);
|
||||
|
||||
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
|
||||
RxStreamDebuggingIsEnabled = true;
|
||||
@@ -157,3 +155,22 @@ public enum eStreamDebuggingSetting
|
||||
Both = Rx | Tx
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The available settings for stream debugging response types
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum eStreamDebuggingDataTypeSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug data in byte format
|
||||
/// </summary>
|
||||
Bytes = 0,
|
||||
/// <summary>
|
||||
/// Debug data in text format
|
||||
/// </summary>
|
||||
Text = 1,
|
||||
/// <summary>
|
||||
/// Debug data in both byte and text formats
|
||||
/// </summary>
|
||||
Both = Bytes | Text,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using Crestron.SimplSharp;
|
||||
using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute;
|
||||
using JsonIgnore = NewtonsoftJson::Newtonsoft.Json.JsonIgnoreAttribute;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using NullValueHandling = NewtonsoftJson::Newtonsoft.Json.NullValueHandling;
|
||||
using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
|
||||
@@ -27,25 +27,25 @@ public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client
|
||||
/// <summary>
|
||||
/// EventArgs class for socket status changes
|
||||
/// </summary>
|
||||
public class GenericSocketStatusChageEventArgs : EventArgs
|
||||
{
|
||||
public class GenericSocketStatusChageEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Client
|
||||
///
|
||||
/// </summary>
|
||||
public ISocketStatus Client { get; private set; }
|
||||
public ISocketStatus Client { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
public GenericSocketStatusChageEventArgs(ISocketStatus client)
|
||||
{
|
||||
Client = client;
|
||||
}
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericSocketStatusChageEventArgs() { }
|
||||
public GenericSocketStatusChageEventArgs(ISocketStatus client)
|
||||
{
|
||||
Client = client;
|
||||
}
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericSocketStatusChageEventArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -60,22 +60,22 @@ public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state
|
||||
public class GenericTcpServerStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the State
|
||||
///
|
||||
/// </summary>
|
||||
public ServerState State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
public GenericTcpServerStateChangedEventArgs(ServerState state)
|
||||
{
|
||||
State = state;
|
||||
}
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerStateChangedEventArgs() { }
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerStateChangedEventArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -91,22 +91,20 @@ public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object sock
|
||||
public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Socket
|
||||
///
|
||||
/// </summary>
|
||||
public object Socket { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the client from which the status change was received
|
||||
///
|
||||
/// </summary>
|
||||
public uint ReceivedFromClientIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ClientStatus
|
||||
///
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="clientStatus"></param>
|
||||
@@ -117,7 +115,7 @@ public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
@@ -128,10 +126,10 @@ public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
||||
ReceivedFromClientIndex = clientIndex;
|
||||
ClientStatus = clientStatus;
|
||||
}
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerSocketStatusChangeEventArgs() { }
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerSocketStatusChangeEventArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -140,28 +138,28 @@ public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
||||
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the client from which the text was received
|
||||
///
|
||||
/// </summary>
|
||||
public uint ReceivedFromClientIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the client from which the text was received as a ushort
|
||||
///
|
||||
/// </summary>
|
||||
public ushort ReceivedFromClientIndexShort
|
||||
{
|
||||
get
|
||||
{
|
||||
return (ushort)ReceivedFromClientIndex;
|
||||
}
|
||||
}
|
||||
public ushort ReceivedFromClientIndexShort
|
||||
{
|
||||
get
|
||||
{
|
||||
return (ushort)ReceivedFromClientIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Text
|
||||
///
|
||||
/// </summary>
|
||||
public string Text { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public GenericTcpServerCommMethodReceiveTextArgs(string text)
|
||||
@@ -170,7 +168,7 @@ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
@@ -179,10 +177,10 @@ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
||||
Text = text;
|
||||
ReceivedFromClientIndex = clientIndex;
|
||||
}
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerCommMethodReceiveTextArgs() { }
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerCommMethodReceiveTextArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -191,23 +189,22 @@ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
||||
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets IsReady
|
||||
///
|
||||
/// </summary>
|
||||
public bool IsReady;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="isReady"></param>
|
||||
public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
|
||||
{
|
||||
IsReady = isReady;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -216,12 +213,11 @@ public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
|
||||
public class GenericUdpConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the UConnected
|
||||
///
|
||||
/// </summary>
|
||||
public ushort UConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Connected status
|
||||
///
|
||||
/// </summary>
|
||||
public bool Connected;
|
||||
|
||||
@@ -231,7 +227,7 @@ public class GenericUdpConnectedEventArgs : EventArgs
|
||||
public GenericUdpConnectedEventArgs() { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="uconnected"></param>
|
||||
public GenericUdpConnectedEventArgs(ushort uconnected)
|
||||
@@ -240,7 +236,7 @@ public class GenericUdpConnectedEventArgs : EventArgs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="connected"></param>
|
||||
public GenericUdpConnectedEventArgs(bool connected)
|
||||
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
@@ -49,7 +48,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
/// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event.
|
||||
/// </summary>
|
||||
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
|
||||
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
|
||||
@@ -182,7 +181,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
|
||||
// private Timer for auto reconnect
|
||||
private Timer RetryTimer;
|
||||
private CTimer RetryTimer;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -265,12 +264,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
/// </summary>
|
||||
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
|
||||
|
||||
Timer HeartbeatSendTimer;
|
||||
Timer HeartbeatAckTimer;
|
||||
CTimer HeartbeatSendTimer;
|
||||
CTimer HeartbeatAckTimer;
|
||||
|
||||
// Used to force disconnection on a dead connect attempt
|
||||
Timer ConnectFailTimer;
|
||||
Timer WaitForSharedKey;
|
||||
CTimer ConnectFailTimer;
|
||||
CTimer WaitForSharedKey;
|
||||
private int ConnectionCount;
|
||||
|
||||
bool ProgramIsStopping;
|
||||
@@ -278,7 +277,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
/// <summary>
|
||||
/// Queue lock
|
||||
/// </summary>
|
||||
private readonly object _dequeueLock = new();
|
||||
CCriticalSection DequeueLock = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
|
||||
@@ -358,7 +357,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
this.LogWarning( "Could not initialize client with key: {0}", Key);
|
||||
Debug.Console(0, this, "Could not initialize client with key: {0}", Key);
|
||||
return;
|
||||
}
|
||||
try
|
||||
@@ -398,7 +397,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Exception initializing client with key: {0}\rException: {1}", Key, ex);
|
||||
Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,7 +410,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing _client connection");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection");
|
||||
ProgramIsStopping = true;
|
||||
Disconnect();
|
||||
}
|
||||
@@ -438,17 +437,17 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
public void Connect()
|
||||
{
|
||||
ConnectionCount++;
|
||||
this.LogVerbose("Attempting connect Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
|
||||
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Already connected. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
|
||||
return;
|
||||
}
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
this.LogInformation("Already trying to connect. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
@@ -461,17 +460,17 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No address set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: Invalid port");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No Shared Key set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -492,10 +491,9 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
|
||||
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
ConnectFailTimer = new Timer(30000) { AutoReset = false };
|
||||
ConnectFailTimer.Elapsed += (s, e) =>
|
||||
ConnectFailTimer = new CTimer(o =>
|
||||
{
|
||||
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
IsTryingToConnect = false;
|
||||
@@ -507,13 +505,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
//SecureClient.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
}
|
||||
};
|
||||
ConnectFailTimer.Start();
|
||||
}, 30000);
|
||||
|
||||
this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
|
||||
_client.ConnectToServerAsync(o =>
|
||||
{
|
||||
this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
|
||||
if (ConnectFailTimer != null)
|
||||
{
|
||||
@@ -523,22 +520,22 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
|
||||
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogVerbose("_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
o.ReceiveDataAsync(Receive);
|
||||
|
||||
if (SharedKeyRequired)
|
||||
{
|
||||
WaitingForSharedKeyResponse = true;
|
||||
WaitForSharedKey = new Timer(15000) { AutoReset = false };
|
||||
WaitForSharedKey.Elapsed += (s, e) =>
|
||||
WaitForSharedKey = new CTimer(timer =>
|
||||
{
|
||||
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
|
||||
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 is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
|
||||
o.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
//OnClientReadyForcommunications(false); // Should send false event
|
||||
};
|
||||
WaitForSharedKey.Start();
|
||||
}, 15000);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -552,14 +549,14 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
|
||||
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("_client connection exception: {0}", ex.Message);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message);
|
||||
IsTryingToConnect = false;
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
@@ -595,14 +592,14 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
if (_client == null) return;
|
||||
|
||||
this.LogInformation("Disconnecting client");
|
||||
Debug.Console(1, this, "Disconnecting client");
|
||||
if (IsConnected)
|
||||
_client.DisconnectFromServer();
|
||||
|
||||
// close up client. ALWAYS use this when disconnecting.
|
||||
IsTryingToConnect = false;
|
||||
|
||||
this.LogVerbose("Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
_client.SocketStatusChange -= Client_SocketStatusChange;
|
||||
_client.Dispose();
|
||||
_client = null;
|
||||
@@ -612,7 +609,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
ConnectFailTimer.Dispose();
|
||||
ConnectFailTimer = null;
|
||||
}
|
||||
|
||||
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
@@ -623,14 +620,14 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
if (_client != null)
|
||||
{
|
||||
this.LogVerbose("Cleaning up remotely closed/failed connection.");
|
||||
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
|
||||
Disconnect();
|
||||
}
|
||||
if (!DisconnectCalledByUser && AutoReconnect)
|
||||
{
|
||||
var halfInterval = AutoReconnectIntervalMs / 2;
|
||||
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
|
||||
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
if (RetryTimer != null)
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
@@ -638,9 +635,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
if (AutoReconnectTriggered != null)
|
||||
AutoReconnectTriggered(this, new EventArgs());
|
||||
RetryTimer = new Timer(rndTime) { AutoReset = false };
|
||||
RetryTimer.Elapsed += (s, e) => Connect();
|
||||
RetryTimer.Start();
|
||||
RetryTimer = new CTimer(o => Connect(), rndTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,13 +654,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
|
||||
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.LogVerbose("_client Received:\r--------\r{0}\r--------", str);
|
||||
Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str);
|
||||
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
|
||||
{
|
||||
|
||||
if (SharedKeyRequired && str == "SharedKey:")
|
||||
{
|
||||
this.LogVerbose("Server asking for shared key, sending");
|
||||
Debug.Console(2, this, "Server asking for shared key, sending");
|
||||
SendText(SharedKey + "\n");
|
||||
}
|
||||
else if (SharedKeyRequired && str == "Shared Key Match")
|
||||
@@ -673,7 +668,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
StopWaitForSharedKeyTimer();
|
||||
|
||||
|
||||
this.LogVerbose("Shared key confirmed. Ready for communication");
|
||||
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
|
||||
OnClientReadyForcommunications(true); // Successful key exchange
|
||||
}
|
||||
else
|
||||
@@ -693,7 +688,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error receiving data: {1}. Error: {0}", str, ex.Message);
|
||||
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
}
|
||||
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
client.ReceiveDataAsync(Receive);
|
||||
@@ -701,18 +696,19 @@ 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 (System.Threading.Monitor.TryEnter(_dequeueLock))
|
||||
System.Threading.Tasks.Task.Run(() => DequeueEvent());
|
||||
var gotLock = DequeueLock.TryEnter();
|
||||
if (gotLock)
|
||||
CrestronInvoke.BeginInvoke((o) => 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
|
||||
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
|
||||
/// It will dequeue items as they are enqueued automatically.
|
||||
/// </summary>
|
||||
void DequeueEvent()
|
||||
@@ -732,10 +728,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogError(e, "DequeueEvent error: {0}", e.Message);
|
||||
this.LogException(e, "DequeueEvent error");
|
||||
}
|
||||
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
|
||||
if (DequeueLock != null)
|
||||
{
|
||||
DequeueLock.Leave();
|
||||
}
|
||||
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
|
||||
System.Threading.Monitor.Exit(_dequeueLock);
|
||||
}
|
||||
|
||||
void HeartbeatStart()
|
||||
@@ -746,15 +745,11 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
if (HeartbeatSendTimer == null)
|
||||
{
|
||||
|
||||
HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
|
||||
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
|
||||
HeartbeatSendTimer.Start();
|
||||
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
|
||||
}
|
||||
if (HeartbeatAckTimer == null)
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -764,13 +759,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
|
||||
if (HeartbeatSendTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Send");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Send");
|
||||
HeartbeatSendTimer.Stop();
|
||||
HeartbeatSendTimer = null;
|
||||
}
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Ack");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Ack");
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer = null;
|
||||
}
|
||||
@@ -779,7 +774,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
void SendHeartbeat(object notused)
|
||||
{
|
||||
this.SendText(HeartbeatString);
|
||||
this.LogVerbose("Sending Heartbeat");
|
||||
Debug.Console(2, this, "Sending Heartbeat");
|
||||
|
||||
}
|
||||
|
||||
@@ -798,17 +793,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
return remainingText;
|
||||
}
|
||||
}
|
||||
@@ -816,7 +807,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
@@ -830,7 +821,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
SendText("Heartbeat not received by server, closing connection");
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
@@ -838,7 +829,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Heartbeat timeout Error on _client: {0}, {1}", Key, ex.Message);
|
||||
ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -871,14 +862,14 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
|
||||
if (n <= 0)
|
||||
{
|
||||
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -897,7 +888,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error sending bytes. Error: {0}", ex.Message);
|
||||
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -907,7 +898,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="clientSocketStatus"></param>
|
||||
void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus)
|
||||
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
|
||||
{
|
||||
if (ProgramIsStopping)
|
||||
{
|
||||
@@ -916,7 +907,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
try
|
||||
{
|
||||
this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
|
||||
OnConnectionChange();
|
||||
// The client could be null or disposed by this time...
|
||||
@@ -929,7 +920,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,12 +941,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
||||
void OnClientReadyForcommunications(bool isReady)
|
||||
{
|
||||
IsReadyForCommunication = isReady;
|
||||
if (IsReadyForCommunication)
|
||||
if (IsReadyForCommunication)
|
||||
HeartbeatStart();
|
||||
|
||||
var handler = ClientReadyForCommunications;
|
||||
if (handler == null) return;
|
||||
|
||||
|
||||
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -15,9 +15,6 @@ 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;
|
||||
@@ -83,7 +80,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
public string Hostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The port number on which the server is listening.
|
||||
/// Port on server
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
@@ -116,7 +113,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharedKey is sent for verification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
|
||||
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
@@ -224,7 +221,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
/// <summary>
|
||||
/// private Timer for auto reconnect
|
||||
/// </summary>
|
||||
System.Timers.Timer RetryTimer;
|
||||
CTimer RetryTimer;
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -256,13 +253,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
/// </summary>
|
||||
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
|
||||
|
||||
System.Timers.Timer HeartbeatSendTimer;
|
||||
System.Timers.Timer HeartbeatAckTimer;
|
||||
CTimer HeartbeatSendTimer;
|
||||
CTimer HeartbeatAckTimer;
|
||||
/// <summary>
|
||||
/// Used to force disconnection on a dead connect attempt
|
||||
/// </summary>
|
||||
System.Timers.Timer ConnectFailTimer;
|
||||
System.Timers.Timer WaitForSharedKey;
|
||||
CTimer ConnectFailTimer;
|
||||
CTimer WaitForSharedKey;
|
||||
private int ConnectionCount;
|
||||
/// <summary>
|
||||
/// Internal secure client
|
||||
@@ -274,7 +271,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
/// <summary>
|
||||
/// Queue lock
|
||||
/// </summary>
|
||||
private readonly object _dequeueLock = new();
|
||||
CCriticalSection DequeueLock = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
|
||||
@@ -339,9 +336,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the client's key property, which is used to identify this client instance.
|
||||
/// Just to help S+ set the key
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key that identifies this client instance.</param>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
Key = key;
|
||||
@@ -350,7 +346,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
/// <summary>
|
||||
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
|
||||
/// </summary>
|
||||
/// <param name="clientConfigObject">The configuration object containing the client's settings.</param>
|
||||
/// <param name="clientConfigObject"></param>
|
||||
public void Initialize(TcpClientConfigObject clientConfigObject)
|
||||
{
|
||||
try
|
||||
@@ -365,7 +361,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
SharedKey = clientConfigObject.SharedKey;
|
||||
SharedKeyRequired = clientConfigObject.SharedKeyRequired;
|
||||
HeartbeatEnabled = clientConfigObject.HeartbeatRequired;
|
||||
HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ?
|
||||
HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ?
|
||||
clientConfigObject.HeartbeatRequiredIntervalInSeconds : (ushort)15;
|
||||
HeartbeatString = string.IsNullOrEmpty(clientConfigObject.HeartbeatStringToMatch) ? "heartbeat" : clientConfigObject.HeartbeatStringToMatch;
|
||||
Port = TcpSshProperties.Port;
|
||||
@@ -391,7 +387,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing Client connection");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
|
||||
ProgramIsStopping = true;
|
||||
Disconnect();
|
||||
}
|
||||
@@ -404,17 +400,17 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
public void Connect()
|
||||
{
|
||||
ConnectionCount++;
|
||||
this.LogVerbose("Attempting connect Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
|
||||
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Already connected. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
|
||||
return;
|
||||
}
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
this.LogInformation("Already trying to connect. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
@@ -427,17 +423,17 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No address set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: Invalid port");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No Shared Key set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -458,10 +454,9 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
ConnectFailTimer = new System.Timers.Timer(30000) { AutoReset = false };
|
||||
ConnectFailTimer.Elapsed += (s, e) =>
|
||||
ConnectFailTimer = new CTimer(o =>
|
||||
{
|
||||
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
IsTryingToConnect = false;
|
||||
@@ -473,13 +468,12 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
//SecureClient.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
}
|
||||
};
|
||||
ConnectFailTimer.Start();
|
||||
}, 30000);
|
||||
|
||||
this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
|
||||
Client.ConnectToServerAsync(o =>
|
||||
{
|
||||
this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
|
||||
if (ConnectFailTimer != null)
|
||||
{
|
||||
@@ -489,22 +483,22 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
o.ReceiveDataAsync(Receive);
|
||||
|
||||
if (SharedKeyRequired)
|
||||
{
|
||||
WaitingForSharedKeyResponse = true;
|
||||
WaitForSharedKey = new System.Timers.Timer(15000) { AutoReset = false };
|
||||
WaitForSharedKey.Elapsed += (s, e) =>
|
||||
WaitForSharedKey = new CTimer(timer =>
|
||||
{
|
||||
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
|
||||
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 is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
|
||||
o.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
//OnClientReadyForcommunications(false); // Should send false event
|
||||
};
|
||||
WaitForSharedKey.Start();
|
||||
}, 15000);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -518,14 +512,14 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
|
||||
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Client connection exception: {0}", ex.Message);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
|
||||
IsTryingToConnect = false;
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
@@ -562,7 +556,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
if (Client != null)
|
||||
{
|
||||
//SecureClient.DisconnectFromServer();
|
||||
this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Client.SocketStatusChange -= Client_SocketStatusChange;
|
||||
Client.Dispose();
|
||||
Client = null;
|
||||
@@ -584,24 +578,22 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
this.LogVerbose("Cleaning up remotely closed/failed connection.");
|
||||
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
|
||||
Cleanup();
|
||||
}
|
||||
if (!DisconnectCalledByUser && AutoReconnect)
|
||||
{
|
||||
var halfInterval = AutoReconnectIntervalMs / 2;
|
||||
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
|
||||
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
if (RetryTimer != null)
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
RetryTimer = null;
|
||||
}
|
||||
if (AutoReconnectTriggered != null)
|
||||
if(AutoReconnectTriggered != null)
|
||||
AutoReconnectTriggered(this, new EventArgs());
|
||||
RetryTimer = new System.Timers.Timer(rndTime) { AutoReset = false };
|
||||
RetryTimer.Elapsed += (s, e) => Connect();
|
||||
RetryTimer.Start();
|
||||
RetryTimer = new CTimer(o => Connect(), rndTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,13 +612,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
|
||||
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
|
||||
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
|
||||
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
|
||||
{
|
||||
|
||||
if (SharedKeyRequired && str == "SharedKey:")
|
||||
{
|
||||
this.LogVerbose("Server asking for shared key, sending");
|
||||
Debug.Console(2, this, "Server asking for shared key, sending");
|
||||
SendText(SharedKey + "\n");
|
||||
}
|
||||
else if (SharedKeyRequired && str == "Shared Key Match")
|
||||
@@ -634,7 +626,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
StopWaitForSharedKeyTimer();
|
||||
|
||||
|
||||
this.LogVerbose("Shared key confirmed. Ready for communication");
|
||||
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
|
||||
OnClientReadyForcommunications(true); // Successful key exchange
|
||||
}
|
||||
else
|
||||
@@ -654,7 +646,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
}
|
||||
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
client.ReceiveDataAsync(Receive);
|
||||
@@ -662,8 +654,9 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
//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());
|
||||
var gotLock = DequeueLock.TryEnter();
|
||||
if (gotLock)
|
||||
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
|
||||
}
|
||||
}
|
||||
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
|
||||
@@ -673,7 +666,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
|
||||
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
|
||||
/// It will dequeue items as they are enqueued automatically.
|
||||
/// </summary>
|
||||
void DequeueEvent()
|
||||
@@ -693,29 +686,28 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogError("DequeueEvent error: {0}", e.Message, e);
|
||||
this.LogException(e, "DequeueEvent error");
|
||||
}
|
||||
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
|
||||
if (DequeueLock != null)
|
||||
{
|
||||
DequeueLock.Leave();
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
void HeartbeatStart()
|
||||
{
|
||||
if (HeartbeatEnabled)
|
||||
{
|
||||
this.LogVerbose("Starting Heartbeat");
|
||||
Debug.Console(2, this, "Starting Heartbeat");
|
||||
if (HeartbeatSendTimer == null)
|
||||
{
|
||||
|
||||
HeartbeatSendTimer = new System.Timers.Timer(HeartbeatInterval) { AutoReset = true };
|
||||
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
|
||||
HeartbeatSendTimer.Start();
|
||||
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
|
||||
}
|
||||
if (HeartbeatAckTimer == null)
|
||||
{
|
||||
HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,13 +717,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
if (HeartbeatSendTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stopping Heartbeat Send");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Send");
|
||||
HeartbeatSendTimer.Stop();
|
||||
HeartbeatSendTimer = null;
|
||||
}
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stopping Heartbeat Ack");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Ack");
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer = null;
|
||||
}
|
||||
@@ -740,7 +732,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
void SendHeartbeat(object notused)
|
||||
{
|
||||
this.SendText(HeartbeatString);
|
||||
this.LogVerbose("Sending Heartbeat");
|
||||
Debug.Console(2, this, "Sending Heartbeat");
|
||||
|
||||
}
|
||||
|
||||
@@ -759,17 +751,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
return remainingText;
|
||||
}
|
||||
}
|
||||
@@ -777,7 +765,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error checking heartbeat: {0}", ex.Message, ex);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
@@ -791,7 +779,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
SendText("Heartbeat not received by server, closing connection");
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
@@ -799,7 +787,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
|
||||
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,14 +820,14 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
|
||||
if (n <= 0)
|
||||
{
|
||||
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error sending text: {1}. Error: {0}", text, ex.Message);
|
||||
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -858,7 +846,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error sending bytes. Error: {0}", ex.Message, ex);
|
||||
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -877,7 +865,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
try
|
||||
{
|
||||
this.LogDebug("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
|
||||
OnConnectionChange();
|
||||
// The client could be null or disposed by this time...
|
||||
@@ -890,7 +878,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
using System;
|
||||
/*PepperDash Technology Corp.
|
||||
JAG
|
||||
Copyright: 2017
|
||||
------------------------------------
|
||||
***Notice of Ownership and Copyright***
|
||||
The material in which this notice appears is the property of PepperDash Technology Corporation,
|
||||
which claims copyright under the laws of the United States of America in the entire body of material
|
||||
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
|
||||
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
|
||||
PepperDash Technology Corporation reserves all rights under applicable laws.
|
||||
------------------------------------ */
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
@@ -59,17 +69,12 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// <summary>
|
||||
/// Server listen lock
|
||||
/// </summary>
|
||||
private readonly object _serverLock = new();
|
||||
CCriticalSection ServerCCSection = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Queue lock
|
||||
/// </summary>
|
||||
private readonly object _dequeueLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast lock
|
||||
/// </summary>
|
||||
private readonly object _broadcastLock = new();
|
||||
CCriticalSection DequeueLock = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
|
||||
@@ -91,7 +96,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// <summary>
|
||||
/// Timer to operate the bandaid monitor client in a loop.
|
||||
/// </summary>
|
||||
Timer MonitorClientTimer;
|
||||
CTimer MonitorClientTimer;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -258,7 +263,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
public string HeartbeatStringToMatch { get; set; }
|
||||
|
||||
//private timers for Heartbeats per client
|
||||
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
|
||||
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
|
||||
|
||||
//flags to show the secure server is waiting for client at index to send the shared key
|
||||
List<uint> WaitingForSharedKey = new List<uint>();
|
||||
@@ -394,57 +399,57 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// </summary>
|
||||
public void Listen()
|
||||
{
|
||||
lock (_serverLock)
|
||||
{
|
||||
ServerCCSection.Enter();
|
||||
try
|
||||
{
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogError("Server '{0}': Invalid port", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogError("Server '{0}': No Shared Key set", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (SecureServer == null)
|
||||
{
|
||||
SecureServer = new SecureTCPServer(Port, MaxClients);
|
||||
if (HeartbeatRequired)
|
||||
SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5);
|
||||
SecureServer.HandshakeTimeout = 30;
|
||||
SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange);
|
||||
}
|
||||
else
|
||||
{
|
||||
SecureServer.PortNumber = Port;
|
||||
}
|
||||
ServerStopped = false;
|
||||
if (SecureServer == null)
|
||||
{
|
||||
SecureServer = new SecureTCPServer(Port, MaxClients);
|
||||
if (HeartbeatRequired)
|
||||
SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5);
|
||||
SecureServer.HandshakeTimeout = 30;
|
||||
SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange);
|
||||
}
|
||||
else
|
||||
{
|
||||
SecureServer.PortNumber = Port;
|
||||
}
|
||||
ServerStopped = false;
|
||||
|
||||
// Start the listner
|
||||
SocketErrorCodes status = SecureServer.WaitForConnectionAsync("0.0.0.0", SecureConnectCallback);
|
||||
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
{
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error starting WaitForConnectionAsync {0}", status);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerStopped = false;
|
||||
}
|
||||
OnServerStateChange(SecureServer.State);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
|
||||
ServerCCSection.Leave();
|
||||
|
||||
// Start the listner
|
||||
SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
|
||||
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
{
|
||||
this.LogError("Error starting WaitForConnectionAsync {0}", status);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerStopped = false;
|
||||
}
|
||||
OnServerStateChange(SecureServer.State);
|
||||
this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
|
||||
ServerCCSection.Leave();
|
||||
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -452,21 +457,21 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// </summary>
|
||||
public void StopListening()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogVerbose("Stopping Listener");
|
||||
if (SecureServer != null)
|
||||
{
|
||||
SecureServer.Stop();
|
||||
this.LogVerbose("Server State: {0}", SecureServer.State);
|
||||
OnServerStateChange(SecureServer.State);
|
||||
}
|
||||
ServerStopped = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
|
||||
}
|
||||
try
|
||||
{
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
|
||||
if (SecureServer != null)
|
||||
{
|
||||
SecureServer.Stop();
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State);
|
||||
OnServerStateChange(SecureServer.State);
|
||||
}
|
||||
ServerStopped = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -478,11 +483,11 @@ public class GenericSecureTcpIpServer : Device
|
||||
try
|
||||
{
|
||||
SecureServer.Disconnect(client);
|
||||
this.LogVerbose("Disconnected client index: {0}", client);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
@@ -490,7 +495,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// </summary>
|
||||
public void DisconnectAllClientsForShutdown()
|
||||
{
|
||||
this.LogInformation("Disconnecting All Clients");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
|
||||
if (SecureServer != null)
|
||||
{
|
||||
SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
|
||||
@@ -502,17 +507,17 @@ public class GenericSecureTcpIpServer : Device
|
||||
try
|
||||
{
|
||||
SecureServer.Disconnect(i);
|
||||
this.LogInformation("Disconnected client index: {0}", i);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex);
|
||||
}
|
||||
}
|
||||
this.LogInformation("Server Status: {0}", SecureServer.ServerSocketStatus);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus);
|
||||
}
|
||||
|
||||
this.LogInformation("Disconnected All Clients");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients");
|
||||
ConnectedClientsIndexes.Clear();
|
||||
|
||||
if (!ProgramIsStopping)
|
||||
@@ -521,7 +526,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
OnServerStateChange(SecureServer.State); //State shows both listening and connected
|
||||
}
|
||||
|
||||
// var o = new { };
|
||||
// var o = new { };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -530,8 +535,8 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// <param name="text"></param>
|
||||
public void BroadcastText(string text)
|
||||
{
|
||||
lock (_broadcastLock)
|
||||
{
|
||||
CCriticalSection CCBroadcast = new CCriticalSection();
|
||||
CCBroadcast.Enter();
|
||||
try
|
||||
{
|
||||
if (ConnectedClientsIndexes.Count > 0)
|
||||
@@ -547,12 +552,13 @@ public class GenericSecureTcpIpServer : Device
|
||||
}
|
||||
}
|
||||
}
|
||||
CCBroadcast.Leave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
CCBroadcast.Leave();
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -573,7 +579,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,19 +597,13 @@ public class GenericSecureTcpIpServer : Device
|
||||
if (noDelimiter.Contains(HeartbeatStringToMatch))
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
// Return Heartbeat
|
||||
SendTextToClient(HeartbeatStringToMatch, clientIndex);
|
||||
return remainingText;
|
||||
@@ -612,25 +612,19 @@ public class GenericSecureTcpIpServer : Device
|
||||
else
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
@@ -642,11 +636,11 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// <returns></returns>
|
||||
public string GetClientIPAddress(uint clientIndex)
|
||||
{
|
||||
this.LogInformation("GetClientIPAddress Index: {0}", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex);
|
||||
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
|
||||
{
|
||||
var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
this.LogInformation("GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
return ipa;
|
||||
|
||||
}
|
||||
@@ -669,13 +663,14 @@ public class GenericSecureTcpIpServer : Device
|
||||
clientIndex = (uint)o;
|
||||
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
|
||||
this.LogInformation("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
|
||||
|
||||
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
|
||||
|
||||
var discoResult = SecureServer.Disconnect(clientIndex);
|
||||
//Debug.Console(1, this, "{0}", discoResult);
|
||||
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
@@ -697,43 +692,29 @@ public class GenericSecureTcpIpServer : Device
|
||||
/// Secure Server Socket Status Changed Callback
|
||||
/// </summary>
|
||||
/// <param name="server"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <param name="serverSocketStatus"></param>
|
||||
void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
|
||||
/// <param name="clientIndex">Index of the client whose socket status has changed</param>
|
||||
/// <param name="status">New socket status</param>
|
||||
void SecureServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedClients: {0} ServerState: {1} Port: {2} ClientIndex: {3} Status: {4}",
|
||||
server.NumberOfClientsConnected, server.State, server.PortNumber, clientIndex, status);
|
||||
|
||||
// Handle connection limit and listening state
|
||||
if (server.MaxNumberOfClientSupported > server.NumberOfClientsConnected)
|
||||
{
|
||||
this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
|
||||
|
||||
if (ConnectedClientsIndexes.Contains(clientIndex))
|
||||
ConnectedClientsIndexes.Remove(clientIndex);
|
||||
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Dispose();
|
||||
HeartbeatTimerDictionary.Remove(clientIndex);
|
||||
}
|
||||
if (ClientReadyAfterKeyExchange.Contains(clientIndex))
|
||||
ClientReadyAfterKeyExchange.Remove(clientIndex);
|
||||
if (WaitingForSharedKey.Contains(clientIndex))
|
||||
WaitingForSharedKey.Remove(clientIndex);
|
||||
if (SecureServer.MaxNumberOfClientSupported > SecureServer.NumberOfClientsConnected)
|
||||
{
|
||||
Listen();
|
||||
}
|
||||
Listen();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex);
|
||||
}
|
||||
//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.
|
||||
System.Threading.Tasks.Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
|
||||
CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -748,7 +729,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogInformation("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
|
||||
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
|
||||
if (clientIndex != 0)
|
||||
@@ -768,7 +749,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
}
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
|
||||
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
|
||||
this.LogInformation("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -778,10 +759,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
{
|
||||
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,27 +768,27 @@ public class GenericSecureTcpIpServer : Device
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Client attempt faulty.");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex);
|
||||
}
|
||||
|
||||
// Rearm the listner
|
||||
SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
|
||||
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
{
|
||||
this.LogError("Socket status connect callback status {0}", status);
|
||||
if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
|
||||
{
|
||||
// There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
|
||||
server.Stop();
|
||||
Listen();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rearm the listner
|
||||
SocketErrorCodes status = server.WaitForConnectionAsync("0.0.0.0", SecureConnectCallback);
|
||||
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
{
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Socket status connect callback status {0}", status);
|
||||
if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
|
||||
{
|
||||
// There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
|
||||
server.Stop();
|
||||
Listen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -825,7 +803,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
{
|
||||
if (numberOfBytesReceived > 0)
|
||||
{
|
||||
|
||||
|
||||
string received = "Nothing";
|
||||
var handler = TextReceivedQueueInvoke;
|
||||
try
|
||||
@@ -839,10 +817,10 @@ public class GenericSecureTcpIpServer : Device
|
||||
if (received != SharedKey)
|
||||
{
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
|
||||
this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
mySecureTCPServer.SendData(clientIndex, b, b.Length);
|
||||
mySecureTCPServer.Disconnect(clientIndex);
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -850,7 +828,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
|
||||
mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
this.LogInformation("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
|
||||
{
|
||||
@@ -863,26 +841,27 @@ public class GenericSecureTcpIpServer : Device
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
|
||||
}
|
||||
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
|
||||
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
|
||||
|
||||
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
|
||||
if (handler != null)
|
||||
{
|
||||
if (System.Threading.Monitor.TryEnter(_dequeueLock))
|
||||
System.Threading.Tasks.Task.Run(() => DequeueEvent());
|
||||
var gotLock = DequeueLock.TryEnter();
|
||||
if (gotLock)
|
||||
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mySecureTCPServer.Disconnect(clientIndex);
|
||||
else
|
||||
{
|
||||
mySecureTCPServer.Disconnect(clientIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
|
||||
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
|
||||
/// It will dequeue items as they are enqueued automatically.
|
||||
/// </summary>
|
||||
void DequeueEvent()
|
||||
@@ -902,10 +881,13 @@ public class GenericSecureTcpIpServer : Device
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogError(e, "DequeueEvent error");
|
||||
this.LogException(e, "DequeueEvent error");
|
||||
}
|
||||
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
|
||||
if (DequeueLock != null)
|
||||
{
|
||||
DequeueLock.Leave();
|
||||
}
|
||||
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
|
||||
System.Threading.Monitor.Exit(_dequeueLock);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -976,7 +958,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
if (MonitorClient != null)
|
||||
MonitorClient.Disconnect();
|
||||
|
||||
this.LogInformation("Program stopping. Closing server");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
|
||||
KillServer();
|
||||
}
|
||||
}
|
||||
@@ -1002,9 +984,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
{
|
||||
return;
|
||||
}
|
||||
MonitorClientTimer = new Timer(60000) { AutoReset = false };
|
||||
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
|
||||
MonitorClientTimer.Start();
|
||||
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1019,7 +999,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
|
||||
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
|
||||
|
||||
this.LogInformation("Starting monitor check");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
|
||||
|
||||
MonitorClient.Connect();
|
||||
// From here MonitorCLient either connects or hangs, MonitorClient will call back
|
||||
@@ -1046,7 +1026,7 @@ public class GenericSecureTcpIpServer : Device
|
||||
{
|
||||
if (args.IsReady)
|
||||
{
|
||||
this.LogInformation("Monitor client connection success. Disconnecting in 2s");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s");
|
||||
MonitorClientTimer.Stop();
|
||||
MonitorClientTimer = null;
|
||||
MonitorClientFailureCount = 0;
|
||||
@@ -1067,13 +1047,13 @@ public class GenericSecureTcpIpServer : Device
|
||||
StopMonitorClient();
|
||||
if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
|
||||
{
|
||||
this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
|
||||
StartMonitorClient();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError(
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error,
|
||||
"\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************",
|
||||
MonitorClientMaxFailureCount);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using System.Threading;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
using PepperDash.Core.Logging;
|
||||
using Renci.SshNet;
|
||||
using Renci.SshNet.Common;
|
||||
@@ -78,7 +79,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSH Client
|
||||
///
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus
|
||||
{
|
||||
@@ -131,9 +132,11 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
|
||||
ShellStream TheStream;
|
||||
|
||||
Timer ReconnectTimer;
|
||||
CTimer ReconnectTimer;
|
||||
|
||||
private System.Threading.SemaphoreSlim connectLock = new System.Threading.SemaphoreSlim(1);
|
||||
//Lock object to prevent simulatneous connect/disconnect operations
|
||||
//private CCriticalSection connectLock = new CCriticalSection();
|
||||
private SemaphoreSlim connectLock = new SemaphoreSlim(1);
|
||||
|
||||
private bool DisconnectLogged = false;
|
||||
|
||||
@@ -144,53 +147,51 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
base(key)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
Key = key;
|
||||
Hostname = hostname;
|
||||
Port = port;
|
||||
Username = username;
|
||||
Password = password;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
Key = key;
|
||||
Hostname = hostname;
|
||||
Port = port;
|
||||
Username = username;
|
||||
Password = password;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
|
||||
ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
|
||||
ReconnectTimer.Elapsed += (s, e) =>
|
||||
ReconnectTimer = new CTimer(o =>
|
||||
{
|
||||
if (ConnectEnabled)
|
||||
{
|
||||
if (ConnectEnabled)
|
||||
{
|
||||
Connect();
|
||||
}
|
||||
};
|
||||
}
|
||||
Connect();
|
||||
}
|
||||
}, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S+ Constructor - Must set all properties before calling Connect
|
||||
/// </summary>
|
||||
public GenericSshClient()
|
||||
: base(SPlusKey)
|
||||
{
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
/// <summary>
|
||||
/// S+ Constructor - Must set all properties before calling Connect
|
||||
/// </summary>
|
||||
public GenericSshClient()
|
||||
: base(SPlusKey)
|
||||
{
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
|
||||
ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
|
||||
ReconnectTimer.Elapsed += (s, e) =>
|
||||
ReconnectTimer = new CTimer(o =>
|
||||
{
|
||||
if (ConnectEnabled)
|
||||
{
|
||||
Connect();
|
||||
}
|
||||
};
|
||||
}
|
||||
}, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles closing this up when the program shuts down
|
||||
/// </summary>
|
||||
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
this.LogDebug("Program stopping. Closing connection");
|
||||
/// <summary>
|
||||
/// Handles closing this up when the program shuts down
|
||||
/// </summary>
|
||||
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
this.LogDebug("Program stopping. Closing connection");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
@@ -223,10 +224,10 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
this.LogDebug("Attempting connect");
|
||||
|
||||
// Cancel reconnect if running.
|
||||
if (ReconnectTimer != null)
|
||||
{
|
||||
ReconnectTimer.Stop();
|
||||
}
|
||||
if (ReconnectTimer != null)
|
||||
{
|
||||
ReconnectTimer.Stop();
|
||||
}
|
||||
|
||||
// Cleanup the old client if it already exists
|
||||
if (Client != null)
|
||||
@@ -264,10 +265,11 @@ 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)
|
||||
{
|
||||
this.LogException(ie, "CONNECTION failure: Cannot reach host");
|
||||
this.LogException(ie, "CONNECTION failure: Cannot reach host");
|
||||
}
|
||||
|
||||
if (ie is System.Net.Sockets.SocketException socketException)
|
||||
@@ -287,12 +289,10 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
if (AutoReconnect)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
ReconnectTimer.Reset(AutoReconnectIntervalMs);
|
||||
}
|
||||
}
|
||||
catch (SshOperationTimeoutException ex)
|
||||
catch(SshOperationTimeoutException ex)
|
||||
{
|
||||
this.LogWarning("Connection attempt timed out: {message}", ex.Message);
|
||||
|
||||
@@ -301,22 +301,19 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
if (AutoReconnect)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
ReconnectTimer.Reset(AutoReconnectIntervalMs);
|
||||
}
|
||||
}
|
||||
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.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
ReconnectTimer.Reset(AutoReconnectIntervalMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,14 +324,18 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
ConnectEnabled = false;
|
||||
// Stop trying reconnects, if we are
|
||||
ReconnectTimer.Stop();
|
||||
/// <summary>
|
||||
/// Disconnect the clients and put away it's resources.
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
ConnectEnabled = false;
|
||||
// Stop trying reconnects, if we are
|
||||
if (ReconnectTimer != null)
|
||||
{
|
||||
ReconnectTimer.Stop();
|
||||
// ReconnectTimer = null;
|
||||
}
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
|
||||
}
|
||||
@@ -360,15 +361,15 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Exception in Kill Client");
|
||||
this.LogException(ex,"Exception in Kill Client");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the stream
|
||||
/// </summary>
|
||||
void KillStream()
|
||||
{
|
||||
void KillStream()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (TheStream != null)
|
||||
@@ -384,60 +385,60 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
{
|
||||
this.LogException(ex, "Exception in Kill Stream:{0}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the keyboard interactive authentication, should it be required.
|
||||
/// </summary>
|
||||
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
|
||||
{
|
||||
foreach (AuthenticationPrompt prompt in e.Prompts)
|
||||
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
|
||||
prompt.Response = Password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
|
||||
/// </summary>
|
||||
void Stream_DataReceived(object sender, ShellDataEventArgs e)
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the keyboard interactive authentication, should it be required.
|
||||
/// </summary>
|
||||
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
|
||||
{
|
||||
foreach (AuthenticationPrompt prompt in e.Prompts)
|
||||
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
|
||||
prompt.Response = Password;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
|
||||
/// </summary>
|
||||
void Stream_DataReceived(object sender, ShellDataEventArgs e)
|
||||
{
|
||||
if (((ShellStream)sender).Length <= 0L)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var response = ((ShellStream)sender).Read();
|
||||
var response = ((ShellStream)sender).Read();
|
||||
|
||||
var bytesHandler = BytesReceived;
|
||||
|
||||
if (bytesHandler != null)
|
||||
{
|
||||
var bytesHandler = BytesReceived;
|
||||
|
||||
if (bytesHandler != null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(response);
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
}
|
||||
|
||||
var textHandler = TextReceived;
|
||||
if (textHandler != null)
|
||||
{
|
||||
}
|
||||
|
||||
var textHandler = TextReceived;
|
||||
if (textHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response));
|
||||
|
||||
textHandler(this, new GenericCommMethodReceiveTextArgs(response));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
|
||||
/// event
|
||||
/// </summary>
|
||||
void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
|
||||
{
|
||||
System.Threading.Tasks.Task.Run(() =>
|
||||
/// <summary>
|
||||
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
|
||||
/// event
|
||||
/// </summary>
|
||||
void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
|
||||
{
|
||||
CrestronInvoke.BeginInvoke(o =>
|
||||
{
|
||||
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
|
||||
this.LogError("Disconnected by remote");
|
||||
@@ -455,9 +456,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
if (AutoReconnect && ConnectEnabled)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
ReconnectTimer.Reset(AutoReconnectIntervalMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -473,50 +472,49 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
|
||||
#region IBasicCommunication Members
|
||||
|
||||
/// <summary>
|
||||
/// Sends text to the server
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public void SendText(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Client != null && TheStream != null && IsConnected)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation(
|
||||
"Sending {length} characters of text: '{text}'",
|
||||
text.Length,
|
||||
ComTextHelper.GetDebugText(text));
|
||||
/// <summary>
|
||||
/// Sends text to the server
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public void SendText(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Client != null && TheStream != null && IsConnected)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation(
|
||||
"Sending {length} characters of text: '{text}'",
|
||||
text.Length,
|
||||
ComTextHelper.GetDebugText(text));
|
||||
|
||||
TheStream.Write(text);
|
||||
TheStream.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogDebug("Client is null or disconnected. Cannot Send Text");
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
TheStream.Write(text);
|
||||
TheStream.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogDebug("Client is null or disconnected. Cannot Send Text");
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReconnectTimer.Reset();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Exception sending text: '{message}'", text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends Bytes to the server
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Client != null && TheStream != null && IsConnected)
|
||||
@@ -537,57 +535,57 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
||||
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Start();
|
||||
ReconnectTimer.Reset();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
//*****************************************************************************************************
|
||||
//*****************************************************************************************************
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
//*****************************************************************************************************
|
||||
/// <summary>
|
||||
/// Fired when connection changes
|
||||
/// </summary>
|
||||
public class SshConnectionChangeEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when connection changes
|
||||
/// Connection State
|
||||
/// </summary>
|
||||
public class SshConnectionChangeEventArgs : EventArgs
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection Status represented as a ushort
|
||||
/// </summary>
|
||||
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
|
||||
|
||||
/// <summary>
|
||||
/// The client
|
||||
/// </summary>
|
||||
public GenericSshClient Client { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Socket Status as represented by
|
||||
/// </summary>
|
||||
public ushort Status { get { return Client.UStatus; } }
|
||||
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public SshConnectionChangeEventArgs() { }
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs class
|
||||
/// </summary>
|
||||
/// <param name="isConnected">Connection State</param>
|
||||
/// <param name="client">The Client</param>
|
||||
public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
|
||||
{
|
||||
/// <summary>
|
||||
/// Connection State
|
||||
/// </summary>
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection Status represented as a ushort
|
||||
/// </summary>
|
||||
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
|
||||
|
||||
/// <summary>
|
||||
/// The client
|
||||
/// </summary>
|
||||
public GenericSshClient Client { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Socket Status as represented by
|
||||
/// </summary>
|
||||
public ushort Status { get { return Client.UStatus; } }
|
||||
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public SshConnectionChangeEventArgs() { }
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs class
|
||||
/// </summary>
|
||||
/// <param name="isConnected">Connection State</param>
|
||||
/// <param name="client">The Client</param>
|
||||
public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
|
||||
{
|
||||
IsConnected = isConnected;
|
||||
Client = client;
|
||||
}
|
||||
IsConnected = isConnected;
|
||||
Client = client;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,17 @@
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Timer = System.Timers.Timer;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
|
||||
using PepperDash.Core.Logging;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A class to handle basic TCP/IP communications with a server
|
||||
/// </summary>
|
||||
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
{
|
||||
private const string SplusKey = "Uninitialized TcpIpClient";
|
||||
/// <summary>
|
||||
@@ -26,44 +19,44 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
/// </summary>
|
||||
public CommunicationStreamDebugging StreamDebugging { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fires when data is received from the server and returns it as a Byte array
|
||||
/// </summary>
|
||||
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
|
||||
/// <summary>
|
||||
/// Fires when data is received from the server and returns it as a Byte array
|
||||
/// </summary>
|
||||
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when data is received from the server and returns it as text
|
||||
/// </summary>
|
||||
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
|
||||
/// <summary>
|
||||
/// Fires when data is received from the server and returns it as text
|
||||
/// </summary>
|
||||
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
|
||||
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
|
||||
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
|
||||
|
||||
|
||||
private string _hostname;
|
||||
private string _hostname;
|
||||
|
||||
/// <summary>
|
||||
/// Address of server
|
||||
/// </summary>
|
||||
public string Hostname
|
||||
{
|
||||
get
|
||||
{
|
||||
return _hostname;
|
||||
}
|
||||
get
|
||||
{
|
||||
return _hostname;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_hostname = value;
|
||||
if (_client != null)
|
||||
{
|
||||
_client.AddressClientConnectedTo = _hostname;
|
||||
}
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
_hostname = value;
|
||||
if (_client != null)
|
||||
{
|
||||
_client.AddressClientConnectedTo = _hostname;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port on server
|
||||
@@ -85,19 +78,19 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The actual client class
|
||||
/// </summary>
|
||||
private TCPClient _client;
|
||||
/// <summary>
|
||||
/// The actual client class
|
||||
/// </summary>
|
||||
private TCPClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// Bool showing if socket is connected
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
/// <summary>
|
||||
/// Bool showing if socket is connected
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// S+ helper for IsConnected
|
||||
/// </summary>
|
||||
@@ -106,15 +99,15 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
get { return (ushort)(IsConnected ? 1 : 0); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// _client socket status Read only
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus
|
||||
{
|
||||
get
|
||||
/// <summary>
|
||||
/// _client socket status Read only
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
|
||||
}
|
||||
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,20 +119,26 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
get { return (ushort)ClientStatus; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <summary>
|
||||
/// Status text shows the message associated with socket status
|
||||
/// </summary>
|
||||
public string ClientStatusText { get { return ClientStatus.ToString(); } }
|
||||
/// </summary>
|
||||
public string ClientStatusText { get { return ClientStatus.ToString(); } }
|
||||
|
||||
/// <summary>
|
||||
/// Connection failure reason
|
||||
/// </summary>
|
||||
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
|
||||
/// <summary>
|
||||
/// Ushort representation of client status
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AutoReconnect
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
/// <summary>
|
||||
/// Connection failure reason
|
||||
/// </summary>
|
||||
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
|
||||
|
||||
/// <summary>
|
||||
/// bool to track if auto reconnect should be set on the socket
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// S+ helper for AutoReconnect
|
||||
@@ -150,29 +149,29 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
set { AutoReconnect = value == 1; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
|
||||
/// </summary>
|
||||
public int AutoReconnectIntervalMs { get; set; }
|
||||
/// <summary>
|
||||
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
|
||||
/// </summary>
|
||||
public int AutoReconnectIntervalMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set only when the disconnect method is called
|
||||
/// </summary>
|
||||
bool DisconnectCalledByUser;
|
||||
/// <summary>
|
||||
/// Set only when the disconnect method is called
|
||||
/// </summary>
|
||||
bool DisconnectCalledByUser;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool Connected
|
||||
{
|
||||
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool Connected
|
||||
{
|
||||
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
}
|
||||
|
||||
//Lock object to prevent simulatneous connect/disconnect operations
|
||||
private readonly object _connectLock = new();
|
||||
private CCriticalSection connectLock = new CCriticalSection();
|
||||
|
||||
// private Timer for auto reconnect
|
||||
private Timer RetryTimer;
|
||||
private CTimer RetryTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
@@ -181,9 +180,9 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
/// <param name="address"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="bufferSize"></param>
|
||||
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
|
||||
: base(key)
|
||||
{
|
||||
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
|
||||
: base(key)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
@@ -191,7 +190,10 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
Port = port;
|
||||
BufferSize = bufferSize;
|
||||
|
||||
SetupRetryTimer();
|
||||
RetryTimer = new CTimer(o =>
|
||||
{
|
||||
Reconnect();
|
||||
}, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -206,30 +208,28 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
BufferSize = 2000;
|
||||
|
||||
SetupRetryTimer();
|
||||
RetryTimer = new CTimer(o =>
|
||||
{
|
||||
Reconnect();
|
||||
}, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor for S+
|
||||
/// </summary>
|
||||
public GenericTcpIpClient()
|
||||
: base(SplusKey)
|
||||
: base(SplusKey)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
BufferSize = 2000;
|
||||
|
||||
SetupRetryTimer();
|
||||
}
|
||||
|
||||
private void SetupRetryTimer()
|
||||
{
|
||||
RetryTimer = new Timer { AutoReset = false, Enabled = false };
|
||||
RetryTimer.Elapsed += (s, e) => Reconnect();
|
||||
}
|
||||
|
||||
|
||||
RetryTimer = new CTimer(o =>
|
||||
{
|
||||
Reconnect();
|
||||
}, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Just to help S+ set the key
|
||||
@@ -246,7 +246,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing connection");
|
||||
Debug.Console(1, this, "Program stopping. Closing connection");
|
||||
Deactivate();
|
||||
}
|
||||
}
|
||||
@@ -255,41 +255,42 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override bool Deactivate()
|
||||
{
|
||||
public override bool Deactivate()
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
RetryTimer.Dispose();
|
||||
if (_client != null)
|
||||
{
|
||||
_client.SocketStatusChange -= this.Client_SocketStatusChange;
|
||||
_client.SocketStatusChange -= this.Client_SocketStatusChange;
|
||||
DisconnectClient();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect to the server
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
public void Connect()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("GenericTcpIpClient '{0}': No address set", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key);
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
{
|
||||
this.LogWarning("GenericTcpIpClient '{0}': Invalid port", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Connection already connected. Exiting Connect()");
|
||||
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -302,7 +303,11 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
_client.ConnectToServerAsync(ConnectToServerCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
private void Reconnect()
|
||||
{
|
||||
@@ -310,34 +315,44 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
if (IsConnected || DisconnectCalledByUser == true)
|
||||
{
|
||||
this.LogInformation("Reconnect no longer needed. Exiting Reconnect()");
|
||||
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogInformation("Attempting reconnect now");
|
||||
Debug.Console(1, this, "Attempting reconnect now");
|
||||
_client.ConnectToServerAsync(ConnectToServerCallback);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to disconnect the client
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
lock (_connectLock)
|
||||
public void Disconnect()
|
||||
{
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
DisconnectCalledByUser = true;
|
||||
|
||||
// Stop trying reconnects, if we are
|
||||
RetryTimer.Stop();
|
||||
DisconnectClient();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the actual disconnect business
|
||||
@@ -346,7 +361,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
{
|
||||
if (_client != null)
|
||||
{
|
||||
this.LogInformation("Disconnecting client");
|
||||
Debug.Console(1, this, "Disconnecting client");
|
||||
if (IsConnected)
|
||||
_client.DisconnectFromServer();
|
||||
}
|
||||
@@ -356,47 +371,50 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
/// Callback method for connection attempt
|
||||
/// </summary>
|
||||
/// <param name="c"></param>
|
||||
void ConnectToServerCallback(TCPClient c)
|
||||
{
|
||||
void ConnectToServerCallback(TCPClient c)
|
||||
{
|
||||
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogInformation("Server connection result: {0}", c.ClientStatus);
|
||||
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus);
|
||||
WaitAndTryReconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogInformation("Server connection result: {0}", c.ClientStatus);
|
||||
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects, waits and attemtps to connect again
|
||||
/// </summary>
|
||||
void WaitAndTryReconnect()
|
||||
{
|
||||
Task.Run(() =>
|
||||
void WaitAndTryReconnect()
|
||||
{
|
||||
CrestronInvoke.BeginInvoke(o =>
|
||||
{
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
|
||||
{
|
||||
DisconnectClient();
|
||||
this.LogInformation("Attempting reconnect, status={0}", _client.ClientStatus);
|
||||
RetryTimer.Stop();
|
||||
RetryTimer.Interval = AutoReconnectIntervalMs;
|
||||
RetryTimer.Start();
|
||||
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus);
|
||||
RetryTimer.Reset(AutoReconnectIntervalMs);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recieves incoming data
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="numBytes"></param>
|
||||
void Receive(TCPClient client, int numBytes)
|
||||
{
|
||||
void Receive(TCPClient client, int numBytes)
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
if (numBytes > 0)
|
||||
@@ -407,7 +425,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
}
|
||||
@@ -418,135 +436,129 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
||||
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
|
||||
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
|
||||
}
|
||||
|
||||
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
|
||||
}
|
||||
}
|
||||
}
|
||||
client.ReceiveDataAsync(Receive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// General send method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
// Check debug level before processing byte array
|
||||
/// <summary>
|
||||
/// General send method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
// Check debug level before processing byte array
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
|
||||
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
|
||||
if (_client != null)
|
||||
_client.SendData(bytes, bytes.Length);
|
||||
}
|
||||
_client.SendData(bytes, bytes.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SendEscapedText method
|
||||
/// </summary>
|
||||
public void SendEscapedText(string text)
|
||||
{
|
||||
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
|
||||
{
|
||||
var hex = s.Groups[1].Value;
|
||||
return ((char)Convert.ToByte(hex, 16)).ToString();
|
||||
});
|
||||
SendText(unescapedText);
|
||||
}
|
||||
/// <summary>
|
||||
/// This is useful from console and...?
|
||||
/// </summary>
|
||||
public void SendEscapedText(string text)
|
||||
{
|
||||
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
|
||||
{
|
||||
var hex = s.Groups[1].Value;
|
||||
return ((char)Convert.ToByte(hex, 16)).ToString();
|
||||
});
|
||||
SendText(unescapedText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends Bytes to the server
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
if (_client != null)
|
||||
_client.SendData(bytes, bytes.Length);
|
||||
}
|
||||
_client.SendData(bytes, bytes.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Socket Status Change Handler
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="clientSocketStatus"></param>
|
||||
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
|
||||
{
|
||||
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
|
||||
{
|
||||
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
WaitAndTryReconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
_client.ReceiveDataAsync(Receive);
|
||||
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
_client.ReceiveDataAsync(Receive);
|
||||
}
|
||||
|
||||
var handler = ConnectionChange;
|
||||
if (handler != null)
|
||||
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
|
||||
}
|
||||
}
|
||||
var handler = ConnectionChange;
|
||||
if (handler != null)
|
||||
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration properties for TCP/SSH Connections
|
||||
/// </summary>
|
||||
public class TcpSshPropertiesConfig
|
||||
{
|
||||
public class TcpSshPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Address to connect to
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string Address { get; set; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public string Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Port to connect to
|
||||
/// </summary>
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public int Port { get; set; }
|
||||
|
||||
[JsonProperty(Required = Required.Always)]
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Username credential
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
public string Username { get; set; }
|
||||
/// <summary>
|
||||
/// Passord credential
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to 32768
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
/// <summary>
|
||||
/// Defaults to 32768
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AutoReconnect
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
/// <summary>
|
||||
/// Defaults to true
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AutoReconnectIntervalMs
|
||||
/// </summary>
|
||||
public int AutoReconnectIntervalMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, turns off echo for the SSH session
|
||||
/// </summary>
|
||||
[JsonProperty("disableSshEcho")]
|
||||
public bool DisableSshEcho { get; set; }
|
||||
/// <summary>
|
||||
/// Defaults to 5000ms
|
||||
/// </summary>
|
||||
public int AutoReconnectIntervalMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
public TcpSshPropertiesConfig()
|
||||
{
|
||||
BufferSize = 32768;
|
||||
AutoReconnect = true;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
public TcpSshPropertiesConfig()
|
||||
{
|
||||
BufferSize = 32768;
|
||||
AutoReconnect = true;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
Username = "";
|
||||
Password = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ PepperDash Technology Corporation reserves all rights under applicable laws.
|
||||
------------------------------------ */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
@@ -209,7 +210,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
/// <summary>
|
||||
/// private Timer for auto reconnect
|
||||
/// </summary>
|
||||
Timer RetryTimer;
|
||||
CTimer RetryTimer;
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -236,13 +237,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
/// </summary>
|
||||
public int HeartbeatInterval = 50000;
|
||||
|
||||
Timer HeartbeatSendTimer;
|
||||
Timer HeartbeatAckTimer;
|
||||
CTimer HeartbeatSendTimer;
|
||||
CTimer HeartbeatAckTimer;
|
||||
/// <summary>
|
||||
/// Used to force disconnection on a dead connect attempt
|
||||
/// </summary>
|
||||
Timer ConnectFailTimer;
|
||||
Timer WaitForSharedKey;
|
||||
CTimer ConnectFailTimer;
|
||||
CTimer WaitForSharedKey;
|
||||
private int ConnectionCount;
|
||||
/// <summary>
|
||||
/// Internal secure client
|
||||
@@ -302,7 +303,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing Client connection");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
|
||||
ProgramIsStopping = true;
|
||||
Disconnect();
|
||||
}
|
||||
@@ -315,17 +316,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
public void Connect()
|
||||
{
|
||||
ConnectionCount++;
|
||||
this.LogDebug("Attempting connect Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
|
||||
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Already connected. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
|
||||
return;
|
||||
}
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
this.LogInformation("Already trying to connect. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
@@ -338,17 +339,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No address set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: Invalid port");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No Shared Key set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,10 +370,9 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
ConnectFailTimer = new Timer(30000) { AutoReset = false };
|
||||
ConnectFailTimer.Elapsed += (s, e) =>
|
||||
ConnectFailTimer = new CTimer(o =>
|
||||
{
|
||||
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
IsTryingToConnect = false;
|
||||
@@ -384,13 +384,12 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
//SecureClient.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
}
|
||||
};
|
||||
ConnectFailTimer.Start();
|
||||
}, 30000);
|
||||
|
||||
this.LogDebug("Making Connection Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
|
||||
Client.ConnectToServerAsync(o =>
|
||||
{
|
||||
this.LogDebug("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
|
||||
if (ConnectFailTimer != null)
|
||||
{
|
||||
@@ -400,22 +399,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
o.ReceiveDataAsync(Receive);
|
||||
|
||||
if (SharedKeyRequired)
|
||||
{
|
||||
WaitingForSharedKeyResponse = true;
|
||||
WaitForSharedKey = new Timer(15000) { AutoReset = false };
|
||||
WaitForSharedKey.Elapsed += (s, e) =>
|
||||
WaitForSharedKey = new CTimer(timer =>
|
||||
{
|
||||
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
|
||||
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 is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
|
||||
o.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
//OnClientReadyForcommunications(false); // Should send false event
|
||||
};
|
||||
WaitForSharedKey.Start();
|
||||
}, 15000);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -429,15 +428,14 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
|
||||
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Client connection exception: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
|
||||
IsTryingToConnect = false;
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
@@ -474,7 +472,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
if (Client != null)
|
||||
{
|
||||
//SecureClient.DisconnectFromServer();
|
||||
this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Client.SocketStatusChange -= Client_SocketStatusChange;
|
||||
Client.Dispose();
|
||||
Client = null;
|
||||
@@ -496,22 +494,20 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
this.LogVerbose("Cleaning up remotely closed/failed connection.");
|
||||
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
|
||||
Cleanup();
|
||||
}
|
||||
if (!DisconnectCalledByUser && AutoReconnect)
|
||||
{
|
||||
var halfInterval = AutoReconnectIntervalMs / 2;
|
||||
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
|
||||
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
if (RetryTimer != null)
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
RetryTimer = null;
|
||||
}
|
||||
RetryTimer = new Timer(rndTime) { AutoReset = false };
|
||||
RetryTimer.Elapsed += (s, e) => Connect();
|
||||
RetryTimer.Start();
|
||||
RetryTimer = new CTimer(o => Connect(), rndTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,18 +526,18 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
|
||||
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
|
||||
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
|
||||
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
|
||||
{
|
||||
if (SharedKeyRequired && str == "SharedKey:")
|
||||
{
|
||||
this.LogVerbose("Server asking for shared key, sending");
|
||||
Debug.Console(2, this, "Server asking for shared key, sending");
|
||||
SendText(SharedKey + "\n");
|
||||
}
|
||||
else if (SharedKeyRequired && str == "Shared Key Match")
|
||||
{
|
||||
StopWaitForSharedKeyTimer();
|
||||
this.LogVerbose("Shared key confirmed. Ready for communication");
|
||||
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
|
||||
OnClientReadyForcommunications(true); // Successful key exchange
|
||||
}
|
||||
else
|
||||
@@ -557,8 +553,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
}
|
||||
}
|
||||
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
@@ -569,19 +564,15 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (HeartbeatEnabled)
|
||||
{
|
||||
this.LogVerbose("Starting Heartbeat");
|
||||
Debug.Console(2, this, "Starting Heartbeat");
|
||||
if (HeartbeatSendTimer == null)
|
||||
{
|
||||
|
||||
HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
|
||||
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
|
||||
HeartbeatSendTimer.Start();
|
||||
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
|
||||
}
|
||||
if (HeartbeatAckTimer == null)
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,13 +582,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
if (HeartbeatSendTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Send");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Send");
|
||||
HeartbeatSendTimer.Stop();
|
||||
HeartbeatSendTimer = null;
|
||||
}
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Ack");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Ack");
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer = null;
|
||||
}
|
||||
@@ -606,7 +597,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
void SendHeartbeat(object notused)
|
||||
{
|
||||
this.SendText(HeartbeatString);
|
||||
this.LogVerbose("Sending Heartbeat");
|
||||
Debug.Console(2, this, "Sending Heartbeat");
|
||||
|
||||
}
|
||||
|
||||
@@ -625,17 +616,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
return remainingText;
|
||||
}
|
||||
}
|
||||
@@ -643,8 +630,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
@@ -658,7 +644,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
SendText("Heartbeat not received by server, closing connection");
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
@@ -666,8 +652,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Heartbeat timeout Error on Client: {0}, {1}", Key, ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -700,15 +685,14 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
|
||||
if (n <= 0)
|
||||
{
|
||||
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,8 +711,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending bytes. Error: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,7 +730,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
try
|
||||
{
|
||||
this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
|
||||
OnConnectionChange();
|
||||
|
||||
@@ -761,8 +744,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in socket status change callback. Error: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
@@ -62,14 +61,9 @@ public class GenericTcpIpServer : Device
|
||||
#region Properties/Variables
|
||||
|
||||
/// <summary>
|
||||
/// Server listen lock
|
||||
///
|
||||
/// </summary>
|
||||
object _serverLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast lock
|
||||
/// </summary>
|
||||
private readonly object _broadcastLock = new();
|
||||
CCriticalSection ServerCCSection = new CCriticalSection();
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -80,7 +74,7 @@ public class GenericTcpIpServer : Device
|
||||
/// <summary>
|
||||
/// Timer to operate the bandaid monitor client in a loop.
|
||||
/// </summary>
|
||||
Timer MonitorClientTimer;
|
||||
CTimer MonitorClientTimer;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -250,7 +244,7 @@ public class GenericTcpIpServer : Device
|
||||
public string HeartbeatStringToMatch { get; set; }
|
||||
|
||||
//private timers for Heartbeats per client
|
||||
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
|
||||
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
|
||||
|
||||
//flags to show the secure server is waiting for client at index to send the shared key
|
||||
List<uint> WaitingForSharedKey = new List<uint>();
|
||||
@@ -371,12 +365,12 @@ public class GenericTcpIpServer : Device
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,18 +379,19 @@ public class GenericTcpIpServer : Device
|
||||
/// </summary>
|
||||
public void Listen()
|
||||
{
|
||||
lock (_serverLock)
|
||||
{
|
||||
ServerCCSection.Enter();
|
||||
try
|
||||
{
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogError("Server '{0}': Invalid port", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogError("Server '{0}': No Shared Key set", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
|
||||
return;
|
||||
}
|
||||
if (IsListening)
|
||||
@@ -405,10 +400,10 @@ public class GenericTcpIpServer : Device
|
||||
if (myTcpServer == null)
|
||||
{
|
||||
myTcpServer = new TCPServer(Port, MaxClients);
|
||||
if(HeartbeatRequired)
|
||||
if (HeartbeatRequired)
|
||||
myTcpServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5);
|
||||
|
||||
// myTcpServer.HandshakeTimeout = 30;
|
||||
|
||||
// myTcpServer.HandshakeTimeout = 30;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -420,17 +415,20 @@ public class GenericTcpIpServer : Device
|
||||
myTcpServer.SocketStatusChange += TcpServer_SocketStatusChange;
|
||||
|
||||
ServerStopped = false;
|
||||
myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
|
||||
myTcpServer.WaitForConnectionAsync("0.0.0.0", TcpConnectCallback);
|
||||
OnServerStateChange(myTcpServer.State);
|
||||
this.LogInformation("TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
|
||||
|
||||
// StartMonitorClient();
|
||||
|
||||
|
||||
ServerCCSection.Leave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error with Dynamic Server: {0}", ex.Message);
|
||||
ServerCCSection.Leave();
|
||||
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -440,18 +438,18 @@ public class GenericTcpIpServer : Device
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogDebug("Stopping Listener");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
|
||||
if (myTcpServer != null)
|
||||
{
|
||||
myTcpServer.Stop();
|
||||
this.LogDebug("Server State: {0}", myTcpServer.State);
|
||||
OnServerStateChange(myTcpServer.State);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State);
|
||||
OnServerStateChange(myTcpServer.State);
|
||||
}
|
||||
ServerStopped = true;
|
||||
ServerStopped = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,11 +462,11 @@ public class GenericTcpIpServer : Device
|
||||
try
|
||||
{
|
||||
myTcpServer.Disconnect(client);
|
||||
this.LogVerbose("Disconnected client index: {0}", client);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
@@ -476,7 +474,7 @@ public class GenericTcpIpServer : Device
|
||||
/// </summary>
|
||||
public void DisconnectAllClientsForShutdown()
|
||||
{
|
||||
this.LogInformation("Disconnecting All Clients");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
|
||||
if (myTcpServer != null)
|
||||
{
|
||||
myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
|
||||
@@ -488,17 +486,17 @@ public class GenericTcpIpServer : Device
|
||||
try
|
||||
{
|
||||
myTcpServer.Disconnect(i);
|
||||
this.LogVerbose("Disconnected client index: {0}", i);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex);
|
||||
}
|
||||
}
|
||||
this.LogVerbose("Server Status: {0}", myTcpServer.ServerSocketStatus);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus);
|
||||
}
|
||||
|
||||
this.LogInformation("Disconnected All Clients");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients");
|
||||
ConnectedClientsIndexes.Clear();
|
||||
|
||||
if (!ProgramIsStopping)
|
||||
@@ -516,8 +514,8 @@ public class GenericTcpIpServer : Device
|
||||
/// <param name="text"></param>
|
||||
public void BroadcastText(string text)
|
||||
{
|
||||
lock (_broadcastLock)
|
||||
{
|
||||
CCriticalSection CCBroadcast = new CCriticalSection();
|
||||
CCBroadcast.Enter();
|
||||
try
|
||||
{
|
||||
if (ConnectedClientsIndexes.Count > 0)
|
||||
@@ -529,16 +527,17 @@ public class GenericTcpIpServer : Device
|
||||
{
|
||||
SocketErrorCodes error = myTcpServer.SendDataAsync(i, b, b.Length, (x, y, z) => { });
|
||||
if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
this.LogError("{error}",error.ToString());
|
||||
this.LogError("{error}", error.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
CCBroadcast.Leave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
CCBroadcast.Leave();
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -559,7 +558,7 @@ public class GenericTcpIpServer : Device
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,19 +576,13 @@ public class GenericTcpIpServer : Device
|
||||
if (noDelimiter.Contains(HeartbeatStringToMatch))
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
// Return Heartbeat
|
||||
SendTextToClient(HeartbeatStringToMatch, clientIndex);
|
||||
return remainingText;
|
||||
@@ -598,25 +591,19 @@ public class GenericTcpIpServer : Device
|
||||
else
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
@@ -628,11 +615,11 @@ public class GenericTcpIpServer : Device
|
||||
/// <returns>IP address of the client</returns>
|
||||
public string GetClientIPAddress(uint clientIndex)
|
||||
{
|
||||
this.LogVerbose("GetClientIPAddress Index: {0}", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex);
|
||||
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
|
||||
{
|
||||
var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
this.LogVerbose("GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
return ipa;
|
||||
|
||||
}
|
||||
@@ -655,13 +642,14 @@ public class GenericTcpIpServer : Device
|
||||
clientIndex = (uint)o;
|
||||
address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
|
||||
this.LogWarning("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
|
||||
|
||||
if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
|
||||
|
||||
var discoResult = myTcpServer.Disconnect(clientIndex);
|
||||
//Debug.Console(1, this, "{0}", discoResult);
|
||||
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
@@ -672,8 +660,7 @@ public class GenericTcpIpServer : Device
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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);
|
||||
ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,35 +671,25 @@ public class GenericTcpIpServer : Device
|
||||
/// Secure Server Socket Status Changed Callback
|
||||
/// </summary>
|
||||
/// <param name="server"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <param name="serverSocketStatus"></param>
|
||||
void TcpServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
|
||||
/// <param name="clientIndex">Index of the client whose socket status has changed</param>
|
||||
/// <param name="status">New socket status</param>
|
||||
void TcpServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TcpServerSocketStatusChange ConnectedClients: {0} ServerState: {1} Port: {2} ClientIndex: {3} Status: {4}",
|
||||
myTcpServer.NumberOfClientsConnected, myTcpServer.State, myTcpServer.PortNumber, clientIndex, status);
|
||||
|
||||
this.LogInformation("SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
// Handle connection limit and listening state
|
||||
if (myTcpServer.MaxNumberOfClientSupported > myTcpServer.NumberOfClientsConnected)
|
||||
{
|
||||
if (ConnectedClientsIndexes.Contains(clientIndex))
|
||||
ConnectedClientsIndexes.Remove(clientIndex);
|
||||
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Dispose();
|
||||
HeartbeatTimerDictionary.Remove(clientIndex);
|
||||
}
|
||||
if (ClientReadyAfterKeyExchange.Contains(clientIndex))
|
||||
ClientReadyAfterKeyExchange.Remove(clientIndex);
|
||||
if (WaitingForSharedKey.Contains(clientIndex))
|
||||
WaitingForSharedKey.Remove(clientIndex);
|
||||
Listen();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex);
|
||||
}
|
||||
onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -727,7 +704,7 @@ public class GenericTcpIpServer : Device
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogDebug("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
|
||||
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
|
||||
if (clientIndex != 0)
|
||||
@@ -747,21 +724,17 @@ public class GenericTcpIpServer : Device
|
||||
}
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
|
||||
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
|
||||
this.LogDebug("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogDebug("Client at index {0} is ready for communications", clientIndex);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
}
|
||||
if (HeartbeatRequired)
|
||||
{
|
||||
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,26 +743,26 @@ public class GenericTcpIpServer : Device
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Client attempt faulty.");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
|
||||
if (!ServerStopped)
|
||||
{
|
||||
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
|
||||
server.WaitForConnectionAsync("0.0.0.0", TcpConnectCallback);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex);
|
||||
}
|
||||
//Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))",
|
||||
// server.State,
|
||||
// MaxClients,
|
||||
// ServerStopped);
|
||||
if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
|
||||
{
|
||||
this.LogDebug("Waiting for next connection");
|
||||
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection");
|
||||
server.WaitForConnectionAsync("0.0.0.0", TcpConnectCallback);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -805,48 +778,48 @@ public class GenericTcpIpServer : Device
|
||||
/// <param name="numberOfBytesReceived"></param>
|
||||
void TcpServerReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived)
|
||||
{
|
||||
if (numberOfBytesReceived > 0)
|
||||
{
|
||||
string received = "Nothing";
|
||||
try
|
||||
{
|
||||
byte[] bytes = myTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
|
||||
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
|
||||
if (WaitingForSharedKey.Contains(clientIndex))
|
||||
{
|
||||
received = received.Replace("\r", "");
|
||||
received = received.Replace("\n", "");
|
||||
if (received != SharedKey)
|
||||
{
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
|
||||
this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
myTCPServer.SendData(clientIndex, b, b.Length);
|
||||
myTCPServer.Disconnect(clientIndex);
|
||||
return;
|
||||
}
|
||||
if (numberOfBytesReceived > 0)
|
||||
{
|
||||
string received = "Nothing";
|
||||
try
|
||||
{
|
||||
byte[] bytes = myTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
|
||||
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
|
||||
if (WaitingForSharedKey.Contains(clientIndex))
|
||||
{
|
||||
received = received.Replace("\r", "");
|
||||
received = received.Replace("\n", "");
|
||||
if (received != SharedKey)
|
||||
{
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
myTCPServer.SendData(clientIndex, b, b.Length);
|
||||
myTCPServer.Disconnect(clientIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
WaitingForSharedKey.Remove(clientIndex);
|
||||
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
|
||||
myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
this.LogDebug("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
}
|
||||
WaitingForSharedKey.Remove(clientIndex);
|
||||
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
|
||||
myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
}
|
||||
|
||||
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
|
||||
onTextReceived(received, clientIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex);
|
||||
}
|
||||
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If numberOfBytesReceived <= 0
|
||||
myTCPServer.Disconnect();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
|
||||
onTextReceived(received, clientIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
|
||||
}
|
||||
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If numberOfBytesReceived <= 0
|
||||
myTCPServer.Disconnect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -918,7 +891,7 @@ public class GenericTcpIpServer : Device
|
||||
if (MonitorClient != null)
|
||||
MonitorClient.Disconnect();
|
||||
|
||||
this.LogInformation("Program stopping. Closing server");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
|
||||
KillServer();
|
||||
}
|
||||
}
|
||||
@@ -944,9 +917,7 @@ public class GenericTcpIpServer : Device
|
||||
{
|
||||
return;
|
||||
}
|
||||
MonitorClientTimer = new Timer(60000) { AutoReset = false };
|
||||
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
|
||||
MonitorClientTimer.Start();
|
||||
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -961,7 +932,7 @@ public class GenericTcpIpServer : Device
|
||||
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
|
||||
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
|
||||
|
||||
this.LogDebug("Starting monitor check");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
|
||||
|
||||
MonitorClient.Connect();
|
||||
// From here MonitorCLient either connects or hangs, MonitorClient will call back
|
||||
@@ -988,7 +959,7 @@ public class GenericTcpIpServer : Device
|
||||
{
|
||||
if (args.IsReady)
|
||||
{
|
||||
this.LogInformation("Monitor client connection success. Disconnecting in 2s");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s");
|
||||
MonitorClientTimer.Stop();
|
||||
MonitorClientTimer = null;
|
||||
MonitorClientFailureCount = 0;
|
||||
@@ -1009,13 +980,13 @@ public class GenericTcpIpServer : Device
|
||||
StopMonitorClient();
|
||||
if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
|
||||
{
|
||||
this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
|
||||
StartMonitorClient();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError(
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error,
|
||||
"\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************",
|
||||
MonitorClientMaxFailureCount);
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core.Logging;
|
||||
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
@@ -36,7 +33,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
/// <summary>
|
||||
/// This event will fire when a message is dequeued that includes the source IP and Port info if needed to determine the source of the received data.
|
||||
/// </summary>
|
||||
public event EventHandler<GenericUdpReceiveTextExtraArgs> DataRecievedExtra;
|
||||
public event EventHandler<GenericUdpReceiveTextExtraArgs> DataRecievedExtra;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -55,12 +52,12 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
get
|
||||
{
|
||||
return Server.ServerStatus;
|
||||
return Server.ClientStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a GenericUdpReceiveTextExtraArgs
|
||||
///
|
||||
/// </summary>
|
||||
public ushort UStatus
|
||||
{
|
||||
@@ -127,7 +124,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
@@ -138,7 +135,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
public GenericUdpServer(string key, string address, int port, int buffefSize)
|
||||
: base(key)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||
Hostname = address;
|
||||
Port = port;
|
||||
BufferSize = buffefSize;
|
||||
@@ -180,10 +177,10 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
/// <param name="programEventType"></param>
|
||||
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType != eProgramStatusEventType.Stopping)
|
||||
if (programEventType != eProgramStatusEventType.Stopping)
|
||||
return;
|
||||
|
||||
this.LogInformation("Program stopping. Disabling Server");
|
||||
Debug.Console(1, this, "Program stopping. Disabling Server");
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
@@ -199,20 +196,20 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("GenericUdpServer '{0}': No address set", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key);
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
{
|
||||
this.LogWarning("GenericUdpServer '{0}': Invalid port", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var status = Server.EnableUDPServer(Hostname, Port);
|
||||
|
||||
this.LogVerbose("SocketErrorCode: {0}", status);
|
||||
Debug.Console(2, this, "SocketErrorCode: {0}", status);
|
||||
if (status == SocketErrorCodes.SOCKET_OK)
|
||||
IsConnected = true;
|
||||
|
||||
@@ -229,7 +226,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if(Server != null)
|
||||
if (Server != null)
|
||||
Server.DisableUDPServer();
|
||||
|
||||
IsConnected = false;
|
||||
@@ -247,11 +244,11 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
/// <param name="numBytes"></param>
|
||||
void Receive(UDPServer server, int numBytes)
|
||||
{
|
||||
this.LogVerbose("Received {0} bytes", numBytes);
|
||||
Debug.Console(2, this, "Received {0} bytes", numBytes);
|
||||
|
||||
try
|
||||
{
|
||||
if (numBytes <= 0)
|
||||
if (numBytes <= 0)
|
||||
return;
|
||||
|
||||
var sourceIp = Server.IPAddressLastMessageReceivedFrom;
|
||||
@@ -263,13 +260,13 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
if (dataRecivedExtra != null)
|
||||
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
|
||||
|
||||
this.LogVerbose("Bytes: {0}", bytes.ToString());
|
||||
Debug.Console(2, this, "Bytes: {0}", bytes.ToString());
|
||||
var bytesHandler = BytesReceived;
|
||||
if (bytesHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
}
|
||||
@@ -277,7 +274,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
if (textHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
|
||||
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
|
||||
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
|
||||
}
|
||||
}
|
||||
@@ -302,7 +299,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
if (IsConnected && Server != null)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogVerbose("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
|
||||
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
|
||||
|
||||
Server.SendData(bytes, bytes.Length);
|
||||
}
|
||||
@@ -315,7 +312,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
|
||||
if (IsConnected && Server != null)
|
||||
Server.SendData(bytes, bytes.Length);
|
||||
@@ -326,24 +323,24 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class GenericUdpReceiveTextExtraArgs : EventArgs
|
||||
{
|
||||
public class GenericUdpReceiveTextExtraArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Text { get; private set; }
|
||||
public string Text { get; private set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string IpAddress { get; private set; }
|
||||
public string IpAddress { get; private set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int Port { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public byte[] Bytes { get; private set; }
|
||||
public byte[] Bytes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -352,20 +349,20 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
/// <param name="ipAddress"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="bytes"></param>
|
||||
public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
|
||||
{
|
||||
Text = text;
|
||||
IpAddress = ipAddress;
|
||||
Port = port;
|
||||
Bytes = bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stupid S+ Constructor
|
||||
/// </summary>
|
||||
public GenericUdpReceiveTextExtraArgs() { }
|
||||
public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
|
||||
{
|
||||
Text = text;
|
||||
IpAddress = ipAddress;
|
||||
Port = port;
|
||||
Bytes = bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stupid S+ Constructor
|
||||
/// </summary>
|
||||
public GenericUdpReceiveTextExtraArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
using System;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for stream debugging
|
||||
/// </summary>
|
||||
public static class StreamDebuggingExtensions
|
||||
{
|
||||
private static readonly string app = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? $"App {InitialParametersClass.ApplicationNumber}" : $"{InitialParametersClass.RoomId}";
|
||||
|
||||
/// <summary>
|
||||
/// Print the sent bytes to the console
|
||||
/// </summary>
|
||||
/// <param name="comms">comms device</param>
|
||||
/// <param name="bytes">bytes to print</param>
|
||||
public static void PrintSentBytes(this IStreamDebugging comms, byte[] bytes)
|
||||
{
|
||||
if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return;
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
|
||||
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print the received bytes to the console
|
||||
/// </summary>
|
||||
/// <param name="comms">comms device</param>
|
||||
/// <param name="bytes">bytes to print</param>
|
||||
public static void PrintReceivedBytes(this IStreamDebugging comms, byte[] bytes)
|
||||
{
|
||||
if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return;
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
|
||||
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print the sent text to the console
|
||||
/// </summary>
|
||||
/// <param name="comms">comms device</param>
|
||||
/// <param name="text">text to print</param>
|
||||
public static void PrintSentText(this IStreamDebugging comms, string text)
|
||||
{
|
||||
if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return;
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
|
||||
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending Text: '{ComTextHelper.GetDebugText(text)}'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print the received text to the console
|
||||
/// </summary>
|
||||
/// <param name="comms">comms device</param>
|
||||
/// <param name="text">text to print</param>
|
||||
public static void PrintReceivedText(this IStreamDebugging comms, string text)
|
||||
{
|
||||
if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return;
|
||||
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
|
||||
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received Text: '{ComTextHelper.GetDebugText(text)}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
|
||||
@@ -74,13 +74,5 @@ public enum eControlMethod
|
||||
/// <summary>
|
||||
/// Secure TCP/IP
|
||||
/// </summary>
|
||||
SecureTcpIp,
|
||||
/// <summary>
|
||||
/// Crestron COM bridge
|
||||
/// </summary>
|
||||
ComBridge,
|
||||
/// <summary>
|
||||
/// Crestron Infinet EX device
|
||||
/// </summary>
|
||||
InfinetEx
|
||||
SecureTcpIp
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The available settings for stream debugging data format types
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum eStreamDebuggingDataTypeSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug data in byte format
|
||||
/// </summary>
|
||||
Bytes = 0,
|
||||
/// <summary>
|
||||
/// Debug data in text format
|
||||
/// </summary>
|
||||
Text = 1,
|
||||
/// <summary>
|
||||
/// Debug data in both byte and text formats
|
||||
/// </summary>
|
||||
Both = Bytes | Text
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
@@ -43,7 +39,7 @@ public interface ICommunicationReceiver : IKeyed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="ICommunicationReceiver"/> with methods for sending text and bytes to a device.
|
||||
/// Represents a device that uses basic connection
|
||||
/// </summary>
|
||||
public interface IBasicCommunication : ICommunicationReceiver
|
||||
{
|
||||
@@ -58,7 +54,7 @@ public interface IBasicCommunication : ICommunicationReceiver
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
void SendBytes(byte[] bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a device that implements IBasicCommunication and IStreamDebugging
|
||||
@@ -71,7 +67,7 @@ public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, I
|
||||
/// <summary>
|
||||
/// Represents a device with stream debugging capablities
|
||||
/// </summary>
|
||||
public interface IStreamDebugging : IKeyed
|
||||
public interface IStreamDebugging
|
||||
{
|
||||
/// <summary>
|
||||
/// Object to enable stream debugging
|
||||
@@ -95,7 +91,7 @@ public interface IStreamDebugging : IKeyed
|
||||
/// The current socket status of the client
|
||||
/// </summary>
|
||||
[JsonProperty("clientStatus")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
SocketStatus ClientStatus { get; }
|
||||
}
|
||||
|
||||
@@ -139,60 +135,59 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
||||
Disconnected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This delegate defines handler for IBasicCommunication status changes
|
||||
/// </summary>
|
||||
/// <param name="comm">Device firing the status change</param>
|
||||
/// <param name="status"></param>
|
||||
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
|
||||
/// <summary>
|
||||
/// This delegate defines handler for IBasicCommunication status changes
|
||||
/// </summary>
|
||||
/// <param name="comm">Device firing the status change</param>
|
||||
/// <param name="status"></param>
|
||||
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
|
||||
|
||||
/// <summary>
|
||||
/// Event args for bytes received from a communication method
|
||||
///
|
||||
/// </summary>
|
||||
public class GenericCommMethodReceiveBytesArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The bytes received
|
||||
///
|
||||
/// </summary>
|
||||
public byte[] Bytes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
public GenericCommMethodReceiveBytesArgs(byte[] bytes)
|
||||
{
|
||||
Bytes = bytes;
|
||||
}
|
||||
{
|
||||
Bytes = bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericCommMethodReceiveBytesArgs() { }
|
||||
}
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericCommMethodReceiveBytesArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for text received
|
||||
///
|
||||
/// </summary>
|
||||
public class GenericCommMethodReceiveTextArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The text received
|
||||
///
|
||||
/// </summary>
|
||||
public string Text { get; private set; }
|
||||
/// <summary>
|
||||
/// The delimiter used to determine the end of a message, if applicable
|
||||
///
|
||||
/// </summary>
|
||||
public string Delimiter { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public GenericCommMethodReceiveTextArgs(string text)
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
@@ -214,7 +209,7 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to get escaped text for debugging communication streams
|
||||
///
|
||||
/// </summary>
|
||||
public class ComTextHelper
|
||||
{
|
||||
@@ -248,4 +243,4 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
||||
{
|
||||
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
|
||||
using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using JToken = NewtonsoftJson::Newtonsoft.Json.Linq.JToken;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
@@ -16,242 +12,223 @@ namespace PepperDash.Core.Config;
|
||||
/// <summary>
|
||||
/// Reads a Portal formatted config file
|
||||
/// </summary>
|
||||
public class PortalConfigReader
|
||||
public class PortalConfigReader
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
|
||||
/// </summary>
|
||||
/// <returns>JObject of config file</returns>
|
||||
public static void ReadAndMergeFileIfNecessary(string filePath, string savePath)
|
||||
{
|
||||
const string template = "template";
|
||||
const string system = "system";
|
||||
const string systemUrl = "system_url";
|
||||
const string templateUrl = "template_url";
|
||||
const string info = "info";
|
||||
const string devices = "devices";
|
||||
const string rooms = "rooms";
|
||||
const string sourceLists = "sourceLists";
|
||||
const string destinationLists = "destinationLists";
|
||||
const string cameraLists = "cameraLists";
|
||||
const string audioControlPointLists = "audioControlPointLists";
|
||||
|
||||
const string tieLines = "tieLines";
|
||||
const string joinMaps = "joinMaps";
|
||||
const string global = "global";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
|
||||
/// </summary>
|
||||
/// <returns>JObject of config file</returns>
|
||||
public static void ReadAndMergeFileIfNecessary(string filePath, string savePath)
|
||||
try
|
||||
{
|
||||
try
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Error,
|
||||
"ERROR: Configuration file not present. Please load file to {0} and reset program", filePath);
|
||||
}
|
||||
|
||||
using (StreamReader fs = new StreamReader(filePath))
|
||||
{
|
||||
var jsonObj = JObject.Parse(fs.ReadToEnd());
|
||||
if (jsonObj["template"] != null && jsonObj["system"] != null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"ERROR: Configuration file not present. Please load file to {0} and reset program", filePath);
|
||||
// it's a double-config, merge it.
|
||||
var merged = MergeConfigs(jsonObj);
|
||||
if (jsonObj["system_url"] != null)
|
||||
{
|
||||
merged["systemUrl"] = jsonObj["system_url"].Value<string>();
|
||||
}
|
||||
|
||||
if (jsonObj["template_url"] != null)
|
||||
{
|
||||
merged["templateUrl"] = jsonObj["template_url"].Value<string>();
|
||||
}
|
||||
|
||||
jsonObj = merged;
|
||||
}
|
||||
|
||||
using (StreamReader fs = new StreamReader(filePath))
|
||||
using (StreamWriter fw = new StreamWriter(savePath))
|
||||
{
|
||||
var jsonObj = JObject.Parse(fs.ReadToEnd());
|
||||
if(jsonObj[template] != null && jsonObj[system] != null)
|
||||
{
|
||||
// it's a double-config, merge it.
|
||||
var merged = MergeConfigs(jsonObj);
|
||||
if (jsonObj[systemUrl] != null)
|
||||
{
|
||||
merged[systemUrl] = (string)jsonObj[systemUrl];
|
||||
}
|
||||
|
||||
if (jsonObj[templateUrl] != null)
|
||||
{
|
||||
merged[templateUrl] = (string)jsonObj[templateUrl];
|
||||
}
|
||||
|
||||
jsonObj = merged;
|
||||
}
|
||||
|
||||
using (StreamWriter fw = new StreamWriter(savePath))
|
||||
{
|
||||
fw.Write(jsonObj.ToString(Formatting.Indented));
|
||||
Debug.LogMessage(LogEventLevel.Debug, "JSON config merged and saved to {0}", savePath);
|
||||
}
|
||||
|
||||
fw.Write(jsonObj.ToString(Formatting.Indented));
|
||||
Debug.LogMessage(LogEventLevel.Debug, "JSON config merged and saved to {0}", savePath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogMessage(e, "ERROR: Config load failed");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="doubleConfig"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// MergeConfigs method
|
||||
/// </summary>
|
||||
public static JObject MergeConfigs(JObject doubleConfig)
|
||||
catch (Exception e)
|
||||
{
|
||||
var system = JObject.FromObject(doubleConfig["system"]);
|
||||
var template = JObject.FromObject(doubleConfig["template"]);
|
||||
var merged = new JObject();
|
||||
|
||||
// Put together top-level objects
|
||||
if (system[info] != null)
|
||||
merged.Add(info, Merge(template[info], system[info], info));
|
||||
else
|
||||
merged.Add(info, template[info]);
|
||||
|
||||
merged.Add(devices, MergeArraysOnTopLevelProperty(template[devices] as JArray,
|
||||
system[devices] as JArray, "key", devices));
|
||||
|
||||
if (system[rooms] == null)
|
||||
merged.Add(rooms, template[rooms]);
|
||||
else
|
||||
merged.Add(rooms, MergeArraysOnTopLevelProperty(template[rooms] as JArray,
|
||||
system[rooms] as JArray, "key", rooms));
|
||||
|
||||
if (system[sourceLists] == null)
|
||||
merged.Add(sourceLists, template[sourceLists]);
|
||||
else
|
||||
merged.Add(sourceLists, Merge(template[sourceLists], system[sourceLists], sourceLists));
|
||||
|
||||
if (system[destinationLists] == null)
|
||||
merged.Add(destinationLists, template[destinationLists]);
|
||||
else
|
||||
merged.Add(destinationLists,
|
||||
Merge(template[destinationLists], system[destinationLists], destinationLists));
|
||||
|
||||
|
||||
if (system["cameraLists"] == null)
|
||||
merged.Add("cameraLists", template["cameraLists"]);
|
||||
else
|
||||
merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists"));
|
||||
|
||||
if (system["audioControlPointLists"] == null)
|
||||
merged.Add("audioControlPointLists", template["audioControlPointLists"]);
|
||||
else
|
||||
merged.Add("audioControlPointLists",
|
||||
Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists"));
|
||||
|
||||
|
||||
// Template tie lines take precedence. Config tool doesn't do them at system
|
||||
// level anyway...
|
||||
if (template["tieLines"] != null)
|
||||
merged.Add("tieLines", template["tieLines"]);
|
||||
else if (system["tieLines"] != null)
|
||||
merged.Add("tieLines", system["tieLines"]);
|
||||
else
|
||||
merged.Add(tieLines, new JArray());
|
||||
|
||||
if (template["joinMaps"] != null)
|
||||
merged.Add("joinMaps", template["joinMaps"]);
|
||||
else
|
||||
merged.Add("joinMaps", new JObject());
|
||||
|
||||
if (system[global] != null)
|
||||
merged.Add(global, Merge(template[global], system[global], global));
|
||||
else
|
||||
merged.Add(global, template[global]);
|
||||
|
||||
return merged;
|
||||
Debug.LogMessage(e, "ERROR: Config load failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the contents of a base and a delta array, matching the entries on a top-level property
|
||||
/// given by propertyName. Returns a merge of them. Items in the delta array that do not have
|
||||
/// a matched item in base array will not be merged. Non keyed system items will replace the template items.
|
||||
/// </summary>
|
||||
static JArray MergeArraysOnTopLevelProperty(JArray a1, JArray a2, string propertyName, string path)
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="doubleConfig"></param>
|
||||
/// <returns></returns>
|
||||
public static JObject MergeConfigs(JObject doubleConfig)
|
||||
{
|
||||
var system = JObject.FromObject(doubleConfig["system"]);
|
||||
var template = JObject.FromObject(doubleConfig["template"]);
|
||||
var merged = new JObject();
|
||||
|
||||
// Put together top-level objects
|
||||
if (system["info"] != null)
|
||||
merged.Add("info", Merge(template["info"], system["info"], "infO"));
|
||||
else
|
||||
merged.Add("info", template["info"]);
|
||||
|
||||
merged.Add("devices", MergeArraysOnTopLevelProperty(template["devices"] as JArray,
|
||||
system["devices"] as JArray, "key", "devices"));
|
||||
|
||||
if (system["rooms"] == null)
|
||||
merged.Add("rooms", template["rooms"]);
|
||||
else
|
||||
merged.Add("rooms", MergeArraysOnTopLevelProperty(template["rooms"] as JArray,
|
||||
system["rooms"] as JArray, "key", "rooms"));
|
||||
|
||||
if (system["sourceLists"] == null)
|
||||
merged.Add("sourceLists", template["sourceLists"]);
|
||||
else
|
||||
merged.Add("sourceLists", Merge(template["sourceLists"], system["sourceLists"], "sourceLists"));
|
||||
|
||||
if (system["destinationLists"] == null)
|
||||
merged.Add("destinationLists", template["destinationLists"]);
|
||||
else
|
||||
merged.Add("destinationLists",
|
||||
Merge(template["destinationLists"], system["destinationLists"], "destinationLists"));
|
||||
|
||||
|
||||
if (system["cameraLists"] == null)
|
||||
merged.Add("cameraLists", template["cameraLists"]);
|
||||
else
|
||||
merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists"));
|
||||
|
||||
if (system["audioControlPointLists"] == null)
|
||||
merged.Add("audioControlPointLists", template["audioControlPointLists"]);
|
||||
else
|
||||
merged.Add("audioControlPointLists",
|
||||
Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists"));
|
||||
|
||||
|
||||
// Template tie lines take precedence. Config tool doesn't do them at system
|
||||
// level anyway...
|
||||
if (template["tieLines"] != null)
|
||||
merged.Add("tieLines", template["tieLines"]);
|
||||
else if (system["tieLines"] != null)
|
||||
merged.Add("tieLines", system["tieLines"]);
|
||||
else
|
||||
merged.Add("tieLines", new JArray());
|
||||
|
||||
if (template["joinMaps"] != null)
|
||||
merged.Add("joinMaps", template["joinMaps"]);
|
||||
else
|
||||
merged.Add("joinMaps", new JObject());
|
||||
|
||||
if (system["global"] != null)
|
||||
merged.Add("global", Merge(template["global"], system["global"], "global"));
|
||||
else
|
||||
merged.Add("global", template["global"]);
|
||||
|
||||
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merges the contents of a base and a delta array, matching the entries on a top-level property
|
||||
/// given by propertyName. Returns a merge of them. Items in the delta array that do not have
|
||||
/// a matched item in base array will not be merged. Non keyed system items will replace the template items.
|
||||
/// </summary>
|
||||
static JArray MergeArraysOnTopLevelProperty(JArray a1, JArray a2, string propertyName, string path)
|
||||
{
|
||||
var result = new JArray();
|
||||
if (a2 == null || a2.Count == 0) // If the system array is null or empty, return the template array
|
||||
return a1;
|
||||
else if (a1 != null)
|
||||
{
|
||||
var result = new JArray();
|
||||
if (a2 == null || a2.Count == 0) // If the system array is null or empty, return the template array
|
||||
return a1;
|
||||
else if (a1 != null)
|
||||
{
|
||||
if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array
|
||||
{ // with the system array
|
||||
return a2;
|
||||
}
|
||||
else // The arrays are keyed, merge them by key
|
||||
{
|
||||
for (int i = 0; i < a1.Count(); i++)
|
||||
{
|
||||
var a1Dev = a1[i];
|
||||
// Try to get a system device and if found, merge it onto template
|
||||
var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value<int>("uid") == tmplDev.Value<int>("uid"));
|
||||
if (a2Match != null)
|
||||
{
|
||||
var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match));
|
||||
result.Add(mergedItem);
|
||||
}
|
||||
else
|
||||
result.Add(a1Dev);
|
||||
}
|
||||
}
|
||||
if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array
|
||||
{ // with the system array
|
||||
return a2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Helper for using with JTokens. Converts to JObject
|
||||
/// </summary>
|
||||
static JObject Merge(JToken t1, JToken t2, string path)
|
||||
{
|
||||
return Merge(JObject.FromObject(t1), JObject.FromObject(t2), path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge o2 onto o1
|
||||
/// </summary>
|
||||
/// <param name="o1"></param>
|
||||
/// <param name="o2"></param>
|
||||
/// <param name="path"></param>
|
||||
static JObject Merge(JObject o1, JObject o2, string path)
|
||||
{
|
||||
foreach (var o2Prop in o2)
|
||||
else // The arrays are keyed, merge them by key
|
||||
{
|
||||
var propKey = o2Prop.Key;
|
||||
var o1Value = o1[propKey];
|
||||
var o2Value = o2[propKey];
|
||||
|
||||
// if the property doesn't exist on o1, then add it.
|
||||
if (o1Value == null)
|
||||
for (int i = 0; i < a1.Count(); i++)
|
||||
{
|
||||
o1.Add(propKey, o2Value);
|
||||
}
|
||||
// otherwise merge them
|
||||
else
|
||||
{
|
||||
// Drill down
|
||||
var propPath = String.Format("{0}.{1}", path, propKey);
|
||||
try
|
||||
var a1Dev = a1[i];
|
||||
// Try to get a system device and if found, merge it onto template
|
||||
var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value<int>("uid") == tmplDev.Value<int>("uid"));
|
||||
if (a2Match != null)
|
||||
{
|
||||
|
||||
if (o1Value is JArray)
|
||||
{
|
||||
if (o2Value is JArray)
|
||||
{
|
||||
o1Value.Replace(MergeArraysOnTopLevelProperty(o1Value as JArray, o2Value as JArray, "key", propPath));
|
||||
}
|
||||
}
|
||||
else if (o2Prop.Value.HasValues && o1Value.HasValues)
|
||||
{
|
||||
o1Value.Replace(Merge(JObject.FromObject(o1Value), JObject.FromObject(o2Value), propPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
o1Value.Replace(o2Prop.Value);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"Cannot merge items at path {propPath}: \r{e}");
|
||||
var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match));
|
||||
result.Add(mergedItem);
|
||||
}
|
||||
else
|
||||
result.Add(a1Dev);
|
||||
}
|
||||
}
|
||||
return o1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Helper for using with JTokens. Converts to JObject
|
||||
/// </summary>
|
||||
static JObject Merge(JToken t1, JToken t2, string path)
|
||||
{
|
||||
return Merge(JObject.FromObject(t1), JObject.FromObject(t2), path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge o2 onto o1
|
||||
/// </summary>
|
||||
/// <param name="o1"></param>
|
||||
/// <param name="o2"></param>
|
||||
/// <param name="path"></param>
|
||||
static JObject Merge(JObject o1, JObject o2, string path)
|
||||
{
|
||||
foreach (var o2Prop in o2)
|
||||
{
|
||||
var propKey = o2Prop.Key;
|
||||
var o1Value = o1[propKey];
|
||||
var o2Value = o2[propKey];
|
||||
|
||||
// if the property doesn't exist on o1, then add it.
|
||||
if (o1Value == null)
|
||||
{
|
||||
o1.Add(propKey, o2Value);
|
||||
}
|
||||
// otherwise merge them
|
||||
else
|
||||
{
|
||||
// Drill down
|
||||
var propPath = String.Format("{0}.{1}", path, propKey);
|
||||
try
|
||||
{
|
||||
|
||||
if (o1Value is JArray)
|
||||
{
|
||||
if (o2Value is JArray)
|
||||
{
|
||||
o1Value.Replace(MergeArraysOnTopLevelProperty(o1Value as JArray, o2Value as JArray, "key", propPath));
|
||||
}
|
||||
}
|
||||
else if (o2Prop.Value.HasValues && o1Value.HasValues)
|
||||
{
|
||||
o1Value.Replace(Merge(JObject.FromObject(o1Value), JObject.FromObject(o2Value), propPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
o1Value.Replace(o2Prop.Value);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Cannot merge items at path {0}: \r{1}", propPath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return o1;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user