mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-02-04 07:14:58 +00:00
Compare commits
38 Commits
v3.0.0-net
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1676ba7649 | ||
|
|
046b6fdb3b | ||
|
|
592607f3c8 | ||
|
|
ea0a779f8b | ||
|
|
86e4d2f7fb | ||
|
|
0069233e13 | ||
|
|
4048efb07e | ||
|
|
1dbac7d1c8 | ||
|
|
799d4c127c | ||
|
|
a6cd9a0571 | ||
|
|
da30424657 | ||
|
|
311452beac | ||
|
|
789113008e | ||
|
|
660836bd5a | ||
|
|
97b2ffed9c | ||
|
|
2bbefa062d | ||
|
|
3421b2f28c | ||
|
|
82889e9794 | ||
|
|
1dcd4e328c | ||
|
|
e76369726d | ||
|
|
2bf0f2092b | ||
|
|
f8455d4110 | ||
|
|
c1eccfd790 | ||
|
|
f1a89161bc | ||
|
|
e59c50d0aa | ||
|
|
9d313d8c7c | ||
|
|
9813673b66 | ||
|
|
f006ed0076 | ||
|
|
ddbcc13c50 | ||
|
|
2a70fc678e | ||
|
|
056614cba1 | ||
|
|
5ff587a8c9 | ||
|
|
26c1baa1f8 | ||
|
|
2b15c2a56f | ||
|
|
a076d531bc | ||
|
|
5e880f0111 | ||
|
|
8f1fb86d37 | ||
|
|
2fa297a204 |
36
.github/workflows/unit-tests.yml
vendored
Normal file
36
.github/workflows/unit-tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Unit Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore tests/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj
|
||||
|
||||
- name: Build tests
|
||||
run: dotnet build tests/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj --no-restore
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test tests/PepperDash.Essentials.Core.Tests/PepperDash.Essentials.Core.Tests.csproj --no-build --verbosity normal --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./tests/PepperDash.Essentials.Core.Tests/TestResults/*/coverage.cobertura.xml
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
fail_ci_if_error: false
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -60,6 +60,15 @@ bld/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Test results and coverage
|
||||
TestResults/
|
||||
coverage/
|
||||
*.trx
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
coverage.cobertura.xml
|
||||
coverage-report/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
@@ -393,4 +402,5 @@ essentials-framework/Essentials Interfaces/PepperDash_Essentials_Interfaces/Pepp
|
||||
/._PepperDash.Essentials.sln
|
||||
.vscode/settings.json
|
||||
_site/
|
||||
api/
|
||||
api/
|
||||
*.DS_Store
|
||||
|
||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"ms-dotnettools.csharp",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"vivaxy.vscode-conventional-commits",
|
||||
"mhutchie.git-graph"
|
||||
]
|
||||
}
|
||||
@@ -36,6 +36,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C8EAB8E7-4F14-4E5C-8D23-1A3956D46DE8}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core.Tests", "tests\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{A1234567-8901-2345-6789-ABCDEF012345}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
|
||||
@@ -79,6 +83,12 @@ Global
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.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
|
||||
{A1234567-8901-2345-6789-ABCDEF012345}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1234567-8901-2345-6789-ABCDEF012345}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1234567-8901-2345-6789-ABCDEF012345}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1234567-8901-2345-6789-ABCDEF012345}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1234567-8901-2345-6789-ABCDEF012345}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1234567-8901-2345-6789-ABCDEF012345}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -90,6 +100,7 @@ Global
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{A1234567-8901-2345-6789-ABCDEF012345} = {C8EAB8E7-4F14-4E5C-8D23-1A3956D46DE8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
||||
|
||||
188
docs/testing/QuickStart.md
Normal file
188
docs/testing/QuickStart.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Quick Start Guide: Unit Testing
|
||||
|
||||
This guide helps you get started with writing unit tests for PepperDash Essentials components.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 8 SDK
|
||||
- Visual Studio 2022 or VS Code with C# extension
|
||||
|
||||
## Running Existing Tests
|
||||
|
||||
```bash
|
||||
# Navigate to the test project
|
||||
cd tests/PepperDash.Essentials.Core.Tests
|
||||
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run with coverage
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
```
|
||||
|
||||
## Creating Your First Test
|
||||
|
||||
### 1. Identify the Component
|
||||
|
||||
Choose a component that has business logic you want to test. Look for classes that:
|
||||
- Have clear inputs and outputs
|
||||
- Contain conditional logic or algorithms
|
||||
- Are used by multiple parts of the system
|
||||
|
||||
### 2. Create Test File
|
||||
|
||||
Create a new test file in `tests/PepperDash.Essentials.Core.Tests/Unit/`:
|
||||
|
||||
```csharp
|
||||
using PepperDash.Essentials.Core.Tests.Abstractions;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Unit
|
||||
{
|
||||
public class YourComponentTests
|
||||
{
|
||||
[Fact]
|
||||
public void YourMethod_WithValidInput_ReturnsExpectedResult()
|
||||
{
|
||||
// Arrange
|
||||
var component = new YourComponent();
|
||||
var input = "test data";
|
||||
|
||||
// Act
|
||||
var result = component.YourMethod(input);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("expected result", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Abstract Dependencies
|
||||
|
||||
If your component uses Crestron SDK or other external dependencies:
|
||||
|
||||
```csharp
|
||||
// Create interface in Abstractions/
|
||||
public interface IYourDependency
|
||||
{
|
||||
string DoSomething(string input);
|
||||
}
|
||||
|
||||
// Modify your component to use the interface
|
||||
public class YourComponent
|
||||
{
|
||||
private readonly IYourDependency _dependency;
|
||||
|
||||
public YourComponent(IYourDependency dependency)
|
||||
{
|
||||
_dependency = dependency;
|
||||
}
|
||||
|
||||
public string YourMethod(string input)
|
||||
{
|
||||
return _dependency.DoSomething(input);
|
||||
}
|
||||
}
|
||||
|
||||
// Test with mocks
|
||||
[Fact]
|
||||
public void YourMethod_CallsDependency_ReturnsResult()
|
||||
{
|
||||
// Arrange
|
||||
var mockDependency = new Mock<IYourDependency>();
|
||||
mockDependency.Setup(x => x.DoSomething("input")).Returns("output");
|
||||
var component = new YourComponent(mockDependency.Object);
|
||||
|
||||
// Act
|
||||
var result = component.YourMethod("input");
|
||||
|
||||
// Assert
|
||||
Assert.Equal("output", result);
|
||||
mockDependency.Verify(x => x.DoSomething("input"), Times.Once);
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Testing Exceptions
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Constructor_WithNullDependency_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new YourComponent(null));
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Async Methods
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task YourAsyncMethod_WithValidInput_ReturnsExpectedResult()
|
||||
{
|
||||
// Arrange
|
||||
var component = new YourComponent();
|
||||
|
||||
// Act
|
||||
var result = await component.YourAsyncMethod();
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Collections
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void GetItems_ReturnsExpectedCount()
|
||||
{
|
||||
// Arrange
|
||||
var component = new YourComponent();
|
||||
|
||||
// Act
|
||||
var items = component.GetItems();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, items.Count());
|
||||
Assert.Contains(items, x => x.Name == "Expected Item");
|
||||
}
|
||||
```
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
1. **Testing Implementation Details** - Test behavior, not internal implementation
|
||||
2. **Overly Complex Tests** - Keep tests simple and focused on one thing
|
||||
3. **Not Testing Edge Cases** - Include null values, empty collections, boundary conditions
|
||||
4. **Ignoring Test Performance** - Tests should run quickly to enable fast feedback
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### In Visual Studio
|
||||
- Set breakpoints in test methods
|
||||
- Use Test Explorer to run individual tests
|
||||
- Check test output window for detailed information
|
||||
|
||||
### From Command Line
|
||||
```bash
|
||||
# Run specific test
|
||||
dotnet test --filter "YourComponentTests.YourMethod_WithValidInput_ReturnsExpectedResult"
|
||||
|
||||
# Run with detailed output
|
||||
dotnet test --verbosity diagnostic
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review Examples** - Look at `TestableActionSequenceTests.cs` for comprehensive examples
|
||||
2. **Start Small** - Begin with simple components and build up complexity
|
||||
3. **Read Documentation** - Check `docs/testing/README.md` for detailed patterns
|
||||
4. **Get Feedback** - Have your tests reviewed by team members
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check existing test examples in the codebase
|
||||
- Review the full testing documentation
|
||||
- Ask team members for guidance on complex scenarios
|
||||
- Consider pair programming for your first few tests
|
||||
255
docs/testing/README.md
Normal file
255
docs/testing/README.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Unit Testing Infrastructure
|
||||
|
||||
This document outlines the unit testing infrastructure and patterns for PepperDash Essentials.
|
||||
|
||||
## Overview
|
||||
|
||||
The testing infrastructure is designed to enable comprehensive testing of business logic while abstracting away Crestron SDK dependencies. This allows for:
|
||||
|
||||
- Fast, reliable unit tests that don't require hardware
|
||||
- Isolated testing of business logic
|
||||
- Better code quality through testability
|
||||
- Easier debugging and maintenance
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── PepperDash.Essentials.Core.Tests/
|
||||
│ ├── Abstractions/ # Interface abstractions for SDK dependencies
|
||||
│ ├── TestableComponents/ # Demonstration components showing abstraction patterns
|
||||
│ ├── Unit/ # Unit test files
|
||||
│ └── GlobalUsings.cs # Global using statements for tests
|
||||
```
|
||||
|
||||
## Test Project Configuration
|
||||
|
||||
The test projects use:
|
||||
- **.NET 8** - Modern testing framework with better performance
|
||||
- **xUnit** - Primary testing framework
|
||||
- **Moq** - Mocking framework for dependencies
|
||||
- **Coverlet** - Code coverage analysis
|
||||
|
||||
## Abstraction Patterns
|
||||
|
||||
### Interface Segregation
|
||||
|
||||
Create focused interfaces that abstract SDK functionality:
|
||||
|
||||
```csharp
|
||||
public interface IThreadService
|
||||
{
|
||||
void Sleep(int milliseconds);
|
||||
object CreateAndStartThread(Func<object, object> threadFunction, object parameter);
|
||||
void AbortThread(object thread);
|
||||
bool IsThreadRunning(object thread);
|
||||
}
|
||||
|
||||
public interface ILogger
|
||||
{
|
||||
void LogDebug(object source, string message, params object[] args);
|
||||
void LogVerbose(object source, string message, params object[] args);
|
||||
}
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Components should accept dependencies via constructor injection:
|
||||
|
||||
```csharp
|
||||
public class TestableActionSequence
|
||||
{
|
||||
private readonly IQueue<TestableSequencedAction> _actionQueue;
|
||||
private readonly IThreadService _threadService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public TestableActionSequence(
|
||||
IQueue<TestableSequencedAction> actionQueue,
|
||||
IThreadService threadService,
|
||||
ILogger logger,
|
||||
List<TestableSequencedAction> actions)
|
||||
{
|
||||
_actionQueue = actionQueue ?? throw new ArgumentNullException(nameof(actionQueue));
|
||||
_threadService = threadService ?? throw new ArgumentNullException(nameof(threadService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Test Organization
|
||||
|
||||
- **One test class per component**
|
||||
- **Descriptive test method names** that describe the scenario and expected outcome
|
||||
- **Arrange-Act-Assert pattern** for clear test structure
|
||||
|
||||
### Example Test Structure
|
||||
|
||||
```csharp
|
||||
public class TestableActionSequenceTests
|
||||
{
|
||||
private readonly Mock<IQueue<TestableSequencedAction>> _mockQueue;
|
||||
private readonly Mock<IThreadService> _mockThreadService;
|
||||
private readonly Mock<ILogger> _mockLogger;
|
||||
private readonly TestableActionSequence _actionSequence;
|
||||
|
||||
public TestableActionSequenceTests()
|
||||
{
|
||||
// Arrange - Set up mocks and dependencies
|
||||
_mockQueue = new Mock<IQueue<TestableSequencedAction>>();
|
||||
_mockThreadService = new Mock<IThreadService>();
|
||||
_mockLogger = new Mock<ILogger>();
|
||||
|
||||
_actionSequence = new TestableActionSequence(
|
||||
_mockQueue.Object,
|
||||
_mockThreadService.Object,
|
||||
_mockLogger.Object,
|
||||
new List<TestableSequencedAction>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_WhenNoThreadRunning_StartsNewThread()
|
||||
{
|
||||
// Arrange
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(It.IsAny<object>())).Returns(false);
|
||||
|
||||
// Act
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Starting Action Sequence"), Times.Once);
|
||||
_mockThreadService.Verify(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null), Times.Once);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Test Naming Convention
|
||||
|
||||
Use descriptive names that follow the pattern:
|
||||
`MethodName_Scenario_ExpectedBehavior`
|
||||
|
||||
Examples:
|
||||
- `StartSequence_WhenNoThreadRunning_StartsNewThread`
|
||||
- `Constructor_WithNullQueue_ThrowsArgumentNullException`
|
||||
- `StopSequence_SetsAllowActionsToFalseAndAbortsThread`
|
||||
|
||||
## Running Tests
|
||||
|
||||
### From Command Line
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
dotnet test
|
||||
|
||||
# Run tests with coverage
|
||||
dotnet test --collect:"XPlat Code Coverage"
|
||||
|
||||
# Run specific test class
|
||||
dotnet test --filter "TestableActionSequenceTests"
|
||||
|
||||
# Run with verbose output
|
||||
dotnet test --verbosity normal
|
||||
```
|
||||
|
||||
### From Visual Studio
|
||||
|
||||
- Use Test Explorer to run and debug tests
|
||||
- Right-click on test methods to run individually
|
||||
- Use "Run Tests in Parallel" for faster execution
|
||||
|
||||
## Code Coverage
|
||||
|
||||
The test projects include Coverlet for code coverage analysis. After running tests with coverage:
|
||||
|
||||
```bash
|
||||
# Generate coverage report
|
||||
dotnet tool install -g reportgenerator
|
||||
reportgenerator -reports:"coverage.cobertura.xml" -targetdir:"coverage-report" -reporttypes:Html
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Test Independence
|
||||
- Each test should be independent and able to run in isolation
|
||||
- Use fresh mock objects for each test
|
||||
- Don't rely on test execution order
|
||||
|
||||
### 2. Mocking Guidelines
|
||||
- Mock external dependencies (SDK calls, file system, network)
|
||||
- Don't mock the component under test
|
||||
- Use strict mocks when behavior verification is important
|
||||
|
||||
### 3. Assertions
|
||||
- Make assertions specific and meaningful
|
||||
- Test both positive and negative scenarios
|
||||
- Verify state changes and behavior calls
|
||||
|
||||
### 4. Test Data
|
||||
- Use meaningful test data that represents real scenarios
|
||||
- Consider edge cases and boundary conditions
|
||||
- Use constants or factory methods for complex test data
|
||||
|
||||
## Migrating Existing Code
|
||||
|
||||
To make existing components testable:
|
||||
|
||||
1. **Identify SDK Dependencies** - Look for direct Crestron SDK usage
|
||||
2. **Extract Interfaces** - Create abstractions for SDK functionality
|
||||
3. **Inject Dependencies** - Modify constructors to accept abstractions
|
||||
4. **Create Tests** - Write comprehensive unit tests for business logic
|
||||
5. **Implement Wrappers** - Create concrete implementations for production use
|
||||
|
||||
## Example Implementation Wrapper
|
||||
|
||||
When implementing the abstractions for production use:
|
||||
|
||||
```csharp
|
||||
public class CrestronThreadService : IThreadService
|
||||
{
|
||||
public void Sleep(int milliseconds)
|
||||
{
|
||||
Thread.Sleep(milliseconds);
|
||||
}
|
||||
|
||||
public object CreateAndStartThread(Func<object, object> threadFunction, object parameter)
|
||||
{
|
||||
var thread = new Thread(threadFunction, parameter, Thread.eThreadStartOptions.Running);
|
||||
return thread;
|
||||
}
|
||||
|
||||
public void AbortThread(object thread)
|
||||
{
|
||||
if (thread is Thread crestronThread)
|
||||
{
|
||||
crestronThread.Abort();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsThreadRunning(object thread)
|
||||
{
|
||||
return thread is Thread crestronThread &&
|
||||
crestronThread.ThreadState == Thread.eThreadStates.ThreadRunning;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
The testing infrastructure can be extended with:
|
||||
|
||||
- **Integration test support** for end-to-end scenarios
|
||||
- **Performance testing** utilities
|
||||
- **Test data builders** for complex object creation
|
||||
- **Custom assertions** for domain-specific validations
|
||||
- **Automated test generation** for common patterns
|
||||
|
||||
## Getting Help
|
||||
|
||||
For questions about testing patterns or infrastructure:
|
||||
|
||||
1. Review existing test examples in the `tests/` directory
|
||||
2. Check this documentation for guidance
|
||||
3. Consult the team for complex abstraction scenarios
|
||||
4. Consider the impact on existing plugin interfaces before major changes
|
||||
@@ -2,7 +2,14 @@
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
public interface IDisplay: IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName
|
||||
/// <summary>
|
||||
/// Interface for display devices that can be controlled and monitored.
|
||||
/// This interface combines functionality for feedback, routing, power control,
|
||||
/// warming/cooling, usage tracking, and key name management.
|
||||
/// It is designed to be implemented by devices that require these capabilities,
|
||||
/// such as projectors, displays, and other visual output devices.
|
||||
/// </summary>
|
||||
public interface IDisplay : IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that provide audio meter feedback.
|
||||
/// This interface is used to standardize access to meter feedback across different devices.
|
||||
/// </summary>
|
||||
public interface IMeterFeedback
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the meter feedback for the device.
|
||||
/// This property provides an IntFeedback that represents the current audio level or meter value.
|
||||
/// </summary>
|
||||
IntFeedback MeterFeedback { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Crestron.SimplSharpPro;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core;
|
||||
@@ -33,11 +35,11 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
|
||||
string SystemUuid { get; }
|
||||
|
||||
BoolFeedback ApiOnlineAndAuthorized { get;}
|
||||
BoolFeedback ApiOnlineAndAuthorized { get; }
|
||||
|
||||
void SendMessageObject(IMobileControlMessage o);
|
||||
|
||||
void AddAction<T>(T messenger, Action<string, string, JToken> action) where T:IMobileControlMessenger;
|
||||
void AddAction<T>(T messenger, Action<string, string, JToken> action) where T : IMobileControlMessenger;
|
||||
|
||||
void RemoveAction(string key);
|
||||
|
||||
@@ -45,14 +47,14 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
|
||||
bool CheckForDeviceMessenger(string key);
|
||||
|
||||
IMobileControlRoomMessenger GetRoomMessenger(string key);
|
||||
IMobileControlRoomMessenger GetRoomMessenger(string key);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a mobile control messenger
|
||||
/// </summary>
|
||||
public interface IMobileControlMessenger: IKeyed
|
||||
public interface IMobileControlMessenger : IKeyed
|
||||
{
|
||||
IMobileControl AppServerController { get; }
|
||||
string MessagePath { get; }
|
||||
@@ -104,16 +106,47 @@ namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
|
||||
public interface IMobileControlAction
|
||||
{
|
||||
IMobileControlMessenger Messenger { get; }
|
||||
IMobileControlMessenger Messenger { get; }
|
||||
|
||||
Action<string,string, JToken> Action { get; }
|
||||
Action<string, string, JToken> Action { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a MobileControl Touchpanel Controller
|
||||
/// </summary>
|
||||
public interface IMobileControlTouchpanelController : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// The default room key for the controller
|
||||
/// </summary>
|
||||
string DefaultRoomKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application URL for the controller
|
||||
/// </summary>
|
||||
/// <param name="url">The application URL</param>
|
||||
void SetAppUrl(string url);
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the controller uses a direct server connection
|
||||
/// </summary>
|
||||
bool UseDirectServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the controller is a Zoom Room controller
|
||||
/// </summary>
|
||||
bool ZoomRoomController { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a MobileControl Crestron Touchpanel Controller
|
||||
/// This interface extends the IMobileControlTouchpanelController to include connected IP information
|
||||
/// </summary>
|
||||
public interface IMobileControlCrestronTouchpanelController : IMobileControlTouchpanelController
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a collection of connected IP information for the touchpanel controller
|
||||
/// </summary>
|
||||
ReadOnlyCollection<ConnectedIpInformation> ConnectedIps { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Crestron.SimplSharpPro.DM.Streaming;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a collection of network port information and provides notifications when the information changes.
|
||||
/// </summary>
|
||||
/// <remarks>This interface is designed to provide access to a list of network port details and to notify
|
||||
/// subscribers when the port information is updated. Implementations of this interface should ensure that the <see
|
||||
/// cref="PortInformationChanged"/> event is raised whenever the <see cref="NetworkPorts"/> collection
|
||||
/// changes.</remarks>
|
||||
public interface INvxNetworkPortInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the port information changes.
|
||||
/// </summary>
|
||||
/// <remarks>This event is triggered whenever there is a change in the port information, such as
|
||||
/// updates to port settings or status. Subscribers can handle this event to respond to such changes.</remarks>
|
||||
event EventHandler PortInformationChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of network port information associated with the current instance.
|
||||
/// </summary>
|
||||
/// <remarks>The collection provides information about the network ports, such as their status,
|
||||
/// configuration, or other relevant details. The returned list is read-only and cannot be modified
|
||||
/// directly.</remarks>
|
||||
List<NvxNetworkPortInformation> NetworkPorts { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about a network port, including its configuration and associated system details.
|
||||
/// </summary>
|
||||
/// <remarks>This class provides properties to describe various attributes of a network port, such as its
|
||||
/// name, description, VLAN configuration, and management IP address. It is typically used to store and retrieve
|
||||
/// metadata about network ports in a managed environment.</remarks>
|
||||
public class NvxNetworkPortInformation
|
||||
{
|
||||
private readonly DmNvxBaseClass.DmNvx35xNetwork.DmNvxNetworkLldpPort port;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the device port.
|
||||
/// </summary>
|
||||
public uint DevicePortIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the port used for communication.
|
||||
/// </summary>
|
||||
public string PortName => port.PortNameFeedback.StringValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the port.
|
||||
/// </summary>
|
||||
public string PortDescription => port.PortNameDescriptionFeedback.StringValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the VLAN (Virtual Local Area Network).
|
||||
/// </summary>
|
||||
public string VlanName => port.VlanNameFeedback.StringValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IP management address associated with the port.
|
||||
/// </summary>
|
||||
public string IpManagementAddress => port.IpManagementAddressFeedback.StringValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the system as reported by the associated port.
|
||||
/// </summary>
|
||||
public string SystemName => port.SystemNameFeedback.StringValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the system name.
|
||||
/// </summary>
|
||||
public string SystemNameDescription => port.SystemNameDescriptionFeedback.StringValue;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NvxNetworkPortInformation"/> class with the specified network port
|
||||
/// and device port index.
|
||||
/// </summary>
|
||||
/// <param name="port">The network port associated with the device. Cannot be <see langword="null"/>.</param>
|
||||
/// <param name="devicePortIndex">The index of the device port.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="port"/> is <see langword="null"/>.</exception>
|
||||
public NvxNetworkPortInformation(DmNvxBaseClass.DmNvx35xNetwork.DmNvxNetworkLldpPort port, uint devicePortIndex)
|
||||
{
|
||||
this.port = port ?? throw new ArgumentNullException(nameof(port), "Port cannot be null");
|
||||
DevicePortIndex = devicePortIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that provide state feedback.
|
||||
/// This interface is used to standardize access to state feedback across different devices.
|
||||
/// </summary>
|
||||
public interface IStateFeedback
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the state feedback for the device.
|
||||
/// This property provides a BoolFeedback that represents the current state (on/off) of the device.
|
||||
/// </summary>
|
||||
BoolFeedback StateFeedback { get; }
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,34 @@ using PepperDash.Essentials.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a destination item in a routing system that can receive audio/video signals.
|
||||
/// Contains information about the destination device, its properties, and location settings.
|
||||
/// </summary>
|
||||
public class DestinationListItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the key identifier for the sink device that this destination represents.
|
||||
/// </summary>
|
||||
[JsonProperty("sinkKey")]
|
||||
public string SinkKey { get; set; }
|
||||
|
||||
private EssentialsDevice _sinkDevice;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual device instance for this destination.
|
||||
/// Lazily loads the device from the DeviceManager using the SinkKey.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public EssentialsDevice SinkDevice
|
||||
{
|
||||
get { return _sinkDevice ?? (_sinkDevice = DeviceManager.GetDeviceForKey(SinkKey) as EssentialsDevice); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the preferred display name for this destination.
|
||||
/// Returns the custom Name if set, otherwise returns the SinkDevice name, or "---" if no device is found.
|
||||
/// </summary>
|
||||
[JsonProperty("preferredName")]
|
||||
public string PreferredName
|
||||
{
|
||||
@@ -32,31 +47,78 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the custom name for this destination.
|
||||
/// If set, this name will be used as the PreferredName instead of the device name.
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this destination should be included in destination lists.
|
||||
/// </summary>
|
||||
[JsonProperty("includeInDestinationList")]
|
||||
public bool IncludeInDestinationList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display order for this destination in lists.
|
||||
/// Lower values appear first in sorted lists.
|
||||
/// </summary>
|
||||
[JsonProperty("order")]
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the surface location identifier for this destination.
|
||||
/// Used to specify which surface or screen this destination is located on.
|
||||
/// </summary>
|
||||
[JsonProperty("surfaceLocation")]
|
||||
public int SurfaceLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical location position for this destination.
|
||||
/// Used for spatial positioning in multi-display configurations.
|
||||
/// </summary>
|
||||
[JsonProperty("verticalLocation")]
|
||||
public int VerticalLocation { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal location position for this destination.
|
||||
/// Used for spatial positioning in multi-display configurations.
|
||||
/// </summary>
|
||||
[JsonProperty("horizontalLocation")]
|
||||
public int HorizontalLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the signal type that this destination can receive (Audio, Video, AudioVideo, etc.).
|
||||
/// </summary>
|
||||
[JsonProperty("sinkType")]
|
||||
public eRoutingSignalType SinkType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this destination is used for codec content sharing.
|
||||
/// </summary>
|
||||
[JsonProperty("isCodecContentDestination")]
|
||||
public bool isCodecContentDestination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this destination is used for program audio output.
|
||||
/// </summary>
|
||||
[JsonProperty("isProgramAudioDestination")]
|
||||
public bool isProgramAudioDestination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this destination supports USB connections.
|
||||
/// Indicates if the destination can handle USB functionality, such as USB signal routing or device connections.
|
||||
/// This property is used to determine compatibility with USB-based devices or systems.
|
||||
/// </summary>
|
||||
[JsonProperty("supportsUsb")]
|
||||
public bool SupportsUsb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the destination port associated with this destination item
|
||||
/// This is used to identify the specific port on the destination device that this item refers to for advanced routing
|
||||
/// </summary>
|
||||
[JsonProperty("destinationPortKey")]
|
||||
public string DestinationPortKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,15 @@ using PepperDash.Essentials.Core.Devices;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a level control item in a list, which can be used to control volume or mute functionality.
|
||||
/// </summary>
|
||||
public class LevelControlListItem : AudioControlListItemBase
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A reference to the IBasicVolumeWithFeedback device for control.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IBasicVolumeWithFeedback LevelControl
|
||||
{
|
||||
@@ -55,7 +60,7 @@ namespace PepperDash.Essentials.Core
|
||||
{
|
||||
get
|
||||
{
|
||||
if(string.IsNullOrEmpty(ItemKey)) return ParentDeviceKey;
|
||||
if (string.IsNullOrEmpty(ItemKey)) return ParentDeviceKey;
|
||||
else
|
||||
{
|
||||
return DeviceManager.AllDevices.
|
||||
@@ -70,13 +75,39 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonProperty("type")]
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public eLevelControlType Type { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the item is a mic or not.
|
||||
/// </summary>
|
||||
[JsonProperty("isMic", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? IsMic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the item should show the raw level in the UI.
|
||||
/// </summary>
|
||||
[JsonProperty("showRawLevel", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? ShowRawLevel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the type of level control item.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum eLevelControlType
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that the item is a level control only
|
||||
/// </summary>
|
||||
Level = 1,
|
||||
/// <summary>
|
||||
/// Indicates that the item is a mute control only
|
||||
/// </summary>
|
||||
Mute = 2,
|
||||
/// <summary>
|
||||
/// Indicates that the item is both a level and mute control
|
||||
/// </summary>
|
||||
LevelAndMute = Level | Mute,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using PepperDash.Core;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Defines the type of source list item, which can be a route, off, or other.
|
||||
/// This is used to categorize the source list items in a room.
|
||||
/// The type is serialized to JSON and can be used to determine how the item should be displayed or handled in the UI.
|
||||
/// </summary>
|
||||
public enum eSourceListItemType
|
||||
{
|
||||
Route, Off, Other, SomethingAwesomerThanThese
|
||||
/// <summary>
|
||||
/// Represents a typical route.
|
||||
/// </summary>
|
||||
Route,
|
||||
/// <summary>
|
||||
/// Represents an off route.
|
||||
/// </summary>
|
||||
Off,
|
||||
/// <summary>
|
||||
/// Represents some other type of route
|
||||
/// </summary>
|
||||
Other,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -18,6 +31,9 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public class SourceListItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The key of the source item, which is used to identify it in the DeviceManager
|
||||
/// </summary>
|
||||
[JsonProperty("sourceKey")]
|
||||
public string SourceKey { get; set; }
|
||||
|
||||
@@ -117,6 +133,9 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonProperty("disableRoutedSharing")]
|
||||
public bool DisableRoutedSharing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
[JsonProperty("destinations")]
|
||||
public List<eSourceListItemDestinationTypes> Destinations { get; set; }
|
||||
/// <summary>
|
||||
@@ -149,31 +168,76 @@ namespace PepperDash.Essentials.Core
|
||||
[JsonProperty("disableSimpleRouting")]
|
||||
public bool DisableSimpleRouting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the device that provides video sync for this source item
|
||||
/// </summary>
|
||||
[JsonProperty("syncProviderDeviceKey")]
|
||||
public string SyncProviderDeviceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the source supports USB connections
|
||||
/// </summary>
|
||||
[JsonProperty("supportsUsb")]
|
||||
public bool SupportsUsb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the source port associated with this source item
|
||||
/// This is used to identify the specific port on the source device that this item refers to for advanced routing
|
||||
/// </summary>
|
||||
[JsonProperty("sourcePortKey")]
|
||||
public string SourcePortKey { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor for SourceListItem, initializes the Icon to "Blank"
|
||||
/// </summary>
|
||||
public SourceListItem()
|
||||
{
|
||||
Icon = "Blank";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the SourceListItem, including the SourceKey and Name
|
||||
/// </summary>
|
||||
/// <returns> A string representation of the SourceListItem</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{SourceKey}:{Name}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a route in a source list item, which defines the source and destination keys and the type of signal being routed
|
||||
/// </summary>
|
||||
public class SourceRouteListItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The key of the source device to route from
|
||||
/// </summary>
|
||||
[JsonProperty("sourceKey")]
|
||||
public string SourceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the source port to route from
|
||||
/// </summary>
|
||||
[JsonProperty("sourcePortKey")]
|
||||
public string SourcePortKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the destination device to route to
|
||||
/// </summary>
|
||||
[JsonProperty("destinationKey")]
|
||||
public string DestinationKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The key of the destination port to route to
|
||||
/// </summary>
|
||||
[JsonProperty("destinationPortKey")]
|
||||
public string DestinationPortKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of signal being routed, such as audio or video
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public eRoutingSignalType Type { get; set; }
|
||||
}
|
||||
@@ -183,15 +247,85 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
public enum eSourceListItemDestinationTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// Default display, used for the main video output in a room
|
||||
/// </summary>
|
||||
defaultDisplay,
|
||||
/// <summary>
|
||||
/// Left display
|
||||
/// </summary>
|
||||
leftDisplay,
|
||||
/// <summary>
|
||||
/// Right display
|
||||
/// </summary>
|
||||
rightDisplay,
|
||||
/// <summary>
|
||||
/// Center display
|
||||
/// </summary>
|
||||
centerDisplay,
|
||||
/// <summary>
|
||||
/// Program audio, used for the main audio output in a room
|
||||
/// </summary>
|
||||
programAudio,
|
||||
/// <summary>
|
||||
/// Codec content, used for sharing content to the far end in a video call
|
||||
/// </summary>
|
||||
codecContent,
|
||||
/// <summary>
|
||||
/// Front left display, used for rooms with multiple displays
|
||||
/// </summary>
|
||||
frontLeftDisplay,
|
||||
/// <summary>
|
||||
/// Front right display, used for rooms with multiple displays
|
||||
/// </summary>
|
||||
frontRightDisplay,
|
||||
/// <summary>
|
||||
/// Rear left display, used for rooms with multiple displays
|
||||
/// </summary>
|
||||
rearLeftDisplay,
|
||||
/// <summary>
|
||||
/// Rear right display, used for rooms with multiple displays
|
||||
/// </summary>
|
||||
rearRightDisplay,
|
||||
/// <summary>
|
||||
/// Auxiliary display 1, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay1,
|
||||
/// <summary>
|
||||
/// Auxiliary display 2, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay2,
|
||||
/// <summary>
|
||||
/// Auxiliary display 3, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay3,
|
||||
/// <summary>
|
||||
/// Auxiliary display 4, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay4,
|
||||
/// <summary>
|
||||
/// Auxiliary display 5, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay5,
|
||||
/// <summary>
|
||||
/// Auxiliary display 6, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay6,
|
||||
/// <summary>
|
||||
/// Auxiliary display 7, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay7,
|
||||
/// <summary>
|
||||
/// Auxiliary display 8, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay8,
|
||||
/// <summary>
|
||||
/// Auxiliary display 9, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay9,
|
||||
/// <summary>
|
||||
/// Auxiliary display 10, used for additional displays in a room
|
||||
/// </summary>
|
||||
auxDisplay10,
|
||||
}
|
||||
}
|
||||
29
src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs
Normal file
29
src/PepperDash.Essentials.Core/Routing/ICurrentSources.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using PepperDash.Essentials.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// The current sources for the room, keyed by eRoutingSignalType.
|
||||
/// This allows for multiple sources to be tracked, such as audio and video.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This interface is used to provide access to the current sources in a room,
|
||||
/// allowing for more complex routing scenarios where multiple signal types are involved.
|
||||
/// </remarks>
|
||||
public interface ICurrentSources
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current sources for the room, keyed by eRoutingSignalType.
|
||||
/// This dictionary contains the current source for each signal type, such as audio, video, and control signals.
|
||||
/// </summary>
|
||||
Dictionary<eRoutingSignalType, SourceListItem> CurrentSources { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current source keys for the room, keyed by eRoutingSignalType.
|
||||
/// This dictionary contains the keys for the current source for each signal type, such as audio, video, and control signals.
|
||||
/// </summary>
|
||||
Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ using PepperDash.Essentials.Core.Routing;
|
||||
using PepperDash.Essentials.Core.Routing;
|
||||
using PepperDash.Essentials.Core.Routing.Interfaces
|
||||
*/
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
@@ -21,10 +23,24 @@ namespace PepperDash.Essentials.Core
|
||||
/// <summary>
|
||||
/// For rooms with a single presentation source, change event
|
||||
/// </summary>
|
||||
[Obsolete("Use ICurrentSources instead")]
|
||||
public interface IHasCurrentSourceInfoChange
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for the current source info, used to look up the source in the SourceList
|
||||
/// </summary>
|
||||
string CurrentSourceInfoKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current source info for the room, used to look up the source in the SourceList
|
||||
/// </summary>
|
||||
SourceListItem CurrentSourceInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event that is raised when the current source info changes.
|
||||
/// This is used to notify the system of changes to the current source info.
|
||||
/// The event handler receives the new source info and the type of change that occurred.
|
||||
/// </summary>
|
||||
event SourceInfoChangeHandler CurrentSourceChange;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
namespace PepperDash.Essentials.Core
|
||||
using PepperDash.Essentials.Core.Routing;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// For fixed-source endpoint devices
|
||||
/// </summary>
|
||||
public interface IRoutingSink : IRoutingInputs, IHasCurrentSourceInfoChange
|
||||
{
|
||||
{
|
||||
}
|
||||
|
||||
public interface IRoutingSinkWithInputPort :IRoutingSink
|
||||
/// <summary>
|
||||
/// For fixed-source endpoint devices with an input port
|
||||
/// </summary>
|
||||
public interface IRoutingSinkWithInputPort : IRoutingSink
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current input port for this routing sink.
|
||||
/// </summary>
|
||||
RoutingInputPort CurrentInputPort { get; }
|
||||
}
|
||||
/*/// <summary>
|
||||
/// For fixed-source endpoint devices
|
||||
/// </summary>
|
||||
public interface IRoutingSink<TSelector> : IRoutingInputs<TSelector>, IHasCurrentSourceInfoChange
|
||||
{
|
||||
void UpdateRouteRequest<TOutputSelector>(RouteRequest<TSelector, TOutputSelector> request);
|
||||
|
||||
RouteRequest<TSelector, TOutputSelector> GetRouteRequest<TOutputSelector>();
|
||||
}*/
|
||||
/// <summary>
|
||||
/// Interface for routing sinks that have access to the current source information.
|
||||
/// </summary>
|
||||
public interface IRoutingSinkWithCurrentSources : IRoutingSink, ICurrentSources
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,60 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum for camera control modes
|
||||
/// </summary>
|
||||
public enum eCameraControlMode
|
||||
{
|
||||
{
|
||||
/// <summary>
|
||||
/// Manual control mode, where the camera is controlled directly by the user or system
|
||||
/// </summary>
|
||||
Manual = 0,
|
||||
/// <summary>
|
||||
/// Off control mode, where the camera is turned off or disabled
|
||||
/// </summary>
|
||||
Off,
|
||||
/// <summary>
|
||||
/// Auto control mode, where the camera automatically adjusts settings based on the environment or conditions
|
||||
/// </summary>
|
||||
Auto
|
||||
}
|
||||
|
||||
|
||||
public interface IHasCameras
|
||||
/// <summary>
|
||||
/// Interface for devices that have cameras
|
||||
/// </summary>
|
||||
public interface IHasCameras : IKeyName
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is raised when a camera is selected
|
||||
/// </summary>
|
||||
event EventHandler<CameraSelectedEventArgs> CameraSelected;
|
||||
|
||||
/// <summary>
|
||||
/// List of cameras on the device. This should be a list of CameraBase objects
|
||||
/// </summary>
|
||||
List<CameraBase> Cameras { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected camera. This should be a CameraBase object
|
||||
/// </summary>
|
||||
CameraBase SelectedCamera { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback that indicates the currently selected camera
|
||||
/// </summary>
|
||||
StringFeedback SelectedCameraFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Selects a camera from the list of available cameras based on the provided key.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier or name of the camera to select.</param>
|
||||
void SelectCamera(string key);
|
||||
}
|
||||
|
||||
@@ -42,7 +73,14 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
/// </summary>
|
||||
public interface IHasCameraOff
|
||||
{
|
||||
/// <summary>
|
||||
/// Feedback that indicates whether the camera is off
|
||||
/// </summary>
|
||||
BoolFeedback CameraIsOffFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Turns the camera off, blanking the near end video
|
||||
/// </summary>
|
||||
void CameraOff();
|
||||
}
|
||||
|
||||
@@ -51,31 +89,71 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
/// </summary>
|
||||
public interface IHasCameraMute
|
||||
{
|
||||
/// <summary>
|
||||
/// Feedback that indicates whether the camera is muted
|
||||
/// </summary>
|
||||
BoolFeedback CameraIsMutedFeedback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mutes the camera video, preventing it from being sent to the far end
|
||||
/// </summary>
|
||||
void CameraMuteOn();
|
||||
|
||||
/// <summary>
|
||||
/// Unmutes the camera video, allowing it to be sent to the far end
|
||||
/// </summary>
|
||||
void CameraMuteOff();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the camera mute state. If the camera is muted, it will be unmuted, and vice versa.
|
||||
/// </summary>
|
||||
void CameraMuteToggle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that can mute and unmute their camera video, with an event for unmute requests
|
||||
/// </summary>
|
||||
public interface IHasCameraMuteWithUnmuteReqeust : IHasCameraMute
|
||||
{
|
||||
/// <summary>
|
||||
/// Event that is raised when a video unmute is requested, typically by the far end
|
||||
/// </summary>
|
||||
event EventHandler VideoUnmuteRequested;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event arguments for the CameraSelected event
|
||||
/// </summary>
|
||||
public class CameraSelectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The selected camera
|
||||
/// </summary>
|
||||
public CameraBase SelectedCamera { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for CameraSelectedEventArgs
|
||||
/// </summary>
|
||||
/// <param name="camera"></param>
|
||||
public CameraSelectedEventArgs(CameraBase camera)
|
||||
{
|
||||
SelectedCamera = camera;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that have a far end camera control
|
||||
/// </summary>
|
||||
public interface IHasFarEndCameraControl
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the far end camera, which is typically a CameraBase object that represents the camera at the far end of a call
|
||||
/// </summary>
|
||||
CameraBase FarEndCamera { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Feedback that indicates whether the far end camera is being controlled
|
||||
/// </summary>
|
||||
BoolFeedback ControllingFarEndCameraFeedback { get; }
|
||||
|
||||
}
|
||||
@@ -88,6 +166,9 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that have camera controls
|
||||
/// </summary>
|
||||
public interface IHasCameraControls
|
||||
{
|
||||
}
|
||||
@@ -108,8 +189,19 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
/// </summary>
|
||||
public interface IHasCameraPanControl : IHasCameraControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Pans the camera left
|
||||
/// </summary>
|
||||
void PanLeft();
|
||||
|
||||
/// <summary>
|
||||
/// Pans the camera right
|
||||
/// </summary>
|
||||
void PanRight();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the camera pan movement
|
||||
/// </summary>
|
||||
void PanStop();
|
||||
}
|
||||
|
||||
@@ -118,8 +210,19 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
/// </summary>
|
||||
public interface IHasCameraTiltControl : IHasCameraControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Tilts the camera down
|
||||
/// </summary>
|
||||
void TiltDown();
|
||||
|
||||
/// <summary>
|
||||
/// Tilts the camera up
|
||||
/// </summary>
|
||||
void TiltUp();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the camera tilt movement
|
||||
/// </summary>
|
||||
void TiltStop();
|
||||
}
|
||||
|
||||
@@ -128,8 +231,19 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
/// </summary>
|
||||
public interface IHasCameraZoomControl : IHasCameraControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Zooms the camera in
|
||||
/// </summary>
|
||||
void ZoomIn();
|
||||
|
||||
/// <summary>
|
||||
/// Zooms the camera out
|
||||
/// </summary>
|
||||
void ZoomOut();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the camera zoom movement
|
||||
/// </summary>
|
||||
void ZoomStop();
|
||||
}
|
||||
|
||||
@@ -138,25 +252,71 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
|
||||
/// </summary>
|
||||
public interface IHasCameraFocusControl : IHasCameraControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Focuses the camera near
|
||||
/// </summary>
|
||||
void FocusNear();
|
||||
|
||||
/// <summary>
|
||||
/// Focuses the camera far
|
||||
/// </summary>
|
||||
void FocusFar();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the camera focus movement
|
||||
/// </summary>
|
||||
void FocusStop();
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the camera's auto focus functionality, if available.
|
||||
/// </summary>
|
||||
void TriggerAutoFocus();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that have auto focus mode control
|
||||
/// </summary>
|
||||
public interface IHasAutoFocusMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the focus mode to auto or manual, or toggles between them.
|
||||
/// </summary>
|
||||
void SetFocusModeAuto();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the focus mode to manual, allowing for manual focus adjustments.
|
||||
/// </summary>
|
||||
void SetFocusModeManual();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the focus mode between auto and manual.
|
||||
/// </summary>
|
||||
void ToggleFocusMode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for devices that have camera auto mode control
|
||||
/// </summary>
|
||||
public interface IHasCameraAutoMode : IHasCameraControls
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables or disables the camera's auto mode, which may include automatic adjustments for focus, exposure, and other settings.
|
||||
/// </summary>
|
||||
void CameraAutoModeOn();
|
||||
|
||||
/// <summary>
|
||||
/// Disables the camera's auto mode, allowing for manual control of camera settings.
|
||||
/// </summary>
|
||||
void CameraAutoModeOff();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the camera's auto mode state. If the camera is in auto mode, it will switch to manual mode, and vice versa.
|
||||
/// </summary>
|
||||
void CameraAutoModeToggle();
|
||||
|
||||
/// <summary>
|
||||
/// Feedback that indicates whether the camera's auto mode is currently enabled.
|
||||
/// </summary>
|
||||
BoolFeedback CameraAutoModeIsOnFeedback { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,110 +1,199 @@
|
||||
using Crestron.SimplSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharpPro.DeviceSupport;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core;
|
||||
using PepperDash.Essentials.Core.Bridges;
|
||||
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
|
||||
using PepperDash.Essentials.Core.Routing;
|
||||
using Serilog.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Feedback = PepperDash.Essentials.Core.Feedback;
|
||||
|
||||
namespace PepperDash.Essentials.Devices.Common.Displays
|
||||
{
|
||||
public abstract class DisplayBase : EssentialsDevice, IDisplay
|
||||
/// <summary>
|
||||
/// Abstract base class for display devices that provides common display functionality
|
||||
/// including power control, input switching, and routing capabilities.
|
||||
/// </summary>
|
||||
public abstract class DisplayBase : EssentialsDevice, IDisplay, ICurrentSources
|
||||
{
|
||||
private RoutingInputPort _currentInputPort;
|
||||
public RoutingInputPort CurrentInputPort
|
||||
{
|
||||
get
|
||||
{
|
||||
return _currentInputPort;
|
||||
}
|
||||
private RoutingInputPort _currentInputPort;
|
||||
|
||||
protected set
|
||||
{
|
||||
if (_currentInputPort == value) return;
|
||||
/// <summary>
|
||||
/// Gets or sets the current input port that is selected on the display.
|
||||
/// </summary>
|
||||
public RoutingInputPort CurrentInputPort
|
||||
{
|
||||
get
|
||||
{
|
||||
return _currentInputPort;
|
||||
}
|
||||
|
||||
_currentInputPort = value;
|
||||
protected set
|
||||
{
|
||||
if (_currentInputPort == value) return;
|
||||
|
||||
InputChanged?.Invoke(this, _currentInputPort);
|
||||
}
|
||||
}
|
||||
_currentInputPort = value;
|
||||
|
||||
public event InputChangedEventHandler InputChanged;
|
||||
InputChanged?.Invoke(this, _currentInputPort);
|
||||
}
|
||||
}
|
||||
|
||||
public event SourceInfoChangeHandler CurrentSourceChange;
|
||||
/// <summary>
|
||||
/// Event that is raised when the input changes on the display.
|
||||
/// </summary>
|
||||
public event InputChangedEventHandler InputChanged;
|
||||
|
||||
public string CurrentSourceInfoKey { get; set; }
|
||||
public SourceListItem CurrentSourceInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CurrentSourceInfo;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == _CurrentSourceInfo) return;
|
||||
/// <summary>
|
||||
/// Event that is raised when the current source information changes.
|
||||
/// </summary>
|
||||
public event SourceInfoChangeHandler CurrentSourceChange;
|
||||
|
||||
var handler = CurrentSourceChange;
|
||||
/// <summary>
|
||||
/// Gets or sets the key of the current source information.
|
||||
/// </summary>
|
||||
public string CurrentSourceInfoKey { get; set; }
|
||||
|
||||
if (handler != null)
|
||||
handler(_CurrentSourceInfo, ChangeType.WillChange);
|
||||
/// <summary>
|
||||
/// Gets or sets the current source information for the display.
|
||||
/// </summary>
|
||||
public SourceListItem CurrentSourceInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
return _CurrentSourceInfo;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == _CurrentSourceInfo) return;
|
||||
|
||||
_CurrentSourceInfo = value;
|
||||
var handler = CurrentSourceChange;
|
||||
|
||||
if (handler != null)
|
||||
handler(_CurrentSourceInfo, ChangeType.DidChange);
|
||||
}
|
||||
}
|
||||
SourceListItem _CurrentSourceInfo;
|
||||
if (handler != null)
|
||||
handler(_CurrentSourceInfo, ChangeType.WillChange);
|
||||
|
||||
_CurrentSourceInfo = value;
|
||||
|
||||
if (handler != null)
|
||||
handler(_CurrentSourceInfo, ChangeType.DidChange);
|
||||
}
|
||||
}
|
||||
SourceListItem _CurrentSourceInfo;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<eRoutingSignalType, SourceListItem> CurrentSources { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback indicating whether the display is currently cooling down after being powered off.
|
||||
/// </summary>
|
||||
public BoolFeedback IsCoolingDownFeedback { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback indicating whether the display is currently warming up after being powered on.
|
||||
/// </summary>
|
||||
public BoolFeedback IsWarmingUpFeedback { get; private set; }
|
||||
|
||||
public UsageTracking UsageTracker { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the usage tracking instance for monitoring display usage statistics.
|
||||
/// </summary>
|
||||
public UsageTracking UsageTracker { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the warmup time in milliseconds for the display to become ready after power on.
|
||||
/// </summary>
|
||||
public uint WarmupTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cooldown time in milliseconds for the display to fully power down.
|
||||
/// </summary>
|
||||
public uint CooldownTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bool Func that will provide a value for the PowerIsOn Output. Must be implemented
|
||||
/// by concrete sub-classes
|
||||
/// Abstract function that must be implemented by derived classes to provide the cooling down feedback value.
|
||||
/// Must be implemented by concrete sub-classes.
|
||||
/// </summary>
|
||||
abstract protected Func<bool> IsCoolingDownFeedbackFunc { get; }
|
||||
abstract protected Func<bool> IsWarmingUpFeedbackFunc { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Abstract function that must be implemented by derived classes to provide the warming up feedback value.
|
||||
/// Must be implemented by concrete sub-classes.
|
||||
/// </summary>
|
||||
abstract protected Func<bool> IsWarmingUpFeedbackFunc { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Timer used for managing display warmup timing.
|
||||
/// </summary>
|
||||
protected CTimer WarmupTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Timer used for managing display cooldown timing.
|
||||
/// </summary>
|
||||
protected CTimer CooldownTimer;
|
||||
|
||||
#region IRoutingInputs Members
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of input ports available on this display device.
|
||||
/// </summary>
|
||||
public RoutingPortCollection<RoutingInputPort> InputPorts { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
protected DisplayBase(string key, string name)
|
||||
: base(key, name)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the DisplayBase class.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key identifier for this display device.</param>
|
||||
/// <param name="name">The friendly name for this display device.</param>
|
||||
protected DisplayBase(string key, string name)
|
||||
: base(key, name)
|
||||
{
|
||||
IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc);
|
||||
IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc);
|
||||
|
||||
InputPorts = new RoutingPortCollection<RoutingInputPort>();
|
||||
|
||||
CurrentSources = new Dictionary<eRoutingSignalType, SourceListItem>
|
||||
{
|
||||
{ eRoutingSignalType.Audio, null },
|
||||
{ eRoutingSignalType.Video, null },
|
||||
};
|
||||
|
||||
CurrentSourceKeys = new Dictionary<eRoutingSignalType, string>
|
||||
{
|
||||
{ eRoutingSignalType.Audio, string.Empty },
|
||||
{ eRoutingSignalType.Video, string.Empty },
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Powers on the display device. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
public abstract void PowerOn();
|
||||
|
||||
/// <summary>
|
||||
/// Powers off the display device. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
public abstract void PowerOff();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the power state of the display device. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
public abstract void PowerToggle();
|
||||
|
||||
public virtual FeedbackCollection<Feedback> Feedbacks
|
||||
/// <summary>
|
||||
/// Gets the collection of feedback objects for this display device.
|
||||
/// </summary>
|
||||
public virtual FeedbackCollection<Feedback> Feedbacks
|
||||
{
|
||||
get
|
||||
{
|
||||
return new FeedbackCollection<Feedback>
|
||||
return new FeedbackCollection<Feedback>
|
||||
{
|
||||
IsCoolingDownFeedback,
|
||||
IsWarmingUpFeedback
|
||||
@@ -112,30 +201,50 @@ namespace PepperDash.Essentials.Devices.Common.Displays
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void ExecuteSwitch(object selector);
|
||||
/// <summary>
|
||||
/// Executes a switch to the specified input on the display device. Must be implemented by derived classes.
|
||||
/// </summary>
|
||||
/// <param name="selector">The selector object that identifies which input to switch to.</param>
|
||||
public abstract void ExecuteSwitch(object selector);
|
||||
|
||||
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey,
|
||||
EiscApiAdvanced bridge)
|
||||
{
|
||||
var joinMap = new DisplayControllerJoinMap(joinStart);
|
||||
/// <summary>
|
||||
/// Links the display device to an API using a trilist, join start, join map key, and bridge.
|
||||
/// This overload uses serialized join map configuration.
|
||||
/// </summary>
|
||||
/// <param name="displayDevice">The display device to link.</param>
|
||||
/// <param name="trilist">The BasicTriList for communication.</param>
|
||||
/// <param name="joinStart">The starting join number for the device.</param>
|
||||
/// <param name="joinMapKey">The key for the join map configuration.</param>
|
||||
/// <param name="bridge">The EISC API bridge instance.</param>
|
||||
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey,
|
||||
EiscApiAdvanced bridge)
|
||||
{
|
||||
var joinMap = new DisplayControllerJoinMap(joinStart);
|
||||
|
||||
var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
|
||||
var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
|
||||
|
||||
if (!string.IsNullOrEmpty(joinMapSerialized))
|
||||
joinMap = JsonConvert.DeserializeObject<DisplayControllerJoinMap>(joinMapSerialized);
|
||||
if (!string.IsNullOrEmpty(joinMapSerialized))
|
||||
joinMap = JsonConvert.DeserializeObject<DisplayControllerJoinMap>(joinMapSerialized);
|
||||
|
||||
if (bridge != null)
|
||||
{
|
||||
bridge.AddJoinMap(Key, joinMap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information,this,"Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
|
||||
}
|
||||
if (bridge != null)
|
||||
{
|
||||
bridge.AddJoinMap(Key, joinMap);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
|
||||
}
|
||||
|
||||
LinkDisplayToApi(displayDevice, trilist, joinMap);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Links the display device to an API using a trilist and join map.
|
||||
/// This overload uses a pre-configured join map instance.
|
||||
/// </summary>
|
||||
/// <param name="displayDevice">The display device to link.</param>
|
||||
/// <param name="trilist">The BasicTriList for communication.</param>
|
||||
/// <param name="joinMap">The join map configuration for the device.</param>
|
||||
protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, DisplayControllerJoinMap joinMap)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X"));
|
||||
@@ -268,68 +377,96 @@ namespace PepperDash.Essentials.Devices.Common.Displays
|
||||
volumeDisplayWithFeedback.MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback
|
||||
/// <summary>
|
||||
/// Abstract base class for two-way display devices that provide feedback capabilities.
|
||||
/// Extends DisplayBase with routing feedback and power control feedback functionality.
|
||||
/// </summary>
|
||||
public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback
|
||||
{
|
||||
public StringFeedback CurrentInputFeedback { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets feedback for the current input selection on the display.
|
||||
/// </summary>
|
||||
public StringFeedback CurrentInputFeedback { get; private set; }
|
||||
|
||||
abstract protected Func<string> CurrentInputFeedbackFunc { get; }
|
||||
/// <summary>
|
||||
/// Abstract function that must be implemented by derived classes to provide the current input feedback value.
|
||||
/// Must be implemented by concrete sub-classes.
|
||||
/// </summary>
|
||||
abstract protected Func<string> CurrentInputFeedbackFunc { get; }
|
||||
|
||||
public BoolFeedback PowerIsOnFeedback { get; protected set; }
|
||||
/// <summary>
|
||||
/// Gets feedback indicating whether the display is currently powered on.
|
||||
/// </summary>
|
||||
public BoolFeedback PowerIsOnFeedback { get; protected set; }
|
||||
|
||||
abstract protected Func<bool> PowerIsOnFeedbackFunc { get; }
|
||||
/// <summary>
|
||||
/// Abstract function that must be implemented by derived classes to provide the power state feedback value.
|
||||
/// Must be implemented by concrete sub-classes.
|
||||
/// </summary>
|
||||
abstract protected Func<bool> PowerIsOnFeedbackFunc { get; }
|
||||
|
||||
|
||||
public static MockDisplay DefaultDisplay
|
||||
{
|
||||
get
|
||||
/// <summary>
|
||||
/// Gets the default mock display instance for testing and development purposes.
|
||||
/// </summary>
|
||||
public static MockDisplay DefaultDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_DefaultDisplay == null)
|
||||
_DefaultDisplay = new MockDisplay("default", "Default Display");
|
||||
return _DefaultDisplay;
|
||||
}
|
||||
}
|
||||
}
|
||||
static MockDisplay _DefaultDisplay;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the TwoWayDisplayBase class.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key identifier for this display device.</param>
|
||||
/// <param name="name">The friendly name for this display device.</param>
|
||||
public TwoWayDisplayBase(string key, string name)
|
||||
: base(key, name)
|
||||
{
|
||||
CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc);
|
||||
CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc);
|
||||
|
||||
WarmupTime = 7000;
|
||||
CooldownTime = 15000;
|
||||
|
||||
PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc);
|
||||
PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc);
|
||||
|
||||
Feedbacks.Add(CurrentInputFeedback);
|
||||
Feedbacks.Add(PowerIsOnFeedback);
|
||||
Feedbacks.Add(CurrentInputFeedback);
|
||||
Feedbacks.Add(PowerIsOnFeedback);
|
||||
|
||||
PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange;
|
||||
PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange;
|
||||
|
||||
}
|
||||
|
||||
void PowerIsOnFeedback_OutputChange(object sender, EventArgs e)
|
||||
{
|
||||
if (UsageTracker != null)
|
||||
{
|
||||
if (PowerIsOnFeedback.BoolValue)
|
||||
UsageTracker.StartDeviceUsage();
|
||||
else
|
||||
UsageTracker.EndDeviceUsage();
|
||||
}
|
||||
}
|
||||
void PowerIsOnFeedback_OutputChange(object sender, EventArgs e)
|
||||
{
|
||||
if (UsageTracker != null)
|
||||
{
|
||||
if (PowerIsOnFeedback.BoolValue)
|
||||
UsageTracker.StartDeviceUsage();
|
||||
else
|
||||
UsageTracker.EndDeviceUsage();
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<RoutingNumericEventArgs> NumericSwitchChange;
|
||||
/// <summary>
|
||||
/// Event that is raised when a numeric switch change occurs on the display.
|
||||
/// </summary>
|
||||
public event EventHandler<RoutingNumericEventArgs> NumericSwitchChange;
|
||||
|
||||
/// <summary>
|
||||
/// Raise an event when the status of a switch object changes.
|
||||
/// </summary>
|
||||
/// <param name="e">Arguments defined as IKeyName sender, output, input, and eRoutingSignalType</param>
|
||||
protected void OnSwitchChange(RoutingNumericEventArgs e)
|
||||
{
|
||||
var newEvent = NumericSwitchChange;
|
||||
if (newEvent != null) newEvent(this, e);
|
||||
}
|
||||
/// <summary>
|
||||
/// Raise an event when the status of a switch object changes.
|
||||
/// </summary>
|
||||
/// <param name="e">Arguments defined as IKeyName sender, output, input, and eRoutingSignalType</param>
|
||||
protected void OnSwitchChange(RoutingNumericEventArgs e)
|
||||
{
|
||||
var newEvent = NumericSwitchChange;
|
||||
if (newEvent != null) newEvent(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,14 @@ using System.Collections.Generic;
|
||||
|
||||
namespace PepperDash.Essentials.AppServer.Messengers
|
||||
{
|
||||
/// <summary>
|
||||
/// Messenger for a CameraBase device
|
||||
/// </summary>
|
||||
public class CameraBaseMessenger : MessengerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Device being bridged
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
public CameraBase Camera { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@@ -45,6 +48,9 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the actions for this messenger. This is called by the base class
|
||||
/// </summary>
|
||||
protected override void RegisterActions()
|
||||
{
|
||||
base.RegisterActions();
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Essentials.Devices.Common.Cameras;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Essentials.AppServer.Messengers
|
||||
{
|
||||
/// <summary>
|
||||
/// Messenger for devices that implement the IHasCameras interface.
|
||||
/// </summary>
|
||||
public class IHasCamerasMessenger : MessengerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Device being bridged that implements IHasCameras interface.
|
||||
/// </summary>
|
||||
public IHasCameras CameraController { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Messenger for devices that implement IHasCameras interface.
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="cameraController"></param>
|
||||
/// <param name="messagePath"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public IHasCamerasMessenger(string key, string messagePath , IHasCameras cameraController)
|
||||
: base(key, messagePath, cameraController)
|
||||
{
|
||||
CameraController = cameraController ?? throw new ArgumentNullException("cameraController");
|
||||
CameraController.CameraSelected += CameraController_CameraSelected;
|
||||
}
|
||||
|
||||
private void CameraController_CameraSelected(object sender, CameraSelectedEventArgs e)
|
||||
{
|
||||
PostStatusMessage(new IHasCamerasStateMessage
|
||||
{
|
||||
SelectedCamera = e.SelectedCamera
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the actions for this messenger.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
protected override void RegisterActions()
|
||||
{
|
||||
base.RegisterActions();
|
||||
|
||||
AddAction("/fullStatus", (id, context) =>
|
||||
{
|
||||
SendFullStatus(id);
|
||||
});
|
||||
|
||||
AddAction("/selectCamera", (id, content) =>
|
||||
{
|
||||
var cameraKey = content?.ToObject<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(cameraKey))
|
||||
{
|
||||
CameraController.SelectCamera(cameraKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Content must be a string representing the camera key");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void SendFullStatus(string clientId)
|
||||
{
|
||||
var state = new IHasCamerasStateMessage
|
||||
{
|
||||
CameraList = CameraController.Cameras,
|
||||
SelectedCamera = CameraController.SelectedCamera
|
||||
};
|
||||
|
||||
PostStatusMessage(state, clientId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State message for devices that implement the IHasCameras interface.
|
||||
/// </summary>
|
||||
public class IHasCamerasStateMessage : DeviceStateMessageBase
|
||||
{
|
||||
/// <summary>
|
||||
/// List of cameras available in the device.
|
||||
/// </summary>
|
||||
[JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<CameraBase> CameraList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently selected camera on the device.
|
||||
/// </summary>
|
||||
[JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public CameraBase SelectedCamera { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -907,6 +907,19 @@ namespace PepperDash.Essentials
|
||||
messengerAdded = true;
|
||||
}
|
||||
|
||||
if (device is IHasCameras cameras)
|
||||
{
|
||||
this.LogVerbose("Adding IHasCamerasMessenger for {deviceKey}", device.Key
|
||||
);
|
||||
var messenger = new IHasCamerasMessenger(
|
||||
$"{device.Key}-cameras-{Key}",
|
||||
$"/device/{device.Key}",
|
||||
cameras
|
||||
);
|
||||
AddDefaultDeviceMessenger(messenger);
|
||||
messengerAdded = true;
|
||||
}
|
||||
|
||||
this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key);
|
||||
|
||||
if (device is EssentialsDevice)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using Crestron.SimplSharpPro;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharpPro;
|
||||
using Crestron.SimplSharpPro.DeviceSupport;
|
||||
using Crestron.SimplSharpPro.UI;
|
||||
using Newtonsoft.Json;
|
||||
@@ -10,64 +14,105 @@ using PepperDash.Essentials.Core.Config;
|
||||
using PepperDash.Essentials.Core.DeviceInfo;
|
||||
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
|
||||
using PepperDash.Essentials.Core.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Feedback = PepperDash.Essentials.Core.Feedback;
|
||||
|
||||
namespace PepperDash.Essentials.Touchpanel
|
||||
{
|
||||
//public interface IMobileControlTouchpanelController
|
||||
//{
|
||||
// StringFeedback AppUrlFeedback { get; }
|
||||
// string DefaultRoomKey { get; }
|
||||
// string DeviceKey { get; }
|
||||
//}
|
||||
|
||||
|
||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlTouchpanelController, ITheme
|
||||
/// <summary>
|
||||
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
||||
/// and mobile control functionality for Crestron touchpanels.
|
||||
/// </summary>
|
||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
|
||||
{
|
||||
private readonly MobileControlTouchpanelProperties localConfig;
|
||||
private IMobileControlRoomMessenger _bridge;
|
||||
|
||||
private string _appUrl;
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback for the current application URL.
|
||||
/// </summary>
|
||||
public StringFeedback AppUrlFeedback { get; private set; }
|
||||
|
||||
private readonly StringFeedback QrCodeUrlFeedback;
|
||||
private readonly StringFeedback McServerUrlFeedback;
|
||||
private readonly StringFeedback UserCodeFeedback;
|
||||
|
||||
private readonly BoolFeedback _appOpenFeedback;
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback indicating whether an application is currently open on the touchpanel.
|
||||
/// </summary>
|
||||
public BoolFeedback AppOpenFeedback => _appOpenFeedback;
|
||||
|
||||
private readonly BoolFeedback _zoomIncomingCallFeedback;
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback indicating whether there is an incoming Zoom call.
|
||||
/// </summary>
|
||||
public BoolFeedback ZoomIncomingCallFeedback => _zoomIncomingCallFeedback;
|
||||
|
||||
private readonly BoolFeedback _zoomInCallFeedback;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is raised when device information changes.
|
||||
/// </summary>
|
||||
public event DeviceInfoChangeHandler DeviceInfoChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback indicating whether a Zoom call is currently active.
|
||||
/// </summary>
|
||||
public BoolFeedback ZoomInCallFeedback => _zoomInCallFeedback;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of feedback objects for this touchpanel controller.
|
||||
/// </summary>
|
||||
public FeedbackCollection<Feedback> Feedbacks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of Zoom-related feedback objects.
|
||||
/// </summary>
|
||||
public FeedbackCollection<Feedback> ZoomFeedbacks { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default room key for this touchpanel controller.
|
||||
/// </summary>
|
||||
public string DefaultRoomKey => _config.DefaultRoomKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use direct server communication.
|
||||
/// </summary>
|
||||
public bool UseDirectServer => localConfig.UseDirectServer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this touchpanel acts as a Zoom Room controller.
|
||||
/// </summary>
|
||||
public bool ZoomRoomController => localConfig.ZoomRoomController;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current theme for the touchpanel interface.
|
||||
/// </summary>
|
||||
public string Theme => localConfig.Theme;
|
||||
|
||||
/// <summary>
|
||||
/// Gets feedback for the current theme setting.
|
||||
/// </summary>
|
||||
public StringFeedback ThemeFeedback { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets device information including MAC address and IP address.
|
||||
/// </summary>
|
||||
public DeviceInfo DeviceInfo => new DeviceInfo();
|
||||
|
||||
public ReadOnlyCollection<ConnectedIpInformation> ConnectedIps => Panel.ConnectedIpList;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MobileControlTouchpanelController class.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key identifier for this touchpanel controller.</param>
|
||||
/// <param name="name">The friendly name for this touchpanel controller.</param>
|
||||
/// <param name="panel">The touchpanel hardware device.</param>
|
||||
/// <param name="config">The configuration properties for this controller.</param>
|
||||
public MobileControlTouchpanelController(string key, string name, BasicTriListWithSmartObject panel, MobileControlTouchpanelProperties config) : base(key, name, panel, config)
|
||||
{
|
||||
localConfig = config;
|
||||
@@ -139,6 +184,10 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
RegisterForExtenders();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the theme setting for this touchpanel controller and persists the change to configuration.
|
||||
/// </summary>
|
||||
/// <param name="theme">The new theme identifier to apply.</param>
|
||||
public void UpdateTheme(string theme)
|
||||
{
|
||||
localConfig.Theme = theme;
|
||||
@@ -271,6 +320,11 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs custom activation setup for the touchpanel controller, including
|
||||
/// registering messengers and linking to mobile control.
|
||||
/// </summary>
|
||||
/// <returns>True if activation was successful; otherwise, false.</returns>
|
||||
public override bool CustomActivate()
|
||||
{
|
||||
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
||||
@@ -300,12 +354,20 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
return base.CustomActivate();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Handles device extender signal changes for system reserved signals.
|
||||
/// </summary>
|
||||
/// <param name="currentDeviceExtender">The device extender that generated the signal change.</param>
|
||||
/// <param name="args">The signal event arguments containing the changed signal information.</param>
|
||||
protected override void ExtenderSystemReservedSigs_DeviceExtenderSigChange(DeviceExtender currentDeviceExtender, SigEventArgs args)
|
||||
{
|
||||
Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"System Device Extender args: ${args.Event}:${args.Sig}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the panel drivers and signal mappings for the specified room.
|
||||
/// </summary>
|
||||
/// <param name="roomKey">The room key to configure the panel drivers for.</param>
|
||||
protected override void SetupPanelDrivers(string roomKey)
|
||||
{
|
||||
AppUrlFeedback.LinkInputSig(Panel.StringInput[1]);
|
||||
@@ -366,6 +428,10 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
SetAppUrl(_bridge.AppUrl);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the application URL and updates the corresponding feedback.
|
||||
/// </summary>
|
||||
/// <param name="url">The new application URL to set.</param>
|
||||
public void SetAppUrl(string url)
|
||||
{
|
||||
_appUrl = url;
|
||||
@@ -391,6 +457,9 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the currently open application on the touchpanel.
|
||||
/// </summary>
|
||||
public void HideOpenApp()
|
||||
{
|
||||
if (Panel is TswX70Base x70Panel)
|
||||
@@ -406,6 +475,9 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens an application on the touchpanel. Note: X60 panels do not support Zoom app opening.
|
||||
/// </summary>
|
||||
public void OpenApp()
|
||||
{
|
||||
if (Panel is TswX70Base x70Panel)
|
||||
@@ -421,6 +493,9 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the currently open application on the touchpanel.
|
||||
/// </summary>
|
||||
public void CloseOpenApp()
|
||||
{
|
||||
if (Panel is TswX70Base x70Panel)
|
||||
@@ -436,6 +511,9 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends the current Zoom call on the touchpanel.
|
||||
/// </summary>
|
||||
public void EndZoomCall()
|
||||
{
|
||||
if (Panel is TswX70Base x70Panel)
|
||||
@@ -451,6 +529,10 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the device information (MAC address and IP address) from the touchpanel
|
||||
/// and raises the DeviceInfoChanged event.
|
||||
/// </summary>
|
||||
public void UpdateDeviceInfo()
|
||||
{
|
||||
if (Panel is TswXX70Base x70Panel)
|
||||
@@ -487,14 +569,27 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory class for creating MobileControlTouchpanelController instances from device configuration.
|
||||
/// Supports various Crestron touchpanel models including TSW, TS, CrestronApp, XPanel, and DGE series.
|
||||
/// </summary>
|
||||
public class MobileControlTouchpanelControllerFactory : EssentialsPluginDeviceFactory<MobileControlTouchpanelController>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MobileControlTouchpanelControllerFactory class.
|
||||
/// Sets up supported device type names and minimum framework version requirements.
|
||||
/// </summary>
|
||||
public MobileControlTouchpanelControllerFactory()
|
||||
{
|
||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
|
||||
MinimumEssentialsFrameworkVersion = "2.0.0";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a MobileControlTouchpanelController device from the provided device configuration.
|
||||
/// </summary>
|
||||
/// <param name="dc">The device configuration containing the device properties and settings.</param>
|
||||
/// <returns>A configured MobileControlTouchpanelController instance.</returns>
|
||||
public override EssentialsDevice BuildDevice(DeviceConfig dc)
|
||||
{
|
||||
var comm = CommFactory.GetControlPropertiesConfig(dc);
|
||||
@@ -557,7 +652,7 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
return new Ts1070(id, Global.ControlSystem);
|
||||
else if (type == "dge1000")
|
||||
return new Dge1000(id, Global.ControlSystem);
|
||||
else
|
||||
else
|
||||
|
||||
{
|
||||
Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "WARNING: Cannot create TSW controller with type '{0}'", type);
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
using Crestron.SimplSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core;
|
||||
@@ -9,12 +15,6 @@ using PepperDash.Essentials.Core.Web;
|
||||
using PepperDash.Essentials.RoomBridges;
|
||||
using PepperDash.Essentials.WebApiHandlers;
|
||||
using Serilog.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using WebSocketSharp;
|
||||
using WebSocketSharp.Net;
|
||||
using WebSocketSharp.Server;
|
||||
@@ -60,7 +60,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
|
||||
private string lanIpAddress => CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter));
|
||||
|
||||
private System.Net.IPAddress csIpAddress;
|
||||
private System.Net.IPAddress csIpAddress;
|
||||
|
||||
private System.Net.IPAddress csSubnetMask;
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
_parent = parent;
|
||||
|
||||
// Set the default port to be 50000 plus the slot number of the program
|
||||
Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
|
||||
Port = 50000 + (int)Global.ControlSystem.ProgramNumber;
|
||||
|
||||
if (customPort != 0)
|
||||
{
|
||||
@@ -156,9 +156,9 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
|
||||
var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
|
||||
var csSubnetMask = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, csAdapterId);
|
||||
var csIpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
|
||||
|
||||
this.csSubnetMask = System.Net.IPAddress.Parse(csSubnetMask);
|
||||
@@ -298,8 +298,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
|
||||
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
|
||||
|
||||
this.LogVerbose("Processor IP: {processorIp}", processorIp);
|
||||
|
||||
foreach (var touchpanel in touchpanels.Select(tp =>
|
||||
{
|
||||
var token = _secret.Tokens.FirstOrDefault((t) => t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase));
|
||||
@@ -321,11 +319,25 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
continue;
|
||||
}
|
||||
|
||||
var appUrl = $"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
|
||||
string ip = processorIp;
|
||||
if (touchpanel.Touchpanel is IMobileControlCrestronTouchpanelController crestronTouchpanel)
|
||||
{
|
||||
ip = crestronTouchpanel.ConnectedIps.Any(ipInfo =>
|
||||
{
|
||||
if (System.Net.IPAddress.TryParse(ipInfo.DeviceIpAddress, out var parsedIp))
|
||||
{
|
||||
return csIpAddress.IsInSameSubnet(parsedIp, csSubnetMask);
|
||||
}
|
||||
this.LogWarning("Invalid IP address: {deviceIpAddress}", ipInfo.DeviceIpAddress);
|
||||
return false;
|
||||
}) ? csIpAddress.ToString() : processorIp;
|
||||
}
|
||||
|
||||
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
|
||||
|
||||
this.LogVerbose("Sending URL {appUrl}", appUrl);
|
||||
|
||||
touchpanel.Messenger.UpdateAppUrl($"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
|
||||
touchpanel.Messenger.UpdateAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +361,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
if (!Directory.Exists($"{userAppPath}{localConfigFolderName}"))
|
||||
{
|
||||
Directory.CreateDirectory($"{userAppPath}{localConfigFolderName}");
|
||||
}
|
||||
}
|
||||
|
||||
using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigFileName}", FileMode.Create, FileAccess.ReadWrite)))
|
||||
{
|
||||
@@ -358,7 +370,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
|
||||
this.LogDebug("LAN Adapter ID: {lanAdapterId}", lanAdapterId);
|
||||
|
||||
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
|
||||
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId);
|
||||
|
||||
var config = GetApplicationConfig(processorIp);
|
||||
|
||||
@@ -378,7 +390,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
return;
|
||||
}
|
||||
|
||||
if(csAdapterId == -1)
|
||||
if (csAdapterId == -1)
|
||||
{
|
||||
this.LogDebug("CS LAN Adapter not found");
|
||||
return;
|
||||
@@ -389,8 +401,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigCsFileName}", FileMode.Create, FileAccess.ReadWrite)))
|
||||
{
|
||||
// Write the CS application configuration file. Used when a request comes in for the application config from the CS
|
||||
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
|
||||
|
||||
var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
|
||||
|
||||
var config = GetApplicationConfig(processorIp);
|
||||
|
||||
var contents = JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
@@ -400,7 +412,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
}
|
||||
|
||||
private MobileControlApplicationConfig GetApplicationConfig(string processorIp)
|
||||
{
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = new MobileControlApplicationConfig
|
||||
@@ -430,10 +442,10 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error getting application configuration");
|
||||
this.LogError(ex, "Error getting application configuration");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -572,7 +584,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
|
||||
var values = s.Split(' ');
|
||||
|
||||
if(values.Length < 2)
|
||||
if (values.Length < 2)
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("Invalid number of arguments. Please provide a room key and a grant code");
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for logging operations to enable testing
|
||||
/// </summary>
|
||||
public interface ILogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Logs a debug message
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the log message</param>
|
||||
/// <param name="message">Message to log</param>
|
||||
/// <param name="args">Format arguments</param>
|
||||
void LogDebug(object source, string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Logs a verbose message
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the log message</param>
|
||||
/// <param name="message">Message to log</param>
|
||||
/// <param name="args">Format arguments</param>
|
||||
void LogVerbose(object source, string message, params object[] args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for queue operations to enable testing
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of items in the queue</typeparam>
|
||||
public interface IQueue<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of items in the queue
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item to the queue
|
||||
/// </summary>
|
||||
/// <param name="item">Item to add</param>
|
||||
void Enqueue(T item);
|
||||
|
||||
/// <summary>
|
||||
/// Removes and returns the next item from the queue
|
||||
/// </summary>
|
||||
/// <returns>The next item, or default(T) if queue is empty</returns>
|
||||
T Dequeue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction for thread operations to enable testing
|
||||
/// </summary>
|
||||
public interface IThreadService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sleeps the current thread for the specified milliseconds
|
||||
/// </summary>
|
||||
/// <param name="milliseconds">Time to sleep in milliseconds</param>
|
||||
void Sleep(int milliseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Creates and starts a new thread
|
||||
/// </summary>
|
||||
/// <param name="threadFunction">The function to execute in the thread</param>
|
||||
/// <param name="parameter">Parameter to pass to the thread function</param>
|
||||
/// <returns>Thread identifier or handle</returns>
|
||||
object CreateAndStartThread(Func<object, object> threadFunction, object parameter);
|
||||
|
||||
/// <summary>
|
||||
/// Aborts the specified thread
|
||||
/// </summary>
|
||||
/// <param name="thread">The thread to abort</param>
|
||||
void AbortThread(object thread);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the thread is currently running
|
||||
/// </summary>
|
||||
/// <param name="thread">The thread to check</param>
|
||||
/// <returns>True if the thread is running, false otherwise</returns>
|
||||
bool IsThreadRunning(object thread);
|
||||
}
|
||||
}
|
||||
5
tests/PepperDash.Essentials.Core.Tests/GlobalUsings.cs
Normal file
5
tests/PepperDash.Essentials.Core.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
global using Xunit;
|
||||
global using Moq;
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.69" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PepperDash.Essentials.Core.Tests.Abstractions;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.TestableComponents
|
||||
{
|
||||
/// <summary>
|
||||
/// A simplified, testable action sequence that demonstrates abstraction patterns
|
||||
/// This shows how we can separate business logic from SDK dependencies
|
||||
/// </summary>
|
||||
public class TestableActionSequence
|
||||
{
|
||||
private readonly IQueue<TestableSequencedAction> _actionQueue;
|
||||
private readonly IThreadService _threadService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly List<TestableSequencedAction> _configuredActions;
|
||||
|
||||
private object _workerThread;
|
||||
private bool _allowActionsToExecute;
|
||||
|
||||
public TestableActionSequence(
|
||||
IQueue<TestableSequencedAction> actionQueue,
|
||||
IThreadService threadService,
|
||||
ILogger logger,
|
||||
List<TestableSequencedAction> actions)
|
||||
{
|
||||
_actionQueue = actionQueue ?? throw new ArgumentNullException(nameof(actionQueue));
|
||||
_threadService = threadService ?? throw new ArgumentNullException(nameof(threadService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_configuredActions = actions ?? new List<TestableSequencedAction>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts executing the sequenced actions
|
||||
/// </summary>
|
||||
public void StartSequence()
|
||||
{
|
||||
if (_workerThread != null && _threadService.IsThreadRunning(_workerThread))
|
||||
{
|
||||
_logger.LogDebug(this, "Thread already running. Cannot Start Sequence");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug(this, "Starting Action Sequence");
|
||||
_allowActionsToExecute = true;
|
||||
AddActionsToQueue();
|
||||
_workerThread = _threadService.CreateAndStartThread(ProcessActions, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops executing the sequenced actions
|
||||
/// </summary>
|
||||
public void StopSequence()
|
||||
{
|
||||
_logger.LogDebug(this, "Stopping Action Sequence");
|
||||
_allowActionsToExecute = false;
|
||||
if (_workerThread != null)
|
||||
{
|
||||
_threadService.AbortThread(_workerThread);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current status of the sequence
|
||||
/// </summary>
|
||||
public bool IsRunning => _allowActionsToExecute && _threadService.IsThreadRunning(_workerThread);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of pending actions
|
||||
/// </summary>
|
||||
public int PendingActionsCount => _actionQueue.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Populates the queue from the configuration information
|
||||
/// </summary>
|
||||
private void AddActionsToQueue()
|
||||
{
|
||||
_logger.LogDebug(this, "Adding {0} actions to queue", _configuredActions.Count);
|
||||
|
||||
foreach (var action in _configuredActions)
|
||||
{
|
||||
_actionQueue.Enqueue(action);
|
||||
}
|
||||
}
|
||||
|
||||
private object ProcessActions(object obj)
|
||||
{
|
||||
while (_allowActionsToExecute && _actionQueue.Count > 0)
|
||||
{
|
||||
var action = _actionQueue.Dequeue();
|
||||
if (action == null)
|
||||
break;
|
||||
|
||||
// Delay before executing
|
||||
if (action.DelayMs > 0)
|
||||
_threadService.Sleep(action.DelayMs);
|
||||
|
||||
ExecuteAction(action);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExecuteAction(TestableSequencedAction action)
|
||||
{
|
||||
if (action == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug(this, "Executing action: {0} with delay: {1}ms", action.Name, action.DelayMs);
|
||||
action.Execute();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogVerbose(this, "Error Executing Action: {0}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A testable action that can be sequenced
|
||||
/// </summary>
|
||||
public class TestableSequencedAction
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int DelayMs { get; set; }
|
||||
public Action ActionToExecute { get; set; }
|
||||
|
||||
public TestableSequencedAction(string name, int delayMs = 0, Action actionToExecute = null)
|
||||
{
|
||||
Name = name;
|
||||
DelayMs = delayMs;
|
||||
ActionToExecute = actionToExecute ?? (() => { });
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
ActionToExecute?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using PepperDash.Essentials.Core.Tests.Abstractions;
|
||||
using PepperDash.Essentials.Core.Tests.TestableComponents;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Tests.Unit
|
||||
{
|
||||
public class TestableActionSequenceTests
|
||||
{
|
||||
private readonly Mock<IQueue<TestableSequencedAction>> _mockQueue;
|
||||
private readonly Mock<IThreadService> _mockThreadService;
|
||||
private readonly Mock<ILogger> _mockLogger;
|
||||
private readonly TestableActionSequence _actionSequence;
|
||||
private readonly List<TestableSequencedAction> _testActions;
|
||||
|
||||
public TestableActionSequenceTests()
|
||||
{
|
||||
_mockQueue = new Mock<IQueue<TestableSequencedAction>>();
|
||||
_mockThreadService = new Mock<IThreadService>();
|
||||
_mockLogger = new Mock<ILogger>();
|
||||
|
||||
_testActions = new List<TestableSequencedAction>
|
||||
{
|
||||
new TestableSequencedAction("Action1", 100),
|
||||
new TestableSequencedAction("Action2", 200)
|
||||
};
|
||||
|
||||
_actionSequence = new TestableActionSequence(
|
||||
_mockQueue.Object,
|
||||
_mockThreadService.Object,
|
||||
_mockLogger.Object,
|
||||
_testActions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullQueue_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new TestableActionSequence(null, _mockThreadService.Object, _mockLogger.Object, _testActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullThreadService_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new TestableActionSequence(_mockQueue.Object, null, _mockLogger.Object, _testActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() =>
|
||||
new TestableActionSequence(_mockQueue.Object, _mockThreadService.Object, null, _testActions));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_WhenNoThreadRunning_StartsNewThread()
|
||||
{
|
||||
// Arrange
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(It.IsAny<object>())).Returns(false);
|
||||
|
||||
// Act
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Starting Action Sequence"), Times.Once);
|
||||
_mockThreadService.Verify(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_WhenThreadAlreadyRunning_DoesNotStartNewThread()
|
||||
{
|
||||
// Arrange
|
||||
var mockThread = new object();
|
||||
_mockThreadService.Setup(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null))
|
||||
.Returns(mockThread);
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(mockThread)).Returns(true);
|
||||
|
||||
// First call to set up the worker thread
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Reset invocations to verify only the second call behavior
|
||||
_mockLogger.Invocations.Clear();
|
||||
_mockThreadService.Invocations.Clear();
|
||||
|
||||
// Act - Second call should detect running thread
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Thread already running. Cannot Start Sequence"), Times.Once);
|
||||
_mockThreadService.Verify(x => x.CreateAndStartThread(It.IsAny<Func<object, object>>(), null), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartSequence_AddsConfiguredActionsToQueue()
|
||||
{
|
||||
// Arrange
|
||||
_mockThreadService.Setup(x => x.IsThreadRunning(It.IsAny<object>())).Returns(false);
|
||||
|
||||
// Act
|
||||
_actionSequence.StartSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Adding {0} actions to queue", 2), Times.Once);
|
||||
_mockQueue.Verify(x => x.Enqueue(It.IsAny<TestableSequencedAction>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StopSequence_SetsAllowActionsToFalseAndAbortsThread()
|
||||
{
|
||||
// Arrange
|
||||
var mockThread = new object();
|
||||
|
||||
// Act
|
||||
_actionSequence.StopSequence();
|
||||
|
||||
// Assert
|
||||
_mockLogger.Verify(x => x.LogDebug(_actionSequence, "Stopping Action Sequence"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PendingActionsCount_ReturnsQueueCount()
|
||||
{
|
||||
// Arrange
|
||||
_mockQueue.Setup(x => x.Count).Returns(5);
|
||||
|
||||
// Act
|
||||
var count = _actionSequence.PendingActionsCount;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(5, count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestableSequencedAction_ExecutesActionWhenCalled()
|
||||
{
|
||||
// Arrange
|
||||
bool actionExecuted = false;
|
||||
var action = new TestableSequencedAction("TestAction", 0, () => actionExecuted = true);
|
||||
|
||||
// Act
|
||||
action.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.True(actionExecuted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestableSequencedAction_WithNullAction_DoesNotThrow()
|
||||
{
|
||||
// Arrange
|
||||
var action = new TestableSequencedAction("TestAction", 0, null);
|
||||
|
||||
// Act & Assert
|
||||
var exception = Record.Exception(() => action.Execute());
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user