mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
Compare commits
142 commits
v3.0.0-fea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f05654638 | ||
|
|
4ca59e3186 | ||
|
|
a55e4811b3 | ||
|
|
5cac0bb6c8 | ||
|
|
44e5ff4c3e | ||
|
|
ec9753b19e | ||
|
|
12e5ff752d | ||
|
|
85a4b635f6 | ||
|
|
aff4c1e897 | ||
|
|
3afc73a446 | ||
|
|
3c9f5e95e0 | ||
|
|
f70c78bcc5 | ||
|
|
5b5619764f | ||
|
|
d21cef0e60 | ||
|
|
1719de195f | ||
|
|
6949253b1a | ||
|
|
e23c987516 | ||
|
|
14cd082076 | ||
|
|
36c15b8d95 | ||
|
|
fbf4c56403 | ||
|
|
f4cb17314d | ||
|
|
aa76551500 | ||
|
|
12828e8dd4 | ||
|
|
4dca16e9bd | ||
|
|
d4284bd59f | ||
|
|
a8ec831064 | ||
|
|
6ceddcf571 | ||
|
|
02f507ccb1 | ||
|
|
22ef9cf7c2 | ||
|
|
02216372bc | ||
|
|
c323c872fc | ||
|
|
1e63b10aa4 | ||
|
|
47e186d8f4 | ||
|
|
2f7789b374 | ||
|
|
588057f3ba | ||
|
|
64d60dacc2 | ||
|
|
f34d7223e0 | ||
|
|
68c44e46ae | ||
|
|
4f1eb979d3 | ||
|
|
8ac4eb7584 | ||
|
|
0240887d93 | ||
|
|
640bd7a8a7 | ||
|
|
2fac0ca926 | ||
|
|
af5611e403 | ||
|
|
3286d27898 | ||
|
|
8d3edde28c | ||
|
|
a220474101 | ||
|
|
b982219e2f | ||
|
|
e03e45ccf5 | ||
|
|
a7b839296e | ||
|
|
a63da82cc3 | ||
|
|
3974455337 | ||
|
|
782bb6c057 | ||
|
|
907eb2f397 | ||
|
|
72a4c63a06 | ||
|
|
5f26cb98fd | ||
|
|
6a4e3d5be2 | ||
|
|
d18fca8a98 | ||
|
|
162a06f9e9 | ||
|
|
75587c361f | ||
|
|
c3511cd1a6 | ||
|
|
b32bab0d33 | ||
|
|
1ce86dab29 | ||
|
|
77c700c565 | ||
|
|
aa050121ae | ||
|
|
abef1d095f | ||
|
|
74a12c9671 | ||
|
|
92d13f29a9 | ||
|
|
f658fdf363 | ||
|
|
6587444970 | ||
|
|
94c9e75d8f | ||
|
|
469999b6f7 | ||
|
|
08aba35334 | ||
|
|
68fe205504 | ||
|
|
21217341b5 | ||
|
|
e1a5c32c1f | ||
|
|
18f7000d76 | ||
|
|
d47cfd5e62 | ||
|
|
4f2d2ca746 | ||
|
|
e6583f7824 | ||
|
|
e57bc43a10 | ||
|
|
b83af26b77 | ||
|
|
c9f5af184b | ||
|
|
beb77ec468 | ||
|
|
b318e7f365 | ||
|
|
a9dd57fdaf | ||
|
|
d879430616 | ||
|
|
1a5840c29a | ||
|
|
6fbdaa9ca0 | ||
|
|
e5e1802da9 | ||
|
|
17adac639e | ||
|
|
2607180ab5 | ||
|
|
f1ef479301 | ||
|
|
8ba993ed66 | ||
|
|
9fc866741e | ||
|
|
f3ea9e1d5a | ||
|
|
404728c708 | ||
|
|
c050bb4eb3 | ||
|
|
2530003a58 | ||
|
|
c52d585a0c | ||
|
|
89c23f5432 | ||
|
|
841279eb0c | ||
|
|
1b32761e8e | ||
|
|
507130c1ae | ||
|
|
1ecfca7e5a | ||
|
|
dd32f44e3d | ||
|
|
ba48f23a71 | ||
|
|
447af3883c | ||
|
|
3b57860123 | ||
|
|
b1a575e4d2 | ||
|
|
2fdd73498a | ||
|
|
ee240ca378 | ||
|
|
f31f0611f1 | ||
|
|
0a991cffb0 | ||
|
|
18088d37a1 | ||
|
|
4f86009209 | ||
|
|
2197dc489d | ||
|
|
db14a614bc | ||
|
|
4a59cf9f81 | ||
|
|
9ea5ec5d1a | ||
|
|
6e9480f503 | ||
|
|
a610e127de | ||
|
|
c820052b41 | ||
|
|
bf517ad8b8 | ||
|
|
e55ac38719 | ||
|
|
e800e0912c | ||
|
|
d2684f364d | ||
|
|
29d5804cb0 | ||
|
|
c20d49f430 | ||
|
|
a82cf4f449 | ||
|
|
d0ab42ccb7 | ||
|
|
1b10910cc2 | ||
|
|
84c730b7a1 | ||
|
|
6db7581295 | ||
|
|
76311b83ff | ||
|
|
8817d70f07 | ||
|
|
305a466b1b | ||
|
|
5a07e837ee | ||
|
|
9c9a643b6a | ||
|
|
fb8216beed | ||
|
|
d05ebecd7d | ||
|
|
d0fe225bbc |
681 changed files with 57936 additions and 53692 deletions
|
|
@ -6,13 +6,9 @@ on:
|
|||
- '**'
|
||||
|
||||
jobs:
|
||||
runTests:
|
||||
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-run-tests.yml@main
|
||||
secrets: inherit
|
||||
getVersion:
|
||||
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main
|
||||
secrets: inherit
|
||||
needs: runTests
|
||||
build-4Series:
|
||||
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main
|
||||
secrets: inherit
|
||||
|
|
|
|||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
|
|
@ -26,9 +26,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
- name: Dotnet Setup
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.x
|
||||
|
||||
|
|
|
|||
|
|
@ -36,189 +36,49 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core.Tests", "src\PepperDash.Core.Tests\PepperDash.Core.Tests.csproj", "{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Core.Tests", "src\PepperDash.Essentials.Core.Tests\PepperDash.Essentials.Core.Tests.csproj", "{F508E0BA-E885-424F-9D4C-359CF0011DEF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.Tests", "src\PepperDash.Essentials.Tests\PepperDash.Essentials.Tests.csproj", "{841EE676-7784-4456-8F76-3697C6D432A6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
|
||||
Debug 4.7.2|x64 = Debug 4.7.2|x64
|
||||
Debug 4.7.2|x86 = Debug 4.7.2|x86
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x64.Build.0 = Release|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{53E204B7-97DD-441D-A96C-721DF014DF82}.Release|x86.Build.0 = Release|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x64.Build.0 = Release|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|Any CPU.Build.0 = Debug 4.7.2|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x64.Build.0 = Release|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x64.Build.0 = Debug 4.7.2|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.ActiveCfg = Debug 4.7.2|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|x86.Build.0 = Debug 4.7.2|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|x86.Build.0 = Release|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x64.Build.0 = Release|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF}.Release|x86.Build.0 = Release|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.ActiveCfg = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x64.Build.0 = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.ActiveCfg = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug 4.7.2|x86.Build.0 = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -230,9 +90,6 @@ Global
|
|||
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
||||
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
|
||||
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
||||
{680BA287-E61F-4B8D-BD7A-84C2504F5F9C} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{F508E0BA-E885-424F-9D4C-359CF0011DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{841EE676-7784-4456-8F76-3697C6D432A6} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ Utilization of Essentials Framework falls into the following categories:
|
|||
|
||||
## Documentation
|
||||
|
||||
For detailed documentation, see the [DocFx Site](https://pepperdash.github.io/Essentials/).
|
||||
For detailed documentation, see the [Wiki](https://github.com/PepperDash/EssentialsFramework/wiki).
|
||||
|
||||
## Support
|
||||
|
||||
|
|
|
|||
|
|
@ -19,13 +19,12 @@ Or use the links to the right to navigate our documentation.
|
|||
|
||||
## Benefits
|
||||
|
||||
- Runs on Crestron **4-Series** and VC-4 Control System platforms
|
||||
- Runs on Crestron 3-Series, **4-Series** and VC-4 Control System platforms
|
||||
- Reduced hardware overhead compared to S+ and Simpl solutions
|
||||
- Quick development cycle
|
||||
- Shared resources made easily available
|
||||
- More flexibility with less code
|
||||
- Configurable using simple JSON files
|
||||
- Built in websocket communication API for HTML5 frontend applications.
|
||||
- Is awesome
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ If the `CustomActivate()` method is long, consider breaking it up into many smal
|
|||
Note: It is best-practice in Essentials to not write arbitrarily-timed startup sequences to ensure that a "system" or room is functional. Rather, we encourage the developer to use various properties and conditions on devices to aggregate together "room is ready" statuses that can trigger further action. This ensures that all devices can be up and alive, allowing them to be debugged within a program that may otherwise be misbehaving - as well as not making users and expensive developers wait for code to start up!
|
||||
|
||||
```cs
|
||||
protected override bool CustomActivate()
|
||||
public override bool CustomActivate()
|
||||
{
|
||||
Debug.Console(0, this, "Final activation. Setting up actions and feedbacks");
|
||||
SetupFunctions();
|
||||
|
|
@ -52,7 +52,7 @@ We can see in the example below that during the `CustomActivate()` phase, we def
|
|||
### **Example**
|
||||
|
||||
```cs
|
||||
protected override bool CustomActivate()
|
||||
public override bool CustomActivate()
|
||||
{
|
||||
foreach (var i in Config.Inputs)
|
||||
{
|
||||
|
|
@ -115,7 +115,7 @@ The main task that should be undertaken in the `Initialize()` method for any 3rd
|
|||
|
||||
### Example (from `PepperDash.Essentials.Devices.Common.VideoCodec.Cisco.CiscoSparkCodec`)
|
||||
```cs
|
||||
protected override void Initialize()
|
||||
public override void Initialize()
|
||||
{
|
||||
var socket = Communication as ISocketStatus;
|
||||
if (socket != null)
|
||||
|
|
|
|||
|
|
@ -2,42 +2,58 @@
|
|||
|
||||
## TL;DR
|
||||
|
||||
Routing is defined by a connection graph or a wiring diagram. Routeable devices are sources, midpoints, or destinations. Devices are connected by tie lines. Tie lines represent the cables connecting devices, and are of type audio, video or both. Routes are made by telling a destination to get an audio/video/combined route from a source.
|
||||
Routing is defined by a connection graph or a wiring diagram. Routable devices are sources, midpoints, or destinations. Devices are connected by tie lines. Tie lines represent the cables connecting devices, and have specific signal types (audio, video, audioVideo, secondaryAudio, usbInput, usbOutput). Routes are made by telling a destination to get a route from a source for a specific signal type. Combined signal types (e.g., audioVideo) are automatically split into separate routing operations.
|
||||
|
||||
## Summary
|
||||
|
||||
Essentials routing is described by defining a graph of connections between devices in a system, typically in configuration. The audio, video and combination connections are like a wiring diagram. This graph is a collection of devices and tie lines, each tie line connecting a source device, source output port, destination device and destination input port. Tie lines are logically represented as a collection.
|
||||
|
||||
When routes are to be executed, Essentials will use this connection graph to decide on routes from source to destination. A method call is made on a destination, which says “destination, find a way for source xyz to get to you.” An algorithm analyzes the tie lines, instantly walking backwards from the destination, down every connection until it finds a complete path from the source. If a connected path is found, the algorithm then walks forward through all midpoints to the destination, executing switches as required until the full route is complete. The developer or configurer only needs to say “destination, get source xyz” and Essentials figures out how, regardless of what devices lie in between.
|
||||
When routes are to be executed, Essentials will use this connection graph to decide on routes from source to destination. A method call is made on a destination, which says "destination, find a way for source xyz to get to you." An algorithm analyzes the tie lines, instantly walking backwards from the destination, down every connection until it finds a complete path from the source. If a connected path is found, the algorithm then walks forward through all midpoints to the destination, executing switches as required until the full route is complete. The developer or configurer only needs to say "destination, get source xyz" and Essentials figures out how, regardless of what devices lie in between.
|
||||
|
||||
### Signal Type Handling
|
||||
|
||||
When a combined signal type like `audioVideo` is requested, Essentials automatically splits it into two separate routing operations—one for audio and one for video. Each signal type is routed independently through the system, ensuring that:
|
||||
- Audio-only tie lines can be used for the audio portion
|
||||
- Video-only tie lines can be used for the video portion
|
||||
- AudioVideo tie lines can be used for both portions
|
||||
|
||||
During path discovery, **only tie lines that support the requested signal type are considered**. For example, if a video route is requested, only tie lines with the video flag will be evaluated. This ensures signal compatibility throughout the entire routing chain.
|
||||
|
||||
### Port-Specific Routing
|
||||
|
||||
The routing system supports routing to and from specific ports on devices. You can specify:
|
||||
- A specific input port on the destination device
|
||||
- A specific output port on the source device
|
||||
- Both specific ports for precise routing control
|
||||
|
||||
When no specific ports are specified, the algorithm will automatically discover the appropriate ports based on available tie lines.
|
||||
|
||||
### Request Queuing
|
||||
|
||||
All routing requests are processed sequentially through a queue. For devices that implement warming/cooling behavior (e.g., projectors), route requests are automatically held when a device is cooling down and executed once the device is ready. This prevents routing errors and ensures proper device state management.
|
||||
|
||||
### Classes Referenced
|
||||
|
||||
* `PepperDash.Essentials.Core.IRoutingSource`
|
||||
* `PepperDash.Essentials.Core.IRoutingOutputs`
|
||||
* `PepperDash.Essentials.Core.IRoutingInputs`
|
||||
* `PepperDash.Essentials.Core.IRoutingMidpoint`
|
||||
* `PepperDash.Essentials.Core.IRoutingMidpointWithFeedback`
|
||||
* `PepperDash.Essentials.Core.IRoutingSinkWithFeedback`
|
||||
|
||||
> **Note:** As of the v3 routing refactor, the older `IRoutingInputsOutputs`, `IRouting`,
|
||||
> `IRoutingWithFeedback`, `IRoutingWithClear`, `IRoutingSink`, `IRoutingSinkWithInputPort`,
|
||||
> `IRoutingSinkNoSwitching`, `IRoutingSinkWithSwitching`, and `IRoutingSinkWithSwitchingWithInputPort`
|
||||
> interfaces have been removed and consolidated - see [Routing interfaces (v3)](#routing-interfaces-v3)
|
||||
> below for the current mapping.
|
||||
* `PepperDash.Essentials.Core.Routing.IRoutingSource`
|
||||
* `PepperDash.Essentials.Core.Routing.IRoutingOutputs`
|
||||
* `PepperDash.Essentials.Core.Routing.IRoutingInputs`
|
||||
* `PepperDash.Essentials.Core.Routing.IRoutingInputsOutputs`
|
||||
* `PepperDash.Essentials.Core.Routing.IRoutingSinkNoSwitching`
|
||||
* `PepperDash.Essentials.Core.Routing.IRoutingSinkWithSwitching`
|
||||
|
||||
## Example system, a simple presentation system
|
||||
|
||||
The diagram below shows the connections in a simple presentation system, with a few variations in connection paths. Example routes will be described following the diagram.
|
||||
|
||||
Each visible line between ports on devices represents a tie line. A tie line connects an output port on one device to an input port on another device, for example: an HDMI port on a document camera to an HDMI input on a matrix switcher. A tie line may be audio, video or both. It is essentially a logical representation of a physical cable in a system. This diagram has 12 tie lines, and those tie lines are defined in the tieLines array in configuration.
|
||||
Each visible line between ports on devices represents a tie line. A tie line connects an output port on one device to an input port on another device, for example: an HDMI port on a document camera to an HDMI input on a matrix switcher. A tie line has a signal type (audio, video, audioVideo, secondaryAudio, usbInput, or usbOutput) that determines what signals can travel through it. It is essentially a logical representation of a physical cable in a system. This diagram has 12 tie lines, and those tie lines are defined in the tieLines array in configuration.
|
||||
|
||||

|
||||
|
||||
Let’s go through some examples of routing, using pseudo-code:
|
||||
|
||||
1. Method call: “Projector 1, show Doc cam.” Routing will walk backwards through DM-RMC-3 and DM-8x8 iterating through all “wired up” ports until it finds a path back to the Doc cam. Routing will then step back through all devices in the discovered chain, switching routes on those that are switchable: Doc cam: no switching; DM 8x8: route input 3 to output 3; DM-RMC-3: no switching; Projector 1: Select input HDMI In. Route is complete.
|
||||
2. Method call: “Projector 2, show Laptop, video-only.” Routing will walk backwards through DM-RMC-4, DM 8x8, DM-TX-1, iterating through all connected ports until it finds a connection to the laptop. Routing then steps back through all devices, switching video where it can: Laptop: No switching; DM-TX-1: Select HDMI in; DM 8x8: Route input 5 to output 4; DM-RMC-4: No switching; Projector 2: Select HDMI input. Route is complete.
|
||||
3. Method call: “Amplifier, connect Laptop audio.” Again walking backwards to Laptop, as in #2 above. Switching will take place on DM-TX-1, DM 8x8, audio-only.
|
||||
2. Method call: "Projector 2, show Laptop, video-only." Routing will walk backwards through DM-RMC-4, DM 8x8, DM-TX-1, iterating through all connected ports until it finds a connection to the laptop. During this search, only tie lines that support video signals are considered. Routing then steps back through all devices, switching video where it can: Laptop: No switching; DM-TX-1: Select HDMI in; DM 8x8: Route input 5 to output 4; DM-RMC-4: No switching; Projector 2: Select HDMI input. Route is complete.
|
||||
3. Method call: "Amplifier, connect Laptop audio." Again walking backwards to Laptop, as in #2 above, but this time only tie lines supporting audio signals are evaluated. Switching will take place on DM-TX-1, DM 8x8, audio-only.
|
||||
4. Very simple call: “Lobby display, show signage controller.” Routing will walk back on HDMI input 1 and immediately find the signage controller. It then does a switch to HDMI 1 on the display.
|
||||
|
||||
All four of the above could be logically combined in a series of calls to define a possible “scene” in a room: Put Document camera on Projector 1, put Laptop on Projector 2 and the audio, put Signage on the Lobby display. They key takeaway is that the developer doesn’t need to define what is involved in making a certain route. The person configuring the system defines how it’s wired up, and the code only needs to tell a given destination to get a source, likely through configuration as well.
|
||||
|
|
@ -46,6 +62,37 @@ All of the above routes can be defined in source list routing tables, covered el
|
|||
|
||||
---
|
||||
|
||||
## Routing Algorithm Details
|
||||
|
||||
### Combined Signal Type Splitting
|
||||
|
||||
When an `audioVideo` route is requested, the routing system automatically splits it into two independent routing operations:
|
||||
|
||||
1. **Audio Route**: Finds the best path for audio signals from source to destination
|
||||
2. **Video Route**: Finds the best path for video signals from source to destination
|
||||
|
||||
Each route can take a different physical path through the system. For example:
|
||||
- Video might travel: Laptop → DM-TX-1 → DM Matrix → Display
|
||||
- Audio might travel: Laptop → DM-TX-1 → DM Matrix → Audio Processor → Amplifier
|
||||
|
||||
Both routes are discovered, stored, and executed independently. This allows for flexible system designs where audio and video follow different paths.
|
||||
|
||||
The same splitting behavior occurs for `Video + SecondaryAudio` requests, where video and secondary audio are routed as separate operations.
|
||||
|
||||
### Signal Type Filtering
|
||||
|
||||
At each step of the route discovery process, the algorithm filters tie lines based on the requested signal type:
|
||||
|
||||
- **Video request**: Only considers tie lines with the `video` flag set
|
||||
- **Audio request**: Only considers tie lines with the `audio` flag set
|
||||
- **AudioVideo request**: Routes audio and video separately, each following their respective filtering rules
|
||||
|
||||
If no tie line exists with the required signal type at any point in the chain, that path is rejected and the algorithm continues searching for an alternative route. If no valid path is found, the route request fails and no switching occurs.
|
||||
|
||||
This filtering ensures that incompatible signal types never interfere with routing decisions. For example, an audio-only cable will never be selected when routing video, preventing misconfiguration errors.
|
||||
|
||||
---
|
||||
|
||||
### Definitions
|
||||
|
||||
#### Ports
|
||||
|
|
@ -62,118 +109,109 @@ Source devices in Essentials must implement `IRoutingOutputs` or `IRoutingSource
|
|||
|
||||
A midpoint is a device in the middle of the signal chain. Typically a switcher, matrix or otherwise. Examples: DM chassis; DM-TX; DM-RMC; A video codec. These devices will have input and output ports.
|
||||
|
||||
Passthrough midpoints (no active switching) must implement `IRoutingMidpoint`. Midpoints that actively
|
||||
switch and report their current routes must implement `IRoutingMidpointWithFeedback`.
|
||||
Midpoint devices must implement `IRoutingInputsOutputs`. Midpoints with switching must implement `IRouting`.
|
||||
|
||||
#### Sink
|
||||
|
||||
A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkWithFeedback`.
|
||||
A sink is a device at the end of a full signal path. For example, a display, amplifier, encoder, etc. Sinks typically contain only input ports. They may or may not have switching, like a display with several inputs. Classes defining sink devices must implement `IRoutingSinkNoSwitching` or `IRoutingSinkWithSwitching`.
|
||||
|
||||
#### Tie-line
|
||||
|
||||
A tie-line is a logical representation of a physical cable connection between two devices. It has five properties that define how the tie-line connects two devices. A configuration snippet for a single tie line connecting HDMI output 1 on a Cisco RoomKit to HDMI input 1 on a display, carrying both audio and video, is shown below.
|
||||
A tie-line is a logical representation of a physical cable connection between two devices. It has five properties that define how the tie-line connects two devices.
|
||||
|
||||
##### How Tie Line Types Are Determined
|
||||
|
||||
The effective type of a tie line is determined by one of two methods:
|
||||
|
||||
1. **Automatic (Recommended)**: When no `type` property is specified in configuration, the tie line's type is automatically calculated as the **intersection** of signal types supported by both the source and destination ports. This ensures only compatible signals are considered for routing.
|
||||
|
||||
Example: If a source port supports `AudioVideo` and the destination port supports `Audio`, the tie line will have type `Audio` (the only common type).
|
||||
|
||||
2. **Manual Override**: When the `type` property is explicitly set, it overrides the automatic calculation. This is useful when the physical cable supports fewer signal types than both ports are capable of.
|
||||
|
||||
Example: Both ports support `AudioVideo`, but the cable only carries audio, so you set `"type": "audio"`.
|
||||
|
||||
##### Validation
|
||||
|
||||
At startup, tie line configurations are validated to ensure:
|
||||
- Both ports exist on their respective devices
|
||||
- The source and destination ports have at least one common signal type
|
||||
- If a `type` override is specified, both ports must support that signal type
|
||||
|
||||
Invalid tie lines will fail to build with descriptive error messages, preventing runtime routing issues.
|
||||
|
||||
##### Signal Types
|
||||
|
||||
Tie lines support the following signal types:
|
||||
|
||||
- `audio` - Audio-only signals
|
||||
- `video` - Video-only signals
|
||||
- `audioVideo` - Combined audio and video (automatically split during routing)
|
||||
- `secondaryAudio` - Secondary audio channel (e.g., program audio separate from microphone audio)
|
||||
- `usbInput` - USB input signals
|
||||
- `usbOutput` - USB output signals
|
||||
|
||||
The `type` property determines which signals can travel through the tie line. During route discovery, only tie lines matching the requested signal type will be considered as valid paths.
|
||||
|
||||
**Note**: In most cases, you should omit the `type` property and let the system automatically calculate it from the port capabilities. Only use it when you need to restrict the tie line to fewer signal types than the ports support or when needed for clarity.
|
||||
|
||||
##### Configuration Examples
|
||||
|
||||
**Example 1: Automatic type calculation (recommended)**
|
||||
|
||||
Connecting an HDMI cable between devices that both support audio and video. The `type` property is omitted, so the tie line will automatically support `AudioVideo`:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceKey": "ciscoSparkPlusCodec-1",
|
||||
"sourcePort": "HdmiOut1",
|
||||
"destinationKey": "display-1",
|
||||
"destinationPort": "HdmiIn1",
|
||||
"type": "audioVideo"
|
||||
"destinationPort": "HdmiIn1"
|
||||
}
|
||||
```
|
||||
|
||||
### Interfaces
|
||||
**Example 2: Type override for cable limitations**
|
||||
|
||||
#### Routing interfaces (v3)
|
||||
|
||||
The v3 routing refactor consolidated several older, overlapping interfaces into two feedback-aware
|
||||
interfaces. If you're updating a device implemented against the pre-v3 API, use this table to find
|
||||
its replacement:
|
||||
|
||||
| Old interface (pre-v3, removed) | Current interface |
|
||||
| --- | --- |
|
||||
| `IRoutingInputsOutputs` | `IRoutingMidpoint` |
|
||||
| `IRouting`, `IRoutingWithFeedback`, `IRoutingWithClear` | `IRoutingMidpointWithFeedback` |
|
||||
| `IRoutingSink`, `IRoutingSinkWithInputPort`, `IRoutingSinkWithSwitching`, `IRoutingSinkWithSwitchingWithInputPort` | `IRoutingSinkWithFeedback` |
|
||||
|
||||
**Base building-block interfaces** (unchanged):
|
||||
|
||||
* `IRoutingSource` - marker interface for a device that originates a signal path; extends `IRoutingOutputs`.
|
||||
* `IRoutingOutputs` - exposes `RoutingPortCollection<RoutingOutputPort> OutputPorts`.
|
||||
* `IRoutingInputs` - exposes `RoutingPortCollection<RoutingInputPort> InputPorts`.
|
||||
|
||||
**Midpoint interfaces:**
|
||||
|
||||
* `IRoutingMidpoint` - a passthrough device with both input and output ports but no active switching. Extends `IRoutingInputs` and `IRoutingOutputs`.
|
||||
* `IRoutingMidpointWithFeedback` - a midpoint that actively switches and reports feedback. Extends `IRoutingMidpoint` and adds:
|
||||
* `void ExecuteSwitch(object inputSelector, object outputSelector, eRoutingSignalType signalType)`
|
||||
* `void ClearRoute(object outputSelector, eRoutingSignalType signalType)`
|
||||
* `List<RouteSwitchDescriptor> CurrentRoutes { get; }`
|
||||
* `event RouteChangedEventHandler RouteChanged`
|
||||
|
||||
**Sink interface:**
|
||||
|
||||
* `IRoutingSinkWithFeedback` - a routing endpoint device that can switch inputs and provides feedback. Extends `IRoutingInputs`, `IKeyName`, and `ICurrentSources` (see below), and adds:
|
||||
* `void ExecuteSwitch(object inputSelector)`
|
||||
* `RoutingInputPort CurrentInputPort { get; }`
|
||||
* `event InputChangedEventHandler InputChanged`
|
||||
|
||||
#### Current source tracking (`ICurrentSources`)
|
||||
|
||||
`ICurrentSources` is a new interface (implemented by `IRoutingSinkWithFeedback`) that reports which
|
||||
source is actually feeding a sink right now, per signal type:
|
||||
|
||||
* `Dictionary<eRoutingSignalType, IRoutingSource> CurrentSources { get; }`
|
||||
* `Dictionary<eRoutingSignalType, string> CurrentSourceKeys { get; }`
|
||||
* `event EventHandler<CurrentSourcesChangedEventArgs> CurrentSourcesChanged`
|
||||
|
||||
A new `RoutingFeedbackManager` subscribes to `RouteChanged` on every `IRoutingMidpointWithFeedback`
|
||||
and `InputChanged` on every `IRoutingSinkWithFeedback` in the system, traces each change back through
|
||||
the tie-line graph to the originating source (debounced ~500ms), and keeps each sink's
|
||||
`ICurrentSources` state up to date automatically - developers no longer need to manually wire up
|
||||
source tracking per device.
|
||||
|
||||
#### Multiview layout support
|
||||
|
||||
For decoders that can display several independently-routable tiles at once (e.g. a WyreStorm
|
||||
NetworkHD-style multiview decoder), two additional interfaces are available:
|
||||
|
||||
* `IRoutingSinkWithLayouts` - extends `IRoutingSource` and exposes `Dictionary<int, IRoutingSinkWithFeedback> WindowTileSinks`, one per-tile "virtual sink" that can be routed independently like any other `IRoutingSinkWithFeedback`.
|
||||
* `IRoutingSinkWithLayoutState` - extends `IRoutingSinkWithLayouts` and additionally reports the current canvas/tile geometry via `MultiviewLayoutState CurrentLayout { get; }` and `event EventHandler<MultiviewLayoutStateEventArgs> LayoutChanged`.
|
||||
|
||||
`MultiviewLayoutState` is a JSON-serializable, product-agnostic model of the canvas: `CanvasWidth`,
|
||||
`CanvasHeight`, and a list of `MultiviewTileState` entries (`TileNumber`, `TileSinkKey`, `X`, `Y`,
|
||||
`Width`, `Height`, `ZOrder`, `SourceDeviceKey`) - suitable for driving a routing diagram UI.
|
||||
|
||||
#### Real-time routing feedback over WebSocket
|
||||
|
||||
Essentials Core includes a built-in WebSocket service, `RoutingFeedbackWebsocket`
|
||||
(`PepperDash.Essentials.Core.Web`), that broadcasts routing state changes to any connected client in
|
||||
real time - this is what powers the live routing diagram in the
|
||||
[Essentials Web Config App](https://github.com/PepperDash/essentials-web-config-app) and
|
||||
[essentials-devtools](https://github.com/PepperDash/essentials-devtools).
|
||||
|
||||
A client starts (or reuses) a session by sending an HTTP `GET` to the `routingFeedbackSession` API
|
||||
route (e.g. `https://[processor-ip]/cws/[appId]/api/routingFeedbackSession`), which starts the
|
||||
WebSocket server on a random high port (if not already running), forwards that port on the CS LAN
|
||||
adapter if present, and returns a JSON body with the connection URL(s):
|
||||
Both devices support `AudioVideo`, but the physical cable only carries audio. The `type` property restricts routing to audio only:
|
||||
|
||||
```json
|
||||
{ "url": "wss://[processor-ip]:[port]/routing/join/", "fallbackUrl": "wss://..." }
|
||||
{
|
||||
"sourceKey": "dmSwitcher-1",
|
||||
"sourcePort": "audioVideoOut1",
|
||||
"destinationKey": "amplifier-1",
|
||||
"destinationPort": "audioVideoIn1",
|
||||
"type": "audio"
|
||||
}
|
||||
```
|
||||
|
||||
Once connected, the client receives a `snapshot` message describing every midpoint route, sink
|
||||
input, and multiview layout currently active, followed by incremental update messages (each
|
||||
debounced ~200ms) as things change:
|
||||
**Example 3: Mismatched port types (automatically handled)**
|
||||
|
||||
| Message `type` | Sent when | Payload |
|
||||
| --- | --- | --- |
|
||||
| `snapshot` | On connect | `midpointRoutes`, `sinkRoutes`, `layouts` for the whole system |
|
||||
| `midpointRouteChanged` | An `IRoutingMidpointWithFeedback`'s `RouteChanged` fires | `deviceKey`, `routes: [{ inputPortKey, outputPortKey, signalType }]` |
|
||||
| `sinkInputChanged` | An `IRoutingSinkWithFeedback`'s `InputChanged` fires | `deviceKey`, `inputPortKey`, `sourceDeviceKey`, `signalType` |
|
||||
| `layoutChanged` | An `IRoutingSinkWithLayoutState`'s `LayoutChanged` fires | `deviceKey`, `layout` (a `MultiviewLayoutState`) |
|
||||
Source only supports audio, destination supports both. No `type` needed—the tie line will automatically be `Audio`:
|
||||
|
||||
Per-tile sinks belonging to an `IRoutingSinkWithLayouts` device are reported to clients as qualified
|
||||
input ports on their parent device's node, rather than as separate graph nodes, so a routing diagram
|
||||
can render a multiview decoder as a single device with per-tile inputs.
|
||||
```json
|
||||
{
|
||||
"sourceKey": "audioProcessor-1",
|
||||
"sourcePort": "audioOut1",
|
||||
"destinationKey": "dmSwitcher-1",
|
||||
"destinationPort": "audioVideoIn1"
|
||||
}
|
||||
```
|
||||
|
||||
**Invalid Example: Incompatible types**
|
||||
|
||||
This configuration will **fail validation** at startup because the ports have no common signal types:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceKey": "audioProcessor-1",
|
||||
"sourcePort": "audioOut1",
|
||||
"destinationKey": "display-1",
|
||||
"destinationPort": "hdmiIn1",
|
||||
"type": "video"
|
||||
}
|
||||
```
|
||||
Error: `"Override type 'Video' is not supported by source port 'audioOut1' (type: Audio)"`
|
||||
|
||||
### Interfaces
|
||||
|
||||
Todo: Define Interfaces IRouting, IRoutingOutputs, IRoutingInputs
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
# Mobile Control: architecture and client communication
|
||||
|
||||
## TL;DR
|
||||
|
||||
Mobile Control is a WebSocket-based messaging layer built into Essentials. Devices expose small
|
||||
"messenger" classes that register JSON message paths (e.g. `/device/{key}/startMeeting`) with a
|
||||
central `MobileControlSystemController`. Clients (typically a React app built on
|
||||
[`@pepperdash/mobile-control-react-app-core`](https://github.com/PepperDash/mobile-control-react-app-core))
|
||||
connect over WebSocket with a per-room token, send the same JSON envelope to invoke device methods,
|
||||
and receive unsolicited state-update messages that a Redux store uses to keep device/room state in
|
||||
sync in real time.
|
||||
|
||||
## Server-side architecture
|
||||
|
||||
### Projects involved
|
||||
|
||||
* `PepperDash.Essentials.MobileControl` - the `MobileControlSystemController` device, its
|
||||
WebSocket server (`WebSocketServer/MobileControlWebsocketServer.cs`), room bridges, and
|
||||
touchpanel controller.
|
||||
* `PepperDash.Essentials.MobileControl.Messengers` (namespace `PepperDash.Essentials.AppServer.Messengers`) -
|
||||
the `MessengerBase` class and the library of built-in messengers for common device interfaces
|
||||
(`IHasStartMeetingMessenger`, `IHasMeetingInfoMessenger`, `ICommunicationMonitorMessenger`,
|
||||
`IRoutingMidpointWithFeedbackMessenger`, etc.), plus the message envelope types.
|
||||
* `PepperDash.Essentials.Core.DeviceTypeInterfaces` - the public contracts:
|
||||
`IMobileControl`, `IMobileControlMessenger`, `IMobileControlMessengerWithSubscriptions`,
|
||||
`IMobileControlRoomMessenger`, `IMobileControlAction`, `IMobileControlMessage`, and the
|
||||
touchpanel-controller interfaces.
|
||||
|
||||
### Direct Server vs. Edge Server
|
||||
|
||||
Mobile Control supports two (non-exclusive) transport modes, configured on the `MobileControlConfig`
|
||||
device:
|
||||
|
||||
* **Direct Server** - `MobileControlWebsocketServer` runs an HTTPS WebSocket server directly on the
|
||||
processor hardware. Clients on the same network connect straight to the processor - lowest
|
||||
latency, no external dependency. This is what `mobile-control-react-app-core`'s local dev flow
|
||||
and the deployed `mcUserApp` React app use.
|
||||
* **Edge Server** (API server / cloud gateway) - the processor instead makes an *outbound* WebSocket
|
||||
connection to an external Mobile Control server, which relays messages between it and remote
|
||||
clients. Useful when clients can't reach the processor directly (e.g. no local network access).
|
||||
|
||||
Both modes can be enabled simultaneously; outgoing messages are queued and sent down whichever
|
||||
transport(s) are active.
|
||||
|
||||
### The WebSocket endpoint (Direct Server)
|
||||
|
||||
`MobileControlWebsocketServer` (`WebSocketServer/MobileControlWebsocketServer.cs`) hosts the
|
||||
Direct Server:
|
||||
|
||||
* **Path**: `/mc/api/ui/join/` (`_wsPath`)
|
||||
* **Port**: `50000 + <program slot number>` by default (e.g. program slot 2 → port `50002`), or a
|
||||
custom port from config
|
||||
* **User app**: static files are served from `/user/programX/mcUserApp` (`_appPath`) at the base
|
||||
href `/mc/app` (`_userAppBaseHref`) - this is exactly the `mcUserApp` deployment directory used
|
||||
when [deploying](../Get-started.md) a built React app.
|
||||
|
||||
### Connecting and authentication (tokens)
|
||||
|
||||
A client doesn't connect straight to the WebSocket - it first needs a **token** identifying which
|
||||
room/UI-client slot it's joining:
|
||||
|
||||
1. **Get a token** - on the processor console, run:
|
||||
```
|
||||
mobileinfo:[programSlot]
|
||||
```
|
||||
This prints the Direct Server port, any existing UI-client tokens, and full connect URLs (e.g.
|
||||
`http://[ip]:[port]/mc/app?token=[token]`). New tokens can also be added dynamically
|
||||
(`mobileadduiclient:[programSlot] [roomKey]`) - see the console command's help text for details.
|
||||
2. **Join a room** - the client calls `GET {apiPath}/ui/joinroom?token={token}` over HTTPS, which
|
||||
validates the token and returns room data including a generated `clientId`.
|
||||
3. **Open the WebSocket** - the client connects to
|
||||
`wss://[processor-ip]:[port]/mc/api/ui/join/{token}?clientId={clientId}`.
|
||||
|
||||
Each connected client is tracked as a `UiClient`; disconnecting removes its subscriptions from
|
||||
every messenger it was subscribed to.
|
||||
|
||||
### The message envelope
|
||||
|
||||
Every message sent in either direction is the same simple JSON envelope
|
||||
(`MobileControlMessage`, namespace `PepperDash.Essentials.AppServer.Messengers`):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "/device/zoomRoom1/startMeeting",
|
||||
"clientId": "abc-123",
|
||||
"content": { "value": 30 }
|
||||
}
|
||||
```
|
||||
|
||||
* `type` - a path identifying the target device/room and action (client → server), or the
|
||||
device/room whose state is being reported (server → client).
|
||||
* `clientId` - which client sent the message, or (server → client) which single client a targeted
|
||||
reply is meant for; omitted/ignored for broadcasts to all subscribers.
|
||||
* `content` - a `JToken` payload. For simple values, `MobileControlSimpleContent<T>` wraps a single
|
||||
`value` property (as in the example above); for state updates, `content` is the device's full
|
||||
serialized state object.
|
||||
|
||||
### The messenger pattern
|
||||
|
||||
Each device that wants to be controllable/observable over Mobile Control gets a small **messenger**
|
||||
class deriving from `MessengerBase` (`PepperDash.Essentials.AppServer.Messengers`):
|
||||
|
||||
```csharp
|
||||
public class IHasStartMeetingMessenger : MessengerBase
|
||||
{
|
||||
// ...
|
||||
protected override void RegisterActions()
|
||||
{
|
||||
AddAction("/fullStatus", (id, content) => SendFullStatus(id));
|
||||
|
||||
AddAction("/startMeeting", (id, content) =>
|
||||
{
|
||||
var msg = content.ToObject<MobileControlSimpleContent<uint>>();
|
||||
_startMeeting.StartMeeting(msg?.Value ?? _startMeeting.DefaultMeetingDurationMin);
|
||||
});
|
||||
|
||||
AddAction("/leaveMeeting", (id, content) => _startMeeting.LeaveMeeting());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* The messenger is constructed with a **base `MessagePath`** (e.g. `/device/zoomRoom1`) and calls
|
||||
`AddAction(subPath, handler)` for each supported action, relative to that base path.
|
||||
* `RegisterWithAppServer(IMobileControl appServerController)` registers the messenger's
|
||||
`HandleMessage` callback with the system controller via `IMobileControl.AddAction<T>(...)`.
|
||||
* When an incoming message's `type` starts with the messenger's `MessagePath`, `HandleMessage`
|
||||
strips that prefix (leaving e.g. `/startMeeting`) and dispatches to the matching registered
|
||||
action. The sending client is automatically added to that messenger's subscriber list, so it
|
||||
receives future unsolicited feedback from it.
|
||||
* To push state out, messengers call `PostStatusMessage(DeviceStateMessageBase message, clientId)` -
|
||||
omitting `clientId` broadcasts to every subscribed client; passing one targets a single client
|
||||
(e.g. replying to a `/fullStatus` request).
|
||||
|
||||
### How devices opt in
|
||||
|
||||
`EssentialsDevice.CustomActivate()` calls a virtual `CreateMobileControlMessengers()` hook once all
|
||||
devices have activated. A device (or a room) overrides this method to look up the `IMobileControl`
|
||||
device and construct/register whichever messengers it needs:
|
||||
|
||||
```csharp
|
||||
protected override void CreateMobileControlMessengers()
|
||||
{
|
||||
var mc = DeviceManager.AllDevices.OfType<IMobileControl>().FirstOrDefault();
|
||||
if (mc == null) return;
|
||||
|
||||
var messenger = new IHasStartMeetingMessenger("zoomRoom1-startMeeting", "/device/zoomRoom1", this);
|
||||
messenger.RegisterWithAppServer(mc);
|
||||
}
|
||||
```
|
||||
|
||||
Rooms typically implement `IMobileControlRoomMessenger` and register a room-level messenger at a
|
||||
`/room/{roomKey}` path (via a `MobileControlBridgeBase`/`MobileControlEssentialsRoomBridge`), in
|
||||
addition to any per-device messengers.
|
||||
|
||||
## Client-side architecture (React)
|
||||
|
||||
`@pepperdash/mobile-control-react-app-core` provides the client building blocks used by apps like
|
||||
[beincourt-pv2-react-app](https://github.com/pepperdash-beincourt/beincourt-pv2-react-app):
|
||||
|
||||
### Connection lifecycle
|
||||
|
||||
The real work happens in a Redux middleware (`src/lib/store/middleware/websocketMiddleware.ts`);
|
||||
`WebsocketProvider`/`WebsocketContext` are a thin, backward-compatible React Context wrapper around
|
||||
it that simply dispatches Redux actions (`wsConnect`, `wsSendMessage`, `wsReconnect`, ...).
|
||||
|
||||
1. On mount, `wsConnect()` is dispatched.
|
||||
2. The middleware reads `apiPath` from the app's local config (`_config.local.json`/
|
||||
`_config.default.json`) and the connection `token` (from the URL's `?token=` query param).
|
||||
3. It calls `GET {apiPath}/ui/joinroom?token={token}` to validate the token and get room data
|
||||
(including a `clientId`).
|
||||
4. It opens a WebSocket at `{apiPath with ws(s) scheme}/ui/join/{token}?clientId={clientId}`.
|
||||
5. On specific close codes (e.g. `4000` user code changed, `4002` room combination changed) it stops
|
||||
auto-reconnecting and surfaces an error; otherwise it automatically retries.
|
||||
|
||||
### Sending messages
|
||||
|
||||
```ts
|
||||
const { sendMessage } = useWebsocketContext();
|
||||
sendMessage('/device/zoomRoom1/startMeeting', { value: 30 });
|
||||
```
|
||||
|
||||
`sendMessage` serializes `{ type, clientId, content }` (the same envelope described above) and
|
||||
sends it over the open WebSocket.
|
||||
|
||||
### Receiving messages / state hydration
|
||||
|
||||
Incoming messages are routed by their `type` prefix:
|
||||
|
||||
| Prefix | Handling |
|
||||
| --- | --- |
|
||||
| `/system/*` | Internal system messages (user code, touchpanel key, room-combination/device-interface changes, initial sync complete, ...) |
|
||||
| `/event/*` | Dispatched to any handlers registered via `addEventHandler(eventType, key, callback)` |
|
||||
| `/room/*` | `dispatch(roomsActions.setRoomState(message))` |
|
||||
| `/device/*` | `dispatch(devicesActions.setDeviceState(message))` |
|
||||
|
||||
Device/room state lives in Redux slices keyed by device/room key, so any component can read the
|
||||
latest known state for a given key.
|
||||
|
||||
### The hook pattern
|
||||
|
||||
Each supported device interface has a small hook that combines reading state with calling actions,
|
||||
e.g. `useIHasStartMeeting`:
|
||||
|
||||
```ts
|
||||
export function useIHasStartMeeting(key: string): IHasStartMeetingReturn | undefined {
|
||||
const { sendMessage } = useWebsocketContext();
|
||||
const state = useGetDevice<IHasStartMeetingState>(key); // reads devices[key] from Redux
|
||||
|
||||
return useMemo(() => {
|
||||
if (!state) return undefined;
|
||||
const path = `/device/${key}`;
|
||||
return {
|
||||
state,
|
||||
startMeeting: (durationMin?: number) => sendMessage(`${path}/startMeeting`, { value: durationMin }),
|
||||
leaveMeeting: () => sendMessage(`${path}/leaveMeeting`, null),
|
||||
};
|
||||
}, [key, sendMessage, state]);
|
||||
}
|
||||
```
|
||||
|
||||
Components use these hooks to read live device state and call device actions without needing to
|
||||
know anything about the WebSocket transport underneath.
|
||||
|
||||
## Full round-trip example: starting a meeting
|
||||
|
||||
1. **UI**: user clicks "Start Meeting"; component calls `startMeeting(30)` from `useIHasStartMeeting('zoomRoom1')`.
|
||||
2. **Client → server**: `sendMessage('/device/zoomRoom1/startMeeting', { value: 30 })` sends
|
||||
`{"type":"/device/zoomRoom1/startMeeting","clientId":"abc-123","content":{"value":30}}` over the WebSocket.
|
||||
3. **Server routing**: the system controller matches `/device/zoomRoom1` against the registered
|
||||
`IHasStartMeetingMessenger`'s `MessagePath`, strips the prefix to get `/startMeeting`, and invokes
|
||||
its registered action, which calls `StartMeeting(30)` on the underlying device.
|
||||
4. **Device fires feedback**: the device's meeting-started feedback fires.
|
||||
5. **Server → clients**: the messenger calls `PostStatusMessage(...)`, broadcasting
|
||||
`{"type":"/device/zoomRoom1","clientId":null,"content":{ ...state }}` to every client subscribed
|
||||
to that messenger.
|
||||
6. **Client receives**: the message's `type` starts with `/device/`, so
|
||||
`dispatch(devicesActions.setDeviceState(message))` merges the new state into Redux under
|
||||
`devices['zoomRoom1']`.
|
||||
7. **UI updates**: `useIHasStartMeeting('zoomRoom1')`'s `state` reflects the new meeting info on the
|
||||
next render.
|
||||
|
|
@ -38,8 +38,6 @@
|
|||
href: technical-docs/Plugins.md
|
||||
- name: Communication Basics
|
||||
href: technical-docs/Communication-Basics.md
|
||||
- name: Mobile Control
|
||||
href: technical-docs/Mobile-Control.md
|
||||
- name: Debugging
|
||||
href: technical-docs/Debugging.md
|
||||
- name: Feedback Classes
|
||||
|
|
|
|||
|
|
@ -183,11 +183,12 @@ namespace PepperDash.Core
|
|||
Cresnet = 8,
|
||||
Cec = 9,
|
||||
Udp = 10,
|
||||
UdpClient = 11,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, and ```Udp```.
|
||||
These enumerations are not case sensitive. Not all methods are valid for a ```genericComm``` device. For a comport, the only valid type would be ```Com```. For a direct network socket, valid options are ```Ssh```, ```Tcpip```, ```Telnet```, ```UdpClient```, and ```Udp```.
|
||||
|
||||
##### ComParams
|
||||
|
||||
|
|
@ -287,7 +288,7 @@ This property maps to the number of the port on the device you have mapped the r
|
|||
|
||||
##### TcpSshParams
|
||||
|
||||
A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
||||
A ```Ssh```, ```TcpIp```, ```UdpClient```, or ```Udp``` device requires a ```tcpSshProperties``` object to set the propeties of the socket.
|
||||
|
||||
```Json
|
||||
{
|
||||
|
|
@ -304,7 +305,7 @@ A ```Ssh```, ```TcpIp```, or ```Udp``` device requires a ```tcpSshProperties```
|
|||
|
||||
**```address```**
|
||||
|
||||
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. In the case of a UDP device, you can set either a single whitelist address with this data, or an appropriate broadcast address.
|
||||
This is the IP address, hostname, or FQDN of the resource you wish to open a socket to. Use ```UdpClient``` for outbound UDP to a remote endpoint. Use ```Udp``` when you need Essentials to bind a local UDP listener.
|
||||
|
||||
**```port```**
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"runtimeOptions": {
|
||||
"configProperties": {
|
||||
"System.Globalization.Invariant": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>3.0.0-local</Version>
|
||||
<Version>2.42.1-local</Version>
|
||||
<InformationalVersion>$(Version)</InformationalVersion>
|
||||
<Authors>PepperDash Technology</Authors>
|
||||
<Company>PepperDash Technology</Company>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<Project>
|
||||
<ItemGroup>
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != ''">
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library'">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build;</PackagePath>
|
||||
</None>
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != '' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' )">
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program'">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build;</PackagePath>
|
||||
</None>
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary' And '$(TargetFramework)' != '' And '$(TargetName)' != '' And '$(TargetDir)' != ''">
|
||||
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary'">
|
||||
<Pack>true</Pack>
|
||||
<PackagePath>build;</PackagePath>
|
||||
</None>
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
namespace PepperDash.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Allows pre-registration of Crestron service implementations before the <c>Debug</c>
|
||||
/// static class initialises. Call <see cref="Register"/> from the composition root
|
||||
/// (e.g. ControlSystem constructor) <em>before</em> any code touches <c>Debug.*</c>.
|
||||
/// Test projects should call it with no-op / in-memory implementations so that the
|
||||
/// <c>Debug</c> static constructor never tries to reach the real Crestron SDK.
|
||||
/// </summary>
|
||||
public static class DebugServiceRegistration
|
||||
{
|
||||
/// <summary>Gets the registered environment abstraction, or <c>null</c> if not registered.</summary>
|
||||
public static ICrestronEnvironment? Environment { get; private set; }
|
||||
|
||||
/// <summary>Gets the registered console abstraction, or <c>null</c> if not registered.</summary>
|
||||
public static ICrestronConsole? Console { get; private set; }
|
||||
|
||||
/// <summary>Gets the registered data-store abstraction, or <c>null</c> if not registered.</summary>
|
||||
public static ICrestronDataStore? DataStore { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Registers the service implementations that <c>Debug</c> will use when its
|
||||
/// static constructor runs. Any parameter may be <c>null</c> to leave the
|
||||
/// corresponding service unregistered (the <c>Debug</c> class will skip that
|
||||
/// capability gracefully).
|
||||
/// </summary>
|
||||
public static void Register(
|
||||
ICrestronEnvironment? environment,
|
||||
ICrestronConsole? console,
|
||||
ICrestronDataStore? dataStore)
|
||||
{
|
||||
Environment = environment;
|
||||
Console = console;
|
||||
DataStore = dataStore;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
namespace PepperDash.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Crestron's <c>eDevicePlatform</c> without requiring the Crestron SDK.
|
||||
/// </summary>
|
||||
public enum DevicePlatform
|
||||
{
|
||||
/// <summary>Hardware appliance (e.g. CP4, MC4).</summary>
|
||||
Appliance,
|
||||
/// <summary>Crestron Virtual Control / server runtime.</summary>
|
||||
Server,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Crestron's <c>eRuntimeEnvironment</c>.
|
||||
/// </summary>
|
||||
public enum RuntimeEnvironment
|
||||
{
|
||||
/// <summary>SimplSharpPro program slot (hardware 4-series).</summary>
|
||||
SimplSharpPro,
|
||||
/// <summary>SimplSharp (older 3-series or server environments).</summary>
|
||||
SimplSharp,
|
||||
/// <summary>Any other environment — check for completeness.</summary>
|
||||
Other,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Crestron's <c>ConsoleAccessLevelEnum</c>.
|
||||
/// </summary>
|
||||
public enum ConsoleAccessLevel
|
||||
{
|
||||
AccessAdministrator = 0,
|
||||
AccessOperator = 1,
|
||||
AccessProgrammer = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Crestron's <c>eProgramStatusEventType</c>.
|
||||
/// </summary>
|
||||
public enum ProgramStatusEventType
|
||||
{
|
||||
Starting,
|
||||
Stopping,
|
||||
Paused,
|
||||
Resumed,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the event type used by Crestron's <c>EthernetEventArgs</c>.
|
||||
/// </summary>
|
||||
public enum EthernetEventType
|
||||
{
|
||||
LinkDown = 0,
|
||||
LinkUp = 1,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for Crestron ethernet link events.
|
||||
/// </summary>
|
||||
public class PepperDashEthernetEventArgs : EventArgs
|
||||
{
|
||||
public EthernetEventType EthernetEventType { get; }
|
||||
public short EthernetAdapter { get; }
|
||||
|
||||
public PepperDashEthernetEventArgs(EthernetEventType eventType, short adapter)
|
||||
{
|
||||
EthernetEventType = eventType;
|
||||
EthernetAdapter = adapter;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the set of <c>CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET</c> values
|
||||
/// used across this codebase — does not aim to be exhaustive.
|
||||
/// </summary>
|
||||
public enum EthernetParameterType
|
||||
{
|
||||
GetCurrentIpAddress,
|
||||
GetHostname,
|
||||
GetDomainName,
|
||||
GetLinkStatus,
|
||||
GetCurrentDhcpState,
|
||||
GetCurrentIpMask,
|
||||
GetCurrentRouter,
|
||||
GetMacAddress,
|
||||
GetDnsServer,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors Crestron's <c>SocketStatus</c> without requiring the Crestron SDK.
|
||||
/// </summary>
|
||||
public enum PepperDashSocketStatus
|
||||
{
|
||||
SocketNotConnected = 0,
|
||||
SocketConnected = 2,
|
||||
SocketConnectionInProgress = 6,
|
||||
SocketConnectFailed = 11,
|
||||
SocketDisconnecting = 12,
|
||||
SocketBrokenRemotely = 7,
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
namespace PepperDash.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts <c>Crestron.SimplSharp.CrestronConsole</c> to allow unit testing
|
||||
/// without the Crestron SDK.
|
||||
/// </summary>
|
||||
public interface ICrestronConsole
|
||||
{
|
||||
/// <summary>Prints a line to the Crestron console/telnet output.</summary>
|
||||
void PrintLine(string message);
|
||||
|
||||
/// <summary>Prints text (without newline) to the Crestron console/telnet output.</summary>
|
||||
void Print(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a response string to the console for the currently-executing console command.
|
||||
/// </summary>
|
||||
void ConsoleCommandResponse(string message);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new command with the Crestron console.
|
||||
/// </summary>
|
||||
/// <param name="callback">Handler invoked when the command is typed.</param>
|
||||
/// <param name="command">Command name (no spaces).</param>
|
||||
/// <param name="helpText">Help text shown by the Crestron console.</param>
|
||||
/// <param name="accessLevel">Minimum access level required to run the command.</param>
|
||||
void AddNewConsoleCommand(
|
||||
Action<string> callback,
|
||||
string command,
|
||||
string helpText,
|
||||
ConsoleAccessLevel accessLevel);
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
namespace PepperDash.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts <c>Crestron.SimplSharp.CrestronDataStore.CrestronDataStoreStatic</c>
|
||||
/// to allow unit testing without the Crestron SDK.
|
||||
/// </summary>
|
||||
public interface ICrestronDataStore
|
||||
{
|
||||
/// <summary>Initialises the data store. Must be called once before any other operation.</summary>
|
||||
void InitStore();
|
||||
|
||||
/// <summary>Reads an integer value from the local (program-slot) store.</summary>
|
||||
/// <returns><c>true</c> if the value was found and read successfully.</returns>
|
||||
bool TryGetLocalInt(string key, out int value);
|
||||
|
||||
/// <summary>Writes an integer value to the local (program-slot) store.</summary>
|
||||
/// <returns><c>true</c> on success.</returns>
|
||||
bool SetLocalInt(string key, int value);
|
||||
|
||||
/// <summary>Writes an unsigned integer value to the local (program-slot) store.</summary>
|
||||
/// <returns><c>true</c> on success.</returns>
|
||||
bool SetLocalUint(string key, uint value);
|
||||
|
||||
/// <summary>Reads a boolean value from the local (program-slot) store.</summary>
|
||||
/// <returns><c>true</c> if the value was found and read successfully.</returns>
|
||||
bool TryGetLocalBool(string key, out bool value);
|
||||
|
||||
/// <summary>Writes a boolean value to the local (program-slot) store.</summary>
|
||||
/// <returns><c>true</c> on success.</returns>
|
||||
bool SetLocalBool(string key, bool value);
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
namespace PepperDash.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts <c>Crestron.SimplSharp.CrestronEnvironment</c> to allow unit testing
|
||||
/// without the Crestron SDK.
|
||||
/// </summary>
|
||||
public interface ICrestronEnvironment
|
||||
{
|
||||
/// <summary>Gets the platform the program is executing on.</summary>
|
||||
DevicePlatform DevicePlatform { get; }
|
||||
|
||||
/// <summary>Gets the current runtime environment.</summary>
|
||||
RuntimeEnvironment RuntimeEnvironment { get; }
|
||||
|
||||
/// <summary>Gets the platform-appropriate newline string.</summary>
|
||||
string NewLine { get; }
|
||||
|
||||
/// <summary>Gets the application number (program slot).</summary>
|
||||
uint ApplicationNumber { get; }
|
||||
|
||||
/// <summary>Gets the room ID (used in Crestron Virtual Control / server environments).</summary>
|
||||
uint RoomId { get; }
|
||||
|
||||
/// <summary>Raised when program status changes (starting, stopping, etc.).</summary>
|
||||
event EventHandler<ProgramStatusEventArgs> ProgramStatusChanged;
|
||||
|
||||
/// <summary>Raised when the ethernet link changes state.</summary>
|
||||
event EventHandler<PepperDashEthernetEventArgs> EthernetEventReceived;
|
||||
|
||||
/// <summary>Gets the application root directory path.</summary>
|
||||
string GetApplicationRootDirectory();
|
||||
|
||||
/// <summary>
|
||||
/// Returns <c>true</c> when running on real Crestron hardware.
|
||||
/// Returns <c>false</c> in test / dev environments so that SDK-specific
|
||||
/// sinks and enrichers can be safely skipped.
|
||||
/// </summary>
|
||||
bool IsHardwareRuntime { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for <see cref="ICrestronEnvironment.ProgramStatusChanged"/>.
|
||||
/// </summary>
|
||||
public class ProgramStatusEventArgs : EventArgs
|
||||
{
|
||||
public ProgramStatusEventType EventType { get; }
|
||||
|
||||
public ProgramStatusEventArgs(ProgramStatusEventType eventType)
|
||||
{
|
||||
EventType = eventType;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
namespace PepperDash.Core.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Abstracts <c>Crestron.SimplSharp.CrestronEthernetHelper</c> to allow unit testing
|
||||
/// without the Crestron SDK.
|
||||
/// </summary>
|
||||
public interface IEthernetHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a network parameter string for the specified adapter.
|
||||
/// </summary>
|
||||
/// <param name="parameter">The parameter to retrieve.</param>
|
||||
/// <param name="ethernetAdapterId">Ethernet adapter index (0 = LAN A).</param>
|
||||
/// <returns>String value of the requested parameter.</returns>
|
||||
string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId);
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>PepperDash.Core.Abstractions</RootNamespace>
|
||||
<AssemblyName>PepperDash.Core.Abstractions</AssemblyName>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Deterministic>true</Deterministic>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<Title>PepperDash Core Abstractions</Title>
|
||||
<Company>PepperDash Technologies</Company>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/PepperDash/PepperDashCore</RepositoryUrl>
|
||||
<NullableContextOptions>enable</NullableContextOptions>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<InformationalVersion>$(Version)</InformationalVersion>
|
||||
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||
<!-- No Crestron SDK reference — this project must remain hardware-agnostic so test projects can reference it -->
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
using FluentAssertions;
|
||||
using PepperDash.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace PepperDash.Core.Tests.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="Device"/> — the base class for all PepperDash devices.
|
||||
/// These run without Crestron hardware; Debug is initialized with fakes via TestInitializer.
|
||||
/// </summary>
|
||||
public class DeviceTests
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// Construction
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SingleArg_SetsKey()
|
||||
{
|
||||
var device = new Device("my-device");
|
||||
|
||||
device.Key.Should().Be("my-device");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SingleArg_SetsNameToEmpty()
|
||||
{
|
||||
var device = new Device("my-device");
|
||||
|
||||
device.Name.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_TwoArg_SetsKeyAndName()
|
||||
{
|
||||
var device = new Device("my-device", "My Device");
|
||||
|
||||
device.Key.Should().Be("my-device");
|
||||
device.Name.Should().Be("My Device");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_KeyWithDot_StillSetsKey()
|
||||
{
|
||||
// The dot triggers a debug log warning but must not prevent construction.
|
||||
var device = new Device("parent.child");
|
||||
|
||||
device.Key.Should().Be("parent.child");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ToString
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithName_FormatsKeyDashName()
|
||||
{
|
||||
var device = new Device("cam-01", "Front Camera");
|
||||
|
||||
device.ToString().Should().Be("cam-01 - Front Camera");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_WithoutName_UsesDashPlaceholder()
|
||||
{
|
||||
var device = new Device("cam-01");
|
||||
|
||||
device.ToString().Should().Be("cam-01 - ---");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// DefaultDevice
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void DefaultDevice_IsNotNull()
|
||||
{
|
||||
Device.DefaultDevice.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultDevice_HasKeyDefault()
|
||||
{
|
||||
Device.DefaultDevice.Key.Should().Be("Default");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// CustomActivate / Activate / Deactivate / Initialize
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void CustomActivate_DefaultReturnTrue()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
|
||||
device.CallCustomActivate().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deactivate_DefaultReturnsTrue()
|
||||
{
|
||||
var device = new Device("d1");
|
||||
|
||||
device.Deactivate().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_CallsCustomActivate_AndReturnsItsResult()
|
||||
{
|
||||
var stub = new ActivateTrackingDevice("d1", result: false);
|
||||
|
||||
stub.Activate().Should().BeFalse();
|
||||
stub.CustomActivateCalled.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Activate_TrueWhenCustomActivateReturnsTrue()
|
||||
{
|
||||
var stub = new ActivateTrackingDevice("d1", result: true);
|
||||
|
||||
stub.Activate().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_DoesNotThrow()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var act = () => device.CallInitialize();
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// PreActivate
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void PreActivate_NoActions_DoesNotThrow()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var act = () => device.PreActivate();
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreActivate_RunsRegisteredActionsInOrder()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var order = new List<int>();
|
||||
|
||||
device.AddPreActivationAction(() => order.Add(1));
|
||||
device.AddPreActivationAction(() => order.Add(2));
|
||||
device.AddPreActivationAction(() => order.Add(3));
|
||||
|
||||
device.PreActivate();
|
||||
|
||||
order.Should().Equal(1, 2, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreActivate_ContinuesAfterFaultingAction()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var reached = false;
|
||||
|
||||
device.AddPreActivationAction(() => throw new InvalidOperationException("boom"));
|
||||
device.AddPreActivationAction(() => reached = true);
|
||||
|
||||
var act = () => device.PreActivate();
|
||||
|
||||
act.Should().NotThrow("exceptions in individual actions must be caught internally");
|
||||
reached.Should().BeTrue("actions after a faulting action must still run");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// PostActivate
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void PostActivate_NoActions_DoesNotThrow()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var act = () => device.PostActivate();
|
||||
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostActivate_RunsRegisteredActionsInOrder()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var order = new List<int>();
|
||||
|
||||
device.AddPostActivationAction(() => order.Add(1));
|
||||
device.AddPostActivationAction(() => order.Add(2));
|
||||
|
||||
device.PostActivate();
|
||||
|
||||
order.Should().Equal(1, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostActivate_ContinuesAfterFaultingAction()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var reached = false;
|
||||
|
||||
device.AddPostActivationAction(() => throw new Exception("boom"));
|
||||
device.AddPostActivationAction(() => reached = true);
|
||||
|
||||
var act = () => device.PostActivate();
|
||||
|
||||
act.Should().NotThrow();
|
||||
reached.Should().BeTrue();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Pre and Post actions are independent lists
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void PreActivationActions_DoNotRunOnPostActivate()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var preRan = false;
|
||||
|
||||
device.AddPreActivationAction(() => preRan = true);
|
||||
device.PostActivate();
|
||||
|
||||
preRan.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PostActivationActions_DoNotRunOnPreActivate()
|
||||
{
|
||||
var device = new TestDevice("d1");
|
||||
var postRan = false;
|
||||
|
||||
device.AddPostActivationAction(() => postRan = true);
|
||||
device.PreActivate();
|
||||
|
||||
postRan.Should().BeFalse();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// OnFalse
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void OnFalse_FiresAction_WhenBoolIsFalse()
|
||||
{
|
||||
var device = new Device("d1");
|
||||
var fired = false;
|
||||
|
||||
device.OnFalse(false, () => fired = true);
|
||||
|
||||
fired.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnFalse_DoesNotFireAction_WhenBoolIsTrue()
|
||||
{
|
||||
var device = new Device("d1");
|
||||
var fired = false;
|
||||
|
||||
device.OnFalse(true, () => fired = true);
|
||||
|
||||
fired.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnFalse_DoesNotFireAction_ForNonBoolType()
|
||||
{
|
||||
var device = new Device("d1");
|
||||
var fired = false;
|
||||
|
||||
device.OnFalse("not a bool", () => fired = true);
|
||||
device.OnFalse(0, () => fired = true);
|
||||
device.OnFalse(null!, () => fired = true);
|
||||
|
||||
fired.Should().BeFalse();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Exposes protected Device members so test methods can call them directly.
|
||||
/// </summary>
|
||||
private class TestDevice : Device
|
||||
{
|
||||
public TestDevice(string key) : base(key) { }
|
||||
public TestDevice(string key, string name) : base(key, name) { }
|
||||
|
||||
public void AddPreActivationAction(Action act) => base.AddPreActivationAction(act);
|
||||
public void AddPostActivationAction(Action act) => base.AddPostActivationAction(act);
|
||||
public bool CallCustomActivate() => base.CustomActivate();
|
||||
public void CallInitialize() => base.Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records whether CustomActivate was invoked and returns a configured result.
|
||||
/// Used to verify Activate() correctly delegates to CustomActivate().
|
||||
/// </summary>
|
||||
private sealed class ActivateTrackingDevice : Device
|
||||
{
|
||||
private readonly bool _result;
|
||||
public bool CustomActivateCalled { get; private set; }
|
||||
|
||||
public ActivateTrackingDevice(string key, bool result = true) : base(key)
|
||||
{
|
||||
_result = result;
|
||||
}
|
||||
|
||||
protected override bool CustomActivate()
|
||||
{
|
||||
CustomActivateCalled = true;
|
||||
return _result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
using PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// No-op ICrestronConsole that captures output for test assertions.
|
||||
/// </summary>
|
||||
public class CapturingCrestronConsole : ICrestronConsole
|
||||
{
|
||||
public List<string> Lines { get; } = new();
|
||||
public List<string> CommandResponses { get; } = new();
|
||||
public List<(string Command, string HelpText)> RegisteredCommands { get; } = new();
|
||||
|
||||
public void PrintLine(string message) => Lines.Add(message);
|
||||
public void Print(string message) => Lines.Add(message);
|
||||
public void ConsoleCommandResponse(string message) => CommandResponses.Add(message);
|
||||
|
||||
public void AddNewConsoleCommand(
|
||||
Action<string> callback,
|
||||
string command,
|
||||
string helpText,
|
||||
ConsoleAccessLevel accessLevel)
|
||||
{
|
||||
RegisteredCommands.Add((command, helpText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal no-op ICrestronConsole that discards all output. Useful when you only
|
||||
/// care about the system under test and not what it logs.
|
||||
/// </summary>
|
||||
public class NoOpCrestronConsole : ICrestronConsole
|
||||
{
|
||||
public void PrintLine(string message) { }
|
||||
public void Print(string message) { }
|
||||
public void ConsoleCommandResponse(string message) { }
|
||||
public void AddNewConsoleCommand(Action<string> _, string __, string ___, ConsoleAccessLevel ____) { }
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
using PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// Configurable ICrestronEnvironment for unit tests.
|
||||
/// Defaults: Appliance / SimplSharpPro / ApplicationNumber=1.
|
||||
/// </summary>
|
||||
public class FakeCrestronEnvironment : ICrestronEnvironment
|
||||
{
|
||||
public DevicePlatform DevicePlatform { get; set; } = DevicePlatform.Appliance;
|
||||
public RuntimeEnvironment RuntimeEnvironment { get; set; } = RuntimeEnvironment.SimplSharpPro;
|
||||
public string NewLine { get; set; } = "\r\n";
|
||||
public uint ApplicationNumber { get; set; } = 1;
|
||||
public uint RoomId { get; set; } = 0;
|
||||
|
||||
public event EventHandler<ProgramStatusEventArgs>? ProgramStatusChanged;
|
||||
public event EventHandler<PepperDashEthernetEventArgs>? EthernetEventReceived;
|
||||
|
||||
public string GetApplicationRootDirectory() => System.IO.Path.GetTempPath();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsHardwareRuntime => false;
|
||||
|
||||
/// <summary>Simulates a program status event for tests.</summary>
|
||||
public void RaiseProgramStatus(ProgramStatusEventType type) =>
|
||||
ProgramStatusChanged?.Invoke(this, new ProgramStatusEventArgs(type));
|
||||
|
||||
/// <summary>Simulates an ethernet event for tests.</summary>
|
||||
public void RaiseEthernetEvent(EthernetEventType type, short adapter = 0) =>
|
||||
EthernetEventReceived?.Invoke(this, new PepperDashEthernetEventArgs(type, adapter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No-op IEthernetHelper that returns configurable values.
|
||||
/// </summary>
|
||||
public class FakeEthernetHelper : IEthernetHelper
|
||||
{
|
||||
private readonly Dictionary<EthernetParameterType, string> _values = new();
|
||||
|
||||
public FakeEthernetHelper Seed(EthernetParameterType param, string value)
|
||||
{
|
||||
_values[param] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId) =>
|
||||
_values.TryGetValue(parameter, out var v) ? v : string.Empty;
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
using PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Tests.Fakes;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory ICrestronDataStore backed by a dictionary.
|
||||
/// Use in unit tests to verify that keys are read from and written to the store correctly.
|
||||
/// </summary>
|
||||
public class InMemoryCrestronDataStore : ICrestronDataStore
|
||||
{
|
||||
private readonly Dictionary<string, object> _store = new();
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public void InitStore() => Initialized = true;
|
||||
|
||||
public bool TryGetLocalInt(string key, out int value)
|
||||
{
|
||||
if (_store.TryGetValue(key, out var raw) && raw is int i)
|
||||
{
|
||||
value = i;
|
||||
return true;
|
||||
}
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool SetLocalInt(string key, int value)
|
||||
{
|
||||
_store[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetLocalUint(string key, uint value)
|
||||
{
|
||||
_store[key] = (int)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetLocalBool(string key, out bool value)
|
||||
{
|
||||
if (_store.TryGetValue(key, out var raw) && raw is bool b)
|
||||
{
|
||||
value = b;
|
||||
return true;
|
||||
}
|
||||
value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool SetLocalBool(string key, bool value)
|
||||
{
|
||||
_store[key] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Seeds a key for testing read paths.</summary>
|
||||
public void Seed(string key, object value) => _store[key] = value;
|
||||
}
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
using FluentAssertions;
|
||||
using PepperDash.Core.Abstractions;
|
||||
using PepperDash.Core.Tests.Fakes;
|
||||
using Xunit;
|
||||
|
||||
namespace PepperDash.Core.Tests.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for Debug-related service interfaces and implementations.
|
||||
/// These tests verify the behaviour of the abstractions in isolation (no Crestron SDK required).
|
||||
/// </summary>
|
||||
public class DebugServiceTests
|
||||
{
|
||||
// -----------------------------------------------------------------------
|
||||
// ICrestronDataStore — InMemoryCrestronDataStore
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void DataStore_InitStore_SetsInitializedFlag()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
store.Initialized.Should().BeFalse("not yet initialized");
|
||||
|
||||
store.InitStore();
|
||||
|
||||
store.Initialized.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStore_SetAndGetLocalInt_RoundTrips()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
|
||||
store.SetLocalInt("MyKey", 42).Should().BeTrue();
|
||||
store.TryGetLocalInt("MyKey", out var value).Should().BeTrue();
|
||||
value.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStore_TryGetLocalInt_ReturnsFalse_WhenKeyAbsent()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
|
||||
store.TryGetLocalInt("Missing", out var value).Should().BeFalse();
|
||||
value.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStore_SetAndGetLocalBool_RoundTrips()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
|
||||
store.SetLocalBool("FlagKey", true).Should().BeTrue();
|
||||
store.TryGetLocalBool("FlagKey", out var value).Should().BeTrue();
|
||||
value.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStore_TryGetLocalBool_ReturnsFalse_WhenKeyAbsent()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
|
||||
store.TryGetLocalBool("Missing", out var value).Should().BeFalse();
|
||||
value.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStore_SetLocalUint_CanBeReadBackAsInt()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
|
||||
store.SetLocalUint("UintKey", 3u).Should().BeTrue();
|
||||
store.TryGetLocalInt("UintKey", out var value).Should().BeTrue();
|
||||
value.Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DataStore_Seed_AllowsTestSetupOfReadPaths()
|
||||
{
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
store.Seed("MyLevel", 2);
|
||||
|
||||
store.TryGetLocalInt("MyLevel", out var level).Should().BeTrue();
|
||||
level.Should().Be(2);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// DebugServiceRegistration
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void ServiceRegistration_Register_StoresAllThreeServices()
|
||||
{
|
||||
var env = new FakeCrestronEnvironment();
|
||||
var console = new NoOpCrestronConsole();
|
||||
var store = new InMemoryCrestronDataStore();
|
||||
|
||||
DebugServiceRegistration.Register(env, console, store);
|
||||
|
||||
DebugServiceRegistration.Environment.Should().BeSameAs(env);
|
||||
DebugServiceRegistration.Console.Should().BeSameAs(console);
|
||||
DebugServiceRegistration.DataStore.Should().BeSameAs(store);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServiceRegistration_Register_AcceptsNullsWithoutThrowing()
|
||||
{
|
||||
var act = () => DebugServiceRegistration.Register(null, null, null);
|
||||
act.Should().NotThrow();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ICrestronEnvironment — FakeCrestronEnvironment
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void FakeEnvironment_DefaultsToAppliance()
|
||||
{
|
||||
var env = new FakeCrestronEnvironment();
|
||||
env.DevicePlatform.Should().Be(DevicePlatform.Appliance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FakeEnvironment_RaiseProgramStatus_FiresEvent()
|
||||
{
|
||||
var env = new FakeCrestronEnvironment();
|
||||
ProgramStatusEventType? received = null;
|
||||
env.ProgramStatusChanged += (_, e) => received = e.EventType;
|
||||
|
||||
env.RaiseProgramStatus(ProgramStatusEventType.Stopping);
|
||||
|
||||
received.Should().Be(ProgramStatusEventType.Stopping);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FakeEnvironment_RaiseEthernetEvent_FiresEvent()
|
||||
{
|
||||
var env = new FakeCrestronEnvironment();
|
||||
EthernetEventType? received = null;
|
||||
env.EthernetEventReceived += (_, e) => received = e.EthernetEventType;
|
||||
|
||||
env.RaiseEthernetEvent(EthernetEventType.LinkUp, adapter: 0);
|
||||
|
||||
received.Should().Be(EthernetEventType.LinkUp);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ICrestronConsole — CapturingCrestronConsole
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void CapturingConsole_PrintLine_CapturesMessage()
|
||||
{
|
||||
var console = new CapturingCrestronConsole();
|
||||
|
||||
console.PrintLine("hello world");
|
||||
|
||||
console.Lines.Should().ContainSingle().Which.Should().Be("hello world");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CapturingConsole_AddNewConsoleCommand_RecordsCommandName()
|
||||
{
|
||||
var console = new CapturingCrestronConsole();
|
||||
|
||||
console.AddNewConsoleCommand(_ => { }, "appdebug", "Sets debug level", ConsoleAccessLevel.AccessOperator);
|
||||
|
||||
console.RegisteredCommands.Should().ContainSingle()
|
||||
.Which.Command.Should().Be("appdebug");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Reference the Abstractions project only — no Crestron SDK dependency -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
|
||||
<!-- PepperDash.Core is referenced so we can test Device and other concrete types.
|
||||
DebugServiceRegistration.Register() is called via a [ModuleInitializer] before any
|
||||
test runs, so Debug's static constructor uses fakes and never touches the Crestron SDK. -->
|
||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using PepperDash.Core.Abstractions;
|
||||
using PepperDash.Core.Tests.Fakes;
|
||||
|
||||
namespace PepperDash.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Runs once before any type in this assembly is accessed.
|
||||
/// Registers fake Crestron service implementations with <see cref="DebugServiceRegistration"/>
|
||||
/// so that the <c>Debug</c> static constructor uses them instead of the real Crestron SDK.
|
||||
/// This must remain a module initializer (not a test fixture) because the static constructor
|
||||
/// fires the first time <em>any</em> type in PepperDash.Core is referenced — before xUnit
|
||||
/// has a chance to run fixture setup code.
|
||||
/// </summary>
|
||||
internal static class TestInitializer
|
||||
{
|
||||
[ModuleInitializer]
|
||||
internal static void Initialize()
|
||||
{
|
||||
DebugServiceRegistration.Register(
|
||||
new FakeCrestronEnvironment
|
||||
{
|
||||
DevicePlatform = DevicePlatform.Server, // avoids any appliance-only code paths
|
||||
RuntimeEnvironment = RuntimeEnvironment.Other, // skips console command registration
|
||||
},
|
||||
new NoOpCrestronConsole(),
|
||||
new InMemoryCrestronDataStore());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
using System;
|
||||
using Crestron.SimplSharp;
|
||||
using PdCore = PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production adapter — delegates ICrestronConsole calls to the real Crestron SDK.
|
||||
/// </summary>
|
||||
public sealed class CrestronConsoleAdapter : PdCore.ICrestronConsole
|
||||
{
|
||||
public void PrintLine(string message) => CrestronConsole.PrintLine(message);
|
||||
|
||||
public void Print(string message) => CrestronConsole.Print(message);
|
||||
|
||||
public void ConsoleCommandResponse(string message) =>
|
||||
CrestronConsole.ConsoleCommandResponse(message);
|
||||
|
||||
public void AddNewConsoleCommand(
|
||||
Action<string> callback,
|
||||
string command,
|
||||
string helpText,
|
||||
PdCore.ConsoleAccessLevel accessLevel)
|
||||
{
|
||||
var crestronLevel = accessLevel switch
|
||||
{
|
||||
PdCore.ConsoleAccessLevel.AccessAdministrator => ConsoleAccessLevelEnum.AccessAdministrator,
|
||||
PdCore.ConsoleAccessLevel.AccessProgrammer => ConsoleAccessLevelEnum.AccessProgrammer,
|
||||
_ => ConsoleAccessLevelEnum.AccessOperator,
|
||||
};
|
||||
|
||||
// Wrap Action<string> in a lambda — Crestron's delegate is not a standard Action<string>.
|
||||
CrestronConsole.AddNewConsoleCommand(s => callback(s), command, helpText, crestronLevel);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
using Crestron.SimplSharp.CrestronDataStore;
|
||||
using PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production adapter — delegates ICrestronDataStore calls to the real Crestron SDK.
|
||||
/// </summary>
|
||||
public sealed class CrestronDataStoreAdapter : ICrestronDataStore
|
||||
{
|
||||
public void InitStore() => CrestronDataStoreStatic.InitCrestronDataStore();
|
||||
|
||||
public bool TryGetLocalInt(string key, out int value)
|
||||
{
|
||||
var err = CrestronDataStoreStatic.GetLocalIntValue(key, out value);
|
||||
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
|
||||
public bool SetLocalInt(string key, int value)
|
||||
{
|
||||
var err = CrestronDataStoreStatic.SetLocalIntValue(key, value);
|
||||
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
|
||||
public bool SetLocalUint(string key, uint value)
|
||||
{
|
||||
var err = CrestronDataStoreStatic.SetLocalUintValue(key, value);
|
||||
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
|
||||
public bool TryGetLocalBool(string key, out bool value)
|
||||
{
|
||||
var err = CrestronDataStoreStatic.GetLocalBoolValue(key, out value);
|
||||
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
|
||||
public bool SetLocalBool(string key, bool value)
|
||||
{
|
||||
var err = CrestronDataStoreStatic.SetLocalBoolValue(key, value);
|
||||
return err == CrestronDataStore.CDS_ERROR.CDS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
using System;
|
||||
using Crestron.SimplSharp;
|
||||
using PdCore = PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production adapter — delegates ICrestronEnvironment calls to the real Crestron SDK.
|
||||
/// </summary>
|
||||
public sealed class CrestronEnvironmentAdapter : PdCore.ICrestronEnvironment
|
||||
{
|
||||
// Subscribe once in constructor and re-raise as our event types.
|
||||
private event EventHandler<PdCore.ProgramStatusEventArgs>? _programStatusChanged;
|
||||
private event EventHandler<PdCore.PepperDashEthernetEventArgs>? _ethernetEventReceived;
|
||||
|
||||
public CrestronEnvironmentAdapter()
|
||||
{
|
||||
CrestronEnvironment.ProgramStatusEventHandler += type =>
|
||||
_programStatusChanged?.Invoke(this, new PdCore.ProgramStatusEventArgs(MapProgramStatus(type)));
|
||||
|
||||
CrestronEnvironment.EthernetEventHandler += args =>
|
||||
_ethernetEventReceived?.Invoke(this, new PdCore.PepperDashEthernetEventArgs(
|
||||
args.EthernetEventType == eEthernetEventType.LinkDown
|
||||
? PdCore.EthernetEventType.LinkDown
|
||||
: PdCore.EthernetEventType.LinkUp,
|
||||
(short)args.EthernetAdapter));
|
||||
}
|
||||
|
||||
public PdCore.DevicePlatform DevicePlatform =>
|
||||
CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
|
||||
? PdCore.DevicePlatform.Appliance
|
||||
: PdCore.DevicePlatform.Server;
|
||||
|
||||
public PdCore.RuntimeEnvironment RuntimeEnvironment =>
|
||||
CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro
|
||||
? PdCore.RuntimeEnvironment.SimplSharpPro
|
||||
: PdCore.RuntimeEnvironment.Other;
|
||||
|
||||
public string NewLine => CrestronEnvironment.NewLine;
|
||||
|
||||
public uint ApplicationNumber => InitialParametersClass.ApplicationNumber;
|
||||
|
||||
public uint RoomId => uint.TryParse(InitialParametersClass.RoomId, out var r) ? r : 0;
|
||||
|
||||
public event EventHandler<PdCore.ProgramStatusEventArgs> ProgramStatusChanged
|
||||
{
|
||||
add => _programStatusChanged += value;
|
||||
remove => _programStatusChanged -= value;
|
||||
}
|
||||
|
||||
public event EventHandler<PdCore.PepperDashEthernetEventArgs> EthernetEventReceived
|
||||
{
|
||||
add => _ethernetEventReceived += value;
|
||||
remove => _ethernetEventReceived -= value;
|
||||
}
|
||||
|
||||
public string GetApplicationRootDirectory() =>
|
||||
Crestron.SimplSharp.CrestronIO.Directory.GetApplicationRootDirectory();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsHardwareRuntime => true;
|
||||
|
||||
private static PdCore.ProgramStatusEventType MapProgramStatus(eProgramStatusEventType type) =>
|
||||
type switch
|
||||
{
|
||||
eProgramStatusEventType.Stopping => PdCore.ProgramStatusEventType.Stopping,
|
||||
eProgramStatusEventType.Paused => PdCore.ProgramStatusEventType.Paused,
|
||||
eProgramStatusEventType.Resumed => PdCore.ProgramStatusEventType.Resumed,
|
||||
_ => PdCore.ProgramStatusEventType.Starting,
|
||||
};
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
using System;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core.Abstractions;
|
||||
|
||||
namespace PepperDash.Core.Adapters;
|
||||
|
||||
/// <summary>
|
||||
/// Production adapter — delegates IEthernetHelper calls to the real Crestron SDK.
|
||||
/// </summary>
|
||||
public sealed class CrestronEthernetAdapter : IEthernetHelper
|
||||
{
|
||||
public string GetEthernetParameter(EthernetParameterType parameter, short ethernetAdapterId)
|
||||
{
|
||||
var crestronParam = parameter switch
|
||||
{
|
||||
EthernetParameterType.GetCurrentIpAddress =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS,
|
||||
EthernetParameterType.GetHostname =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME,
|
||||
EthernetParameterType.GetDomainName =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME,
|
||||
EthernetParameterType.GetLinkStatus =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS,
|
||||
EthernetParameterType.GetCurrentDhcpState =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE,
|
||||
EthernetParameterType.GetCurrentIpMask =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK,
|
||||
EthernetParameterType.GetCurrentRouter =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER,
|
||||
EthernetParameterType.GetMacAddress =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS,
|
||||
EthernetParameterType.GetDnsServer =>
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(parameter), parameter, null),
|
||||
};
|
||||
|
||||
return CrestronEthernetHelper.GetEthernetParameter(crestronParam, ethernetAdapterId);
|
||||
}
|
||||
}
|
||||
43
src/PepperDash.Core/ComTextHelper.cs
Normal file
43
src/PepperDash.Core/ComTextHelper.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for formatting communication text and byte data for debugging purposes.
|
||||
/// </summary>
|
||||
public class ComTextHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets escaped text for a byte array
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns>string with all bytes escaped</returns>
|
||||
public static string GetEscapedText(byte[] bytes)
|
||||
{
|
||||
return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets escaped text for a string
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns>string with all bytes escaped</returns>
|
||||
public static string GetEscapedText(string text)
|
||||
{
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets debug text for a string
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns>string with all non-printable characters escaped</returns>
|
||||
public static string GetDebugText(string text)
|
||||
{
|
||||
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,8 @@ using Crestron.SimplSharp;
|
|||
using PepperDash.Core;
|
||||
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the string event handler for line events on the gather
|
||||
/// </summary>
|
||||
|
|
@ -175,3 +175,4 @@ namespace PepperDash.Core;
|
|||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable
|
||||
/// </summary>
|
||||
public class CommunicationStreamDebugging
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable
|
||||
/// </summary>
|
||||
public class CommunicationStreamDebugging
|
||||
{
|
||||
/// <summary>
|
||||
/// Device Key that this instance configures
|
||||
/// </summary>
|
||||
|
|
@ -20,10 +19,10 @@ public class CommunicationStreamDebugging
|
|||
/// <summary>
|
||||
/// Timer to disable automatically if not manually disabled
|
||||
/// </summary>
|
||||
private Timer DebugExpiryPeriod;
|
||||
private CTimer DebugExpiryPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// The current debug setting
|
||||
/// Gets or sets the DebugSetting
|
||||
/// </summary>
|
||||
public eStreamDebuggingSetting DebugSetting { get; private set; }
|
||||
|
||||
|
|
@ -37,14 +36,14 @@ public class CommunicationStreamDebugging
|
|||
{
|
||||
get
|
||||
{
|
||||
return _DebugTimeoutInMs/60000;
|
||||
return _DebugTimeoutInMs / 60000;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that receive stream debugging is enabled
|
||||
/// Gets or sets the RxStreamDebuggingIsEnabled
|
||||
/// </summary>
|
||||
public bool RxStreamDebuggingIsEnabled{ get; private set; }
|
||||
public bool RxStreamDebuggingIsEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that transmit stream debugging is enabled
|
||||
|
|
@ -65,6 +64,9 @@ public class CommunicationStreamDebugging
|
|||
/// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues
|
||||
/// </summary>
|
||||
/// <param name="setting"></param>
|
||||
/// <summary>
|
||||
/// SetDebuggingWithDefaultTimeout method
|
||||
/// </summary>
|
||||
public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting)
|
||||
{
|
||||
if (setting == eStreamDebuggingSetting.Off)
|
||||
|
|
@ -81,6 +83,9 @@ public class CommunicationStreamDebugging
|
|||
/// </summary>
|
||||
/// <param name="setting"></param>
|
||||
/// <param name="minutes"></param>
|
||||
/// <summary>
|
||||
/// SetDebuggingWithSpecificTimeout method
|
||||
/// </summary>
|
||||
public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes)
|
||||
{
|
||||
if (setting == eStreamDebuggingSetting.Off)
|
||||
|
|
@ -93,9 +98,7 @@ public class CommunicationStreamDebugging
|
|||
|
||||
StopDebugTimer();
|
||||
|
||||
DebugExpiryPeriod = new Timer(_DebugTimeoutInMs) { AutoReset = false };
|
||||
DebugExpiryPeriod.Elapsed += (s, e) => DisableDebugging();
|
||||
DebugExpiryPeriod.Start();
|
||||
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs);
|
||||
|
||||
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
|
||||
RxStreamDebuggingIsEnabled = true;
|
||||
|
|
@ -131,29 +134,5 @@ public class CommunicationStreamDebugging
|
|||
DebugExpiryPeriod.Dispose();
|
||||
DebugExpiryPeriod = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The available settings for stream debugging
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum eStreamDebuggingSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug off
|
||||
/// </summary>
|
||||
Off = 0,
|
||||
/// <summary>
|
||||
/// Debug received data
|
||||
/// </summary>
|
||||
Rx = 1,
|
||||
/// <summary>
|
||||
/// Debug transmitted data
|
||||
/// </summary>
|
||||
Tx = 2,
|
||||
/// <summary>
|
||||
/// Debug both received and transmitted data
|
||||
/// </summary>
|
||||
Both = Rx | Tx
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,15 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using Crestron.SimplSharp;
|
||||
using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute;
|
||||
using JsonIgnore = NewtonsoftJson::Newtonsoft.Json.JsonIgnoreAttribute;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using NullValueHandling = NewtonsoftJson::Newtonsoft.Json.NullValueHandling;
|
||||
using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Config properties that indicate how to communicate with a device for control
|
||||
/// </summary>
|
||||
public class ControlPropertiesConfig
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a ControlPropertiesConfig
|
||||
/// </summary>
|
||||
public class ControlPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The method of control
|
||||
/// </summary>
|
||||
|
|
@ -94,4 +89,5 @@ public class ControlPropertiesConfig
|
|||
public ControlPropertiesConfig()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,26 +16,26 @@ using Crestron.SimplSharp;
|
|||
using Crestron.SimplSharp.CrestronSockets;
|
||||
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for notifying of socket status changes
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs class for socket status changes
|
||||
/// </summary>
|
||||
public class GenericSocketStatusChageEventArgs : EventArgs
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for notifying of socket status changes
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs class for socket status changes
|
||||
/// </summary>
|
||||
public class GenericSocketStatusChageEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Client
|
||||
/// </summary>
|
||||
public ISocketStatus Client { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
public GenericSocketStatusChageEventArgs(ISocketStatus client)
|
||||
|
|
@ -46,26 +46,26 @@ public class GenericSocketStatusChageEventArgs : EventArgs
|
|||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericSocketStatusChageEventArgs() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for notifying of TCP Server state changes
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
|
||||
/// <summary>
|
||||
/// Delegate for notifying of TCP Server state changes
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs class for TCP Server state changes
|
||||
/// </summary>
|
||||
public class GenericTcpServerStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// EventArgs class for TCP Server state changes
|
||||
/// </summary>
|
||||
public class GenericTcpServerStateChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the State
|
||||
/// </summary>
|
||||
public ServerState State { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
public GenericTcpServerStateChangedEventArgs(ServerState state)
|
||||
|
|
@ -76,37 +76,35 @@ public class GenericTcpServerStateChangedEventArgs : EventArgs
|
|||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerStateChangedEventArgs() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for TCP Server socket status changes
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <param name="clientStatus"></param>
|
||||
public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
|
||||
/// <summary>
|
||||
/// EventArgs for TCP server socket status changes
|
||||
/// </summary>
|
||||
public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Socket
|
||||
/// Delegate for TCP Server socket status changes
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <param name="clientStatus"></param>
|
||||
public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
|
||||
/// <summary>
|
||||
/// EventArgs for TCP server socket status changes
|
||||
/// </summary>
|
||||
public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public object Socket { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the client from which the status change was received
|
||||
///
|
||||
/// </summary>
|
||||
public uint ReceivedFromClientIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ClientStatus
|
||||
///
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="clientStatus"></param>
|
||||
|
|
@ -117,7 +115,7 @@ public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="socket"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
|
|
@ -132,20 +130,20 @@ public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
|
|||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerSocketStatusChangeEventArgs() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs for TCP server com method receive text
|
||||
/// </summary>
|
||||
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the client from which the text was received
|
||||
/// EventArgs for TCP server com method receive text
|
||||
/// </summary>
|
||||
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public uint ReceivedFromClientIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the index of the client from which the text was received as a ushort
|
||||
///
|
||||
/// </summary>
|
||||
public ushort ReceivedFromClientIndexShort
|
||||
{
|
||||
|
|
@ -161,7 +159,7 @@ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
|||
public string Text { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public GenericTcpServerCommMethodReceiveTextArgs(string text)
|
||||
|
|
@ -170,7 +168,7 @@ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
|
|
@ -183,45 +181,43 @@ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
|
|||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerCommMethodReceiveTextArgs() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs for TCP server client ready for communication
|
||||
/// </summary>
|
||||
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets IsReady
|
||||
/// EventArgs for TCP server client ready for communication
|
||||
/// </summary>
|
||||
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public bool IsReady;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="isReady"></param>
|
||||
public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
|
||||
{
|
||||
IsReady = isReady;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S+ Constructor
|
||||
/// </summary>
|
||||
public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EventArgs for UDP connected
|
||||
/// </summary>
|
||||
public class GenericUdpConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the UConnected
|
||||
/// EventArgs for UDP connected
|
||||
/// </summary>
|
||||
public class GenericUdpConnectedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public ushort UConnected;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Connected status
|
||||
///
|
||||
/// </summary>
|
||||
public bool Connected;
|
||||
|
||||
|
|
@ -231,7 +227,7 @@ public class GenericUdpConnectedEventArgs : EventArgs
|
|||
public GenericUdpConnectedEventArgs() { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="uconnected"></param>
|
||||
public GenericUdpConnectedEventArgs(ushort uconnected)
|
||||
|
|
@ -240,7 +236,7 @@ public class GenericUdpConnectedEventArgs : EventArgs
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="connected"></param>
|
||||
public GenericUdpConnectedEventArgs(bool connected)
|
||||
|
|
@ -248,5 +244,8 @@ public class GenericUdpConnectedEventArgs : EventArgs
|
|||
Connected = connected;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -3,18 +3,17 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A class to handle secure TCP/IP communications with a server
|
||||
/// </summary>
|
||||
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A class to handle secure TCP/IP communications with a server
|
||||
/// </summary>
|
||||
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
{
|
||||
private const string SplusKey = "Uninitialized Secure Tcp _client";
|
||||
/// <summary>
|
||||
/// Stream debugging
|
||||
|
|
@ -80,7 +79,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port on server
|
||||
/// Gets or sets the Port
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
|
|
@ -150,7 +149,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
|
||||
|
||||
/// <summary>
|
||||
/// bool to track if auto reconnect should be set on the socket
|
||||
/// Gets or sets the AutoReconnect
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
|
|
@ -182,14 +181,14 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
// private Timer for auto reconnect
|
||||
private Timer RetryTimer;
|
||||
private CTimer RetryTimer;
|
||||
|
||||
#endregion
|
||||
|
||||
#region GenericSecureTcpIpClient properties
|
||||
|
||||
/// <summary>
|
||||
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
|
||||
/// Gets or sets the SharedKeyRequired
|
||||
/// </summary>
|
||||
public bool SharedKeyRequired { get; set; }
|
||||
|
||||
|
|
@ -208,7 +207,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
|
||||
/// Gets or sets the SharedKey
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
|
|
@ -223,7 +222,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
bool IsTryingToConnect;
|
||||
|
||||
/// <summary>
|
||||
/// Bool showing if socket is ready for communication after shared key exchange
|
||||
/// Gets or sets the IsReadyForCommunication
|
||||
/// </summary>
|
||||
public bool IsReadyForCommunication { get; set; }
|
||||
|
||||
|
|
@ -265,12 +264,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
/// </summary>
|
||||
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
|
||||
|
||||
Timer HeartbeatSendTimer;
|
||||
Timer HeartbeatAckTimer;
|
||||
CTimer HeartbeatSendTimer;
|
||||
CTimer HeartbeatAckTimer;
|
||||
|
||||
// Used to force disconnection on a dead connect attempt
|
||||
Timer ConnectFailTimer;
|
||||
Timer WaitForSharedKey;
|
||||
CTimer ConnectFailTimer;
|
||||
CTimer WaitForSharedKey;
|
||||
private int ConnectionCount;
|
||||
|
||||
bool ProgramIsStopping;
|
||||
|
|
@ -278,7 +277,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
/// <summary>
|
||||
/// Queue lock
|
||||
/// </summary>
|
||||
private readonly object _dequeueLock = new();
|
||||
CCriticalSection DequeueLock = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
|
||||
|
|
@ -343,7 +342,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Just to help S+ set the key
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
|
|
@ -358,7 +357,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
{
|
||||
if (config == null)
|
||||
{
|
||||
this.LogWarning( "Could not initialize client with key: {0}", Key);
|
||||
Debug.Console(0, this, "Could not initialize client with key: {0}", Key);
|
||||
return;
|
||||
}
|
||||
try
|
||||
|
|
@ -398,7 +397,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Exception initializing client with key: {0}\rException: {1}", Key, ex);
|
||||
Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -411,7 +410,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing _client connection");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection");
|
||||
ProgramIsStopping = true;
|
||||
Disconnect();
|
||||
}
|
||||
|
|
@ -422,6 +421,9 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
/// Deactivate the client
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// Deactivate method
|
||||
/// </summary>
|
||||
public override bool Deactivate()
|
||||
{
|
||||
if (_client != null)
|
||||
|
|
@ -433,22 +435,22 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
ConnectionCount++;
|
||||
this.LogVerbose("Attempting connect Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
|
||||
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Already connected. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
|
||||
return;
|
||||
}
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
this.LogInformation("Already trying to connect. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
|
|
@ -461,17 +463,17 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No address set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: Invalid port");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No Shared Key set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -492,10 +494,9 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
|
||||
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
ConnectFailTimer = new Timer(30000) { AutoReset = false };
|
||||
ConnectFailTimer.Elapsed += (s, e) =>
|
||||
ConnectFailTimer = new CTimer(o =>
|
||||
{
|
||||
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
IsTryingToConnect = false;
|
||||
|
|
@ -507,13 +508,12 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
//SecureClient.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
}
|
||||
};
|
||||
ConnectFailTimer.Start();
|
||||
}, 30000);
|
||||
|
||||
this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
|
||||
_client.ConnectToServerAsync(o =>
|
||||
{
|
||||
this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
|
||||
if (ConnectFailTimer != null)
|
||||
{
|
||||
|
|
@ -523,22 +523,22 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
|
||||
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogVerbose("_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
o.ReceiveDataAsync(Receive);
|
||||
|
||||
if (SharedKeyRequired)
|
||||
{
|
||||
WaitingForSharedKeyResponse = true;
|
||||
WaitForSharedKey = new Timer(15000) { AutoReset = false };
|
||||
WaitForSharedKey.Elapsed += (s, e) =>
|
||||
WaitForSharedKey = new CTimer(timer =>
|
||||
{
|
||||
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
|
||||
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
|
||||
o.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
//OnClientReadyForcommunications(false); // Should send false event
|
||||
};
|
||||
WaitForSharedKey.Start();
|
||||
}, 15000);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -552,21 +552,21 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
else
|
||||
{
|
||||
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
|
||||
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("_client connection exception: {0}", ex.Message);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message);
|
||||
IsTryingToConnect = false;
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
|
|
@ -589,20 +589,20 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the actual disconnect business
|
||||
/// DisconnectClient method
|
||||
/// </summary>
|
||||
public void DisconnectClient()
|
||||
{
|
||||
if (_client == null) return;
|
||||
|
||||
this.LogInformation("Disconnecting client");
|
||||
Debug.Console(1, this, "Disconnecting client");
|
||||
if (IsConnected)
|
||||
_client.DisconnectFromServer();
|
||||
|
||||
// close up client. ALWAYS use this when disconnecting.
|
||||
IsTryingToConnect = false;
|
||||
|
||||
this.LogVerbose("Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
_client.SocketStatusChange -= Client_SocketStatusChange;
|
||||
_client.Dispose();
|
||||
_client = null;
|
||||
|
|
@ -623,14 +623,14 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
{
|
||||
if (_client != null)
|
||||
{
|
||||
this.LogVerbose("Cleaning up remotely closed/failed connection.");
|
||||
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
|
||||
Disconnect();
|
||||
}
|
||||
if (!DisconnectCalledByUser && AutoReconnect)
|
||||
{
|
||||
var halfInterval = AutoReconnectIntervalMs / 2;
|
||||
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
|
||||
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
if (RetryTimer != null)
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
|
|
@ -638,9 +638,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
if (AutoReconnectTriggered != null)
|
||||
AutoReconnectTriggered(this, new EventArgs());
|
||||
RetryTimer = new Timer(rndTime) { AutoReset = false };
|
||||
RetryTimer.Elapsed += (s, e) => Connect();
|
||||
RetryTimer.Start();
|
||||
RetryTimer = new CTimer(o => Connect(), rndTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -659,13 +657,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
{
|
||||
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
|
||||
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.LogVerbose("_client Received:\r--------\r{0}\r--------", str);
|
||||
Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str);
|
||||
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
|
||||
{
|
||||
|
||||
if (SharedKeyRequired && str == "SharedKey:")
|
||||
{
|
||||
this.LogVerbose("Server asking for shared key, sending");
|
||||
Debug.Console(2, this, "Server asking for shared key, sending");
|
||||
SendText(SharedKey + "\n");
|
||||
}
|
||||
else if (SharedKeyRequired && str == "Shared Key Match")
|
||||
|
|
@ -673,7 +671,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
StopWaitForSharedKeyTimer();
|
||||
|
||||
|
||||
this.LogVerbose("Shared key confirmed. Ready for communication");
|
||||
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
|
||||
OnClientReadyForcommunications(true); // Successful key exchange
|
||||
}
|
||||
else
|
||||
|
|
@ -693,7 +691,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error receiving data: {1}. Error: {0}", str, ex.Message);
|
||||
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
}
|
||||
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
client.ReceiveDataAsync(Receive);
|
||||
|
|
@ -701,8 +699,9 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
|
||||
if (handler != null)
|
||||
{
|
||||
if (System.Threading.Monitor.TryEnter(_dequeueLock))
|
||||
System.Threading.Tasks.Task.Run(() => DequeueEvent());
|
||||
var gotLock = DequeueLock.TryEnter();
|
||||
if (gotLock)
|
||||
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
|
||||
}
|
||||
}
|
||||
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
|
||||
|
|
@ -712,7 +711,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
|
||||
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
|
||||
/// It will dequeue items as they are enqueued automatically.
|
||||
/// </summary>
|
||||
void DequeueEvent()
|
||||
|
|
@ -732,10 +731,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogError(e, "DequeueEvent error: {0}", e.Message);
|
||||
this.LogException(e, "DequeueEvent error");
|
||||
}
|
||||
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
|
||||
if (DequeueLock != null)
|
||||
{
|
||||
DequeueLock.Leave();
|
||||
}
|
||||
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
|
||||
System.Threading.Monitor.Exit(_dequeueLock);
|
||||
}
|
||||
|
||||
void HeartbeatStart()
|
||||
|
|
@ -746,15 +748,11 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
if (HeartbeatSendTimer == null)
|
||||
{
|
||||
|
||||
HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
|
||||
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
|
||||
HeartbeatSendTimer.Start();
|
||||
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
|
||||
}
|
||||
if (HeartbeatAckTimer == null)
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -764,13 +762,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
|
||||
if (HeartbeatSendTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Send");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Send");
|
||||
HeartbeatSendTimer.Stop();
|
||||
HeartbeatSendTimer = null;
|
||||
}
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Ack");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Ack");
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer = null;
|
||||
}
|
||||
|
|
@ -779,7 +777,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
void SendHeartbeat(object notused)
|
||||
{
|
||||
this.SendText(HeartbeatString);
|
||||
this.LogVerbose("Sending Heartbeat");
|
||||
Debug.Console(2, this, "Sending Heartbeat");
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -798,17 +796,13 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
{
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
return remainingText;
|
||||
}
|
||||
}
|
||||
|
|
@ -816,7 +810,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
|
@ -830,7 +824,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
SendText("Heartbeat not received by server, closing connection");
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
|
|
@ -838,7 +832,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Heartbeat timeout Error on _client: {0}, {1}", Key, ex.Message);
|
||||
ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -855,7 +849,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// General send method
|
||||
/// SendText method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
|
|
@ -871,20 +865,20 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
|
||||
if (n <= 0)
|
||||
{
|
||||
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// SendBytes method
|
||||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
|
|
@ -897,7 +891,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error sending bytes. Error: {0}", ex.Message);
|
||||
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -916,7 +910,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
try
|
||||
{
|
||||
this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
|
||||
OnConnectionChange();
|
||||
// The client could be null or disposed by this time...
|
||||
|
|
@ -929,7 +923,7 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -959,4 +953,6 @@ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging
|
|||
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,20 +15,17 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generic secure TCP/IP client for server
|
||||
/// </summary>
|
||||
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic secure TCP/IP client for server
|
||||
/// </summary>
|
||||
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
/// <summary>
|
||||
/// Band aid delegate for choked server
|
||||
/// </summary>
|
||||
|
|
@ -83,7 +80,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
public string Hostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The port number on which the server is listening.
|
||||
/// Gets or sets the Port
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
|
|
@ -116,7 +113,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharedKey is sent for verification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
|
||||
/// Gets or sets the SharedKey
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
|
|
@ -126,7 +123,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
private bool WaitingForSharedKeyResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to 2000
|
||||
/// Gets or sets the BufferSize
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
|
|
@ -224,7 +221,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
/// <summary>
|
||||
/// private Timer for auto reconnect
|
||||
/// </summary>
|
||||
System.Timers.Timer RetryTimer;
|
||||
CTimer RetryTimer;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -256,13 +253,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
/// </summary>
|
||||
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
|
||||
|
||||
System.Timers.Timer HeartbeatSendTimer;
|
||||
System.Timers.Timer HeartbeatAckTimer;
|
||||
CTimer HeartbeatSendTimer;
|
||||
CTimer HeartbeatAckTimer;
|
||||
/// <summary>
|
||||
/// Used to force disconnection on a dead connect attempt
|
||||
/// </summary>
|
||||
System.Timers.Timer ConnectFailTimer;
|
||||
System.Timers.Timer WaitForSharedKey;
|
||||
CTimer ConnectFailTimer;
|
||||
CTimer WaitForSharedKey;
|
||||
private int ConnectionCount;
|
||||
/// <summary>
|
||||
/// Internal secure client
|
||||
|
|
@ -274,7 +271,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
/// <summary>
|
||||
/// Queue lock
|
||||
/// </summary>
|
||||
private readonly object _dequeueLock = new();
|
||||
CCriticalSection DequeueLock = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
|
||||
|
|
@ -339,9 +336,8 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the client's key property, which is used to identify this client instance.
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key that identifies this client instance.</param>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -350,7 +346,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
/// <summary>
|
||||
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
|
||||
/// </summary>
|
||||
/// <param name="clientConfigObject">The configuration object containing the client's settings.</param>
|
||||
/// <param name="clientConfigObject"></param>
|
||||
public void Initialize(TcpClientConfigObject clientConfigObject)
|
||||
{
|
||||
try
|
||||
|
|
@ -391,7 +387,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing Client connection");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
|
||||
ProgramIsStopping = true;
|
||||
Disconnect();
|
||||
}
|
||||
|
|
@ -399,22 +395,22 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
ConnectionCount++;
|
||||
this.LogVerbose("Attempting connect Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
|
||||
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Already connected. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
|
||||
return;
|
||||
}
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
this.LogInformation("Already trying to connect. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
|
|
@ -427,17 +423,17 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No address set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: Invalid port");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No Shared Key set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -458,10 +454,9 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
ConnectFailTimer = new System.Timers.Timer(30000) { AutoReset = false };
|
||||
ConnectFailTimer.Elapsed += (s, e) =>
|
||||
ConnectFailTimer = new CTimer(o =>
|
||||
{
|
||||
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
IsTryingToConnect = false;
|
||||
|
|
@ -473,13 +468,12 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
//SecureClient.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
}
|
||||
};
|
||||
ConnectFailTimer.Start();
|
||||
}, 30000);
|
||||
|
||||
this.LogVerbose("Making Connection Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
|
||||
Client.ConnectToServerAsync(o =>
|
||||
{
|
||||
this.LogVerbose("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
|
||||
if (ConnectFailTimer != null)
|
||||
{
|
||||
|
|
@ -489,22 +483,22 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
o.ReceiveDataAsync(Receive);
|
||||
|
||||
if (SharedKeyRequired)
|
||||
{
|
||||
WaitingForSharedKeyResponse = true;
|
||||
WaitForSharedKey = new System.Timers.Timer(15000) { AutoReset = false };
|
||||
WaitForSharedKey.Elapsed += (s, e) =>
|
||||
WaitForSharedKey = new CTimer(timer =>
|
||||
{
|
||||
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
|
||||
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
|
||||
o.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
//OnClientReadyForcommunications(false); // Should send false event
|
||||
};
|
||||
WaitForSharedKey.Start();
|
||||
}, 15000);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -518,21 +512,21 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
else
|
||||
{
|
||||
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
|
||||
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Client connection exception: {0}", ex.Message);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
|
||||
IsTryingToConnect = false;
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
|
|
@ -562,7 +556,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
if (Client != null)
|
||||
{
|
||||
//SecureClient.DisconnectFromServer();
|
||||
this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Client.SocketStatusChange -= Client_SocketStatusChange;
|
||||
Client.Dispose();
|
||||
Client = null;
|
||||
|
|
@ -584,24 +578,22 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (Client != null)
|
||||
{
|
||||
this.LogVerbose("Cleaning up remotely closed/failed connection.");
|
||||
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
|
||||
Cleanup();
|
||||
}
|
||||
if (!DisconnectCalledByUser && AutoReconnect)
|
||||
{
|
||||
var halfInterval = AutoReconnectIntervalMs / 2;
|
||||
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
|
||||
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
if (RetryTimer != null)
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
RetryTimer = null;
|
||||
}
|
||||
if (AutoReconnectTriggered != null)
|
||||
if(AutoReconnectTriggered != null)
|
||||
AutoReconnectTriggered(this, new EventArgs());
|
||||
RetryTimer = new System.Timers.Timer(rndTime) { AutoReset = false };
|
||||
RetryTimer.Elapsed += (s, e) => Connect();
|
||||
RetryTimer.Start();
|
||||
RetryTimer = new CTimer(o => Connect(), rndTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -620,13 +612,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
|
||||
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
|
||||
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
|
||||
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
|
||||
{
|
||||
|
||||
if (SharedKeyRequired && str == "SharedKey:")
|
||||
{
|
||||
this.LogVerbose("Server asking for shared key, sending");
|
||||
Debug.Console(2, this, "Server asking for shared key, sending");
|
||||
SendText(SharedKey + "\n");
|
||||
}
|
||||
else if (SharedKeyRequired && str == "Shared Key Match")
|
||||
|
|
@ -634,7 +626,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
StopWaitForSharedKeyTimer();
|
||||
|
||||
|
||||
this.LogVerbose("Shared key confirmed. Ready for communication");
|
||||
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
|
||||
OnClientReadyForcommunications(true); // Successful key exchange
|
||||
}
|
||||
else
|
||||
|
|
@ -654,7 +646,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
}
|
||||
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
client.ReceiveDataAsync(Receive);
|
||||
|
|
@ -662,8 +654,9 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
|
||||
if (handler != null)
|
||||
{
|
||||
if (Monitor.TryEnter(_dequeueLock))
|
||||
Task.Run(() => DequeueEvent());
|
||||
var gotLock = DequeueLock.TryEnter();
|
||||
if (gotLock)
|
||||
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
|
||||
}
|
||||
}
|
||||
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
|
||||
|
|
@ -673,7 +666,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
|
||||
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
|
||||
/// It will dequeue items as they are enqueued automatically.
|
||||
/// </summary>
|
||||
void DequeueEvent()
|
||||
|
|
@ -693,29 +686,28 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogError("DequeueEvent error: {0}", e.Message, e);
|
||||
this.LogException(e, "DequeueEvent error");
|
||||
}
|
||||
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
|
||||
if (DequeueLock != null)
|
||||
{
|
||||
DequeueLock.Leave();
|
||||
}
|
||||
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
|
||||
Monitor.Exit(_dequeueLock);
|
||||
}
|
||||
|
||||
void HeartbeatStart()
|
||||
{
|
||||
if (HeartbeatEnabled)
|
||||
{
|
||||
this.LogVerbose("Starting Heartbeat");
|
||||
Debug.Console(2, this, "Starting Heartbeat");
|
||||
if (HeartbeatSendTimer == null)
|
||||
{
|
||||
|
||||
HeartbeatSendTimer = new System.Timers.Timer(HeartbeatInterval) { AutoReset = true };
|
||||
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
|
||||
HeartbeatSendTimer.Start();
|
||||
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
|
||||
}
|
||||
if (HeartbeatAckTimer == null)
|
||||
{
|
||||
HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -725,13 +717,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
if (HeartbeatSendTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stopping Heartbeat Send");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Send");
|
||||
HeartbeatSendTimer.Stop();
|
||||
HeartbeatSendTimer = null;
|
||||
}
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stopping Heartbeat Ack");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Ack");
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer = null;
|
||||
}
|
||||
|
|
@ -740,7 +732,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
void SendHeartbeat(object notused)
|
||||
{
|
||||
this.SendText(HeartbeatString);
|
||||
this.LogVerbose("Sending Heartbeat");
|
||||
Debug.Console(2, this, "Sending Heartbeat");
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -759,17 +751,13 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeartbeatAckTimer = new System.Timers.Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
return remainingText;
|
||||
}
|
||||
}
|
||||
|
|
@ -777,7 +765,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error checking heartbeat: {0}", ex.Message, ex);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
|
@ -791,7 +779,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
SendText("Heartbeat not received by server, closing connection");
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
|
|
@ -799,7 +787,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
|
||||
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -816,7 +804,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// General send method
|
||||
/// SendText method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
|
|
@ -832,20 +820,20 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
|
||||
if (n <= 0)
|
||||
{
|
||||
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error sending text: {1}. Error: {0}", text, ex.Message);
|
||||
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// SendBytes method
|
||||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
|
|
@ -858,7 +846,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error sending bytes. Error: {0}", ex.Message, ex);
|
||||
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -877,7 +865,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
try
|
||||
{
|
||||
this.LogDebug("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
|
||||
OnConnectionChange();
|
||||
// The client could be null or disposed by this time...
|
||||
|
|
@ -890,7 +878,7 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -916,4 +904,6 @@ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,19 +1,29 @@
|
|||
using System;
|
||||
/*PepperDash Technology Corp.
|
||||
JAG
|
||||
Copyright: 2017
|
||||
------------------------------------
|
||||
***Notice of Ownership and Copyright***
|
||||
The material in which this notice appears is the property of PepperDash Technology Corporation,
|
||||
which claims copyright under the laws of the United States of America in the entire body of material
|
||||
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
|
||||
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
|
||||
PepperDash Technology Corporation reserves all rights under applicable laws.
|
||||
------------------------------------ */
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generic secure TCP/IP server
|
||||
/// </summary>
|
||||
public class GenericSecureTcpIpServer : Device
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic secure TCP/IP server
|
||||
/// </summary>
|
||||
public class GenericSecureTcpIpServer : Device
|
||||
{
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// Event for Receiving text
|
||||
|
|
@ -48,7 +58,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Delegate for ServerHasChokedCallbackDelegate
|
||||
/// </summary>
|
||||
public delegate void ServerHasChokedCallbackDelegate();
|
||||
|
||||
|
|
@ -59,17 +69,12 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// <summary>
|
||||
/// Server listen lock
|
||||
/// </summary>
|
||||
private readonly object _serverLock = new();
|
||||
CCriticalSection ServerCCSection = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Queue lock
|
||||
/// </summary>
|
||||
private readonly object _dequeueLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast lock
|
||||
/// </summary>
|
||||
private readonly object _broadcastLock = new();
|
||||
CCriticalSection DequeueLock = new CCriticalSection();
|
||||
|
||||
/// <summary>
|
||||
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
|
||||
|
|
@ -91,7 +96,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// <summary>
|
||||
/// Timer to operate the bandaid monitor client in a loop.
|
||||
/// </summary>
|
||||
Timer MonitorClientTimer;
|
||||
CTimer MonitorClientTimer;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
|
@ -99,7 +104,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
int MonitorClientFailureCount;
|
||||
|
||||
/// <summary>
|
||||
/// 3 by default
|
||||
/// Gets or sets the MonitorClientMaxFailureCount
|
||||
/// </summary>
|
||||
public int MonitorClientMaxFailureCount { get; set; }
|
||||
|
||||
|
|
@ -185,7 +190,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port Server should listen on
|
||||
/// Gets or sets the Port
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
|
|
@ -218,8 +223,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module.
|
||||
/// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
|
||||
/// Gets or sets the SharedKey
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
|
|
@ -243,7 +247,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
|
||||
/// Gets or sets the HeartbeatRequiredIntervalMs
|
||||
/// </summary>
|
||||
public int HeartbeatRequiredIntervalMs { get; set; }
|
||||
|
||||
|
|
@ -253,12 +257,12 @@ public class GenericSecureTcpIpServer : Device
|
|||
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
|
||||
|
||||
/// <summary>
|
||||
/// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
|
||||
/// Gets or sets the HeartbeatStringToMatch
|
||||
/// </summary>
|
||||
public string HeartbeatStringToMatch { get; set; }
|
||||
|
||||
//private timers for Heartbeats per client
|
||||
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
|
||||
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
|
||||
|
||||
//flags to show the secure server is waiting for client at index to send the shared key
|
||||
List<uint> WaitingForSharedKey = new List<uint>();
|
||||
|
|
@ -271,7 +275,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
public List<uint> ConnectedClientsIndexes = new List<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to 2000
|
||||
/// Gets or sets the BufferSize
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
|
|
@ -334,7 +338,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
|
||||
#region Methods - Server Actions
|
||||
/// <summary>
|
||||
/// Disconnects all clients and stops the server
|
||||
/// KillServer method
|
||||
/// </summary>
|
||||
public void KillServer()
|
||||
{
|
||||
|
|
@ -351,6 +355,9 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -390,23 +397,22 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start listening on the specified port
|
||||
/// Listen method
|
||||
/// </summary>
|
||||
public void Listen()
|
||||
{
|
||||
lock (_serverLock)
|
||||
{
|
||||
ServerCCSection.Enter();
|
||||
try
|
||||
{
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogError("Server '{0}': Invalid port", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogError("Server '{0}': No Shared Key set", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
|
||||
return;
|
||||
}
|
||||
|
|
@ -430,42 +436,43 @@ public class GenericSecureTcpIpServer : Device
|
|||
SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
|
||||
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
{
|
||||
this.LogError("Error starting WaitForConnectionAsync {0}", status);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error starting WaitForConnectionAsync {0}", status);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerStopped = false;
|
||||
}
|
||||
OnServerStateChange(SecureServer.State);
|
||||
this.LogInformation("Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
|
||||
ServerCCSection.Leave();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
|
||||
ServerCCSection.Leave();
|
||||
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop Listeneing
|
||||
/// StopListening method
|
||||
/// </summary>
|
||||
public void StopListening()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogVerbose("Stopping Listener");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
|
||||
if (SecureServer != null)
|
||||
{
|
||||
SecureServer.Stop();
|
||||
this.LogVerbose("Server State: {0}", SecureServer.State);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State);
|
||||
OnServerStateChange(SecureServer.State);
|
||||
}
|
||||
ServerStopped = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -473,24 +480,27 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// Disconnects Client
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <summary>
|
||||
/// DisconnectClient method
|
||||
/// </summary>
|
||||
public void DisconnectClient(uint client)
|
||||
{
|
||||
try
|
||||
{
|
||||
SecureServer.Disconnect(client);
|
||||
this.LogVerbose("Disconnected client index: {0}", client);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Disconnect All Clients
|
||||
/// DisconnectAllClientsForShutdown method
|
||||
/// </summary>
|
||||
public void DisconnectAllClientsForShutdown()
|
||||
{
|
||||
this.LogInformation("Disconnecting All Clients");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
|
||||
if (SecureServer != null)
|
||||
{
|
||||
SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
|
||||
|
|
@ -502,17 +512,17 @@ public class GenericSecureTcpIpServer : Device
|
|||
try
|
||||
{
|
||||
SecureServer.Disconnect(i);
|
||||
this.LogInformation("Disconnected client index: {0}", i);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex);
|
||||
}
|
||||
}
|
||||
this.LogInformation("Server Status: {0}", SecureServer.ServerSocketStatus);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus);
|
||||
}
|
||||
|
||||
this.LogInformation("Disconnected All Clients");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients");
|
||||
ConnectedClientsIndexes.Clear();
|
||||
|
||||
if (!ProgramIsStopping)
|
||||
|
|
@ -528,10 +538,13 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// Broadcast text from server to all connected clients
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <summary>
|
||||
/// BroadcastText method
|
||||
/// </summary>
|
||||
public void BroadcastText(string text)
|
||||
{
|
||||
lock (_broadcastLock)
|
||||
{
|
||||
CCriticalSection CCBroadcast = new CCriticalSection();
|
||||
CCBroadcast.Enter();
|
||||
try
|
||||
{
|
||||
if (ConnectedClientsIndexes.Count > 0)
|
||||
|
|
@ -547,12 +560,13 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
}
|
||||
}
|
||||
CCBroadcast.Leave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
CCBroadcast.Leave();
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -560,6 +574,9 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <summary>
|
||||
/// SendTextToClient method
|
||||
/// </summary>
|
||||
public void SendTextToClient(string text, uint clientIndex)
|
||||
{
|
||||
try
|
||||
|
|
@ -573,7 +590,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,19 +608,13 @@ public class GenericSecureTcpIpServer : Device
|
|||
if (noDelimiter.Contains(HeartbeatStringToMatch))
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogDebug("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
// Return Heartbeat
|
||||
SendTextToClient(HeartbeatStringToMatch, clientIndex);
|
||||
return remainingText;
|
||||
|
|
@ -612,25 +623,19 @@ public class GenericSecureTcpIpServer : Device
|
|||
else
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogInformation("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
|
@ -640,13 +645,16 @@ public class GenericSecureTcpIpServer : Device
|
|||
/// </summary>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// GetClientIPAddress method
|
||||
/// </summary>
|
||||
public string GetClientIPAddress(uint clientIndex)
|
||||
{
|
||||
this.LogInformation("GetClientIPAddress Index: {0}", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex);
|
||||
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
|
||||
{
|
||||
var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
this.LogInformation("GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
return ipa;
|
||||
|
||||
}
|
||||
|
|
@ -669,13 +677,14 @@ public class GenericSecureTcpIpServer : Device
|
|||
clientIndex = (uint)o;
|
||||
address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
|
||||
this.LogInformation("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
|
||||
|
||||
if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
|
||||
|
||||
var discoResult = SecureServer.Disconnect(clientIndex);
|
||||
//Debug.Console(1, this, "{0}", discoResult);
|
||||
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
|
|
@ -704,9 +713,11 @@ public class GenericSecureTcpIpServer : Device
|
|||
try
|
||||
{
|
||||
|
||||
|
||||
// Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogInformation("SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
|
||||
|
||||
if (ConnectedClientsIndexes.Contains(clientIndex))
|
||||
ConnectedClientsIndexes.Remove(clientIndex);
|
||||
|
|
@ -728,12 +739,12 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex);
|
||||
}
|
||||
//Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state
|
||||
//after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag
|
||||
//is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event.
|
||||
System.Threading.Tasks.Task.Run(() => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)));
|
||||
CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -748,7 +759,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
{
|
||||
try
|
||||
{
|
||||
this.LogInformation("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
|
||||
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
|
||||
if (clientIndex != 0)
|
||||
|
|
@ -768,7 +779,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
|
||||
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
|
||||
this.LogInformation("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -778,10 +789,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
{
|
||||
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -790,19 +798,19 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Client attempt faulty.");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex);
|
||||
}
|
||||
|
||||
// Rearm the listner
|
||||
SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
|
||||
if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
|
||||
{
|
||||
this.LogError("Socket status connect callback status {0}", status);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Socket status connect callback status {0}", status);
|
||||
if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
|
||||
{
|
||||
// There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
|
||||
|
|
@ -839,7 +847,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
if (received != SharedKey)
|
||||
{
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
|
||||
this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
mySecureTCPServer.SendData(clientIndex, b, b.Length);
|
||||
mySecureTCPServer.Disconnect(clientIndex);
|
||||
|
||||
|
|
@ -850,7 +858,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
|
||||
mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
this.LogInformation("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
|
||||
{
|
||||
|
|
@ -863,7 +871,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
|
||||
}
|
||||
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
|
||||
|
|
@ -871,8 +879,9 @@ public class GenericSecureTcpIpServer : Device
|
|||
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
|
||||
if (handler != null)
|
||||
{
|
||||
if (System.Threading.Monitor.TryEnter(_dequeueLock))
|
||||
System.Threading.Tasks.Task.Run(() => DequeueEvent());
|
||||
var gotLock = DequeueLock.TryEnter();
|
||||
if (gotLock)
|
||||
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -882,7 +891,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method gets spooled up in its own thread an protected by a lock to prevent multiple threads from running concurrently.
|
||||
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
|
||||
/// It will dequeue items as they are enqueued automatically.
|
||||
/// </summary>
|
||||
void DequeueEvent()
|
||||
|
|
@ -902,10 +911,13 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogError(e, "DequeueEvent error");
|
||||
this.LogException(e, "DequeueEvent error");
|
||||
}
|
||||
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
|
||||
if (DequeueLock != null)
|
||||
{
|
||||
DequeueLock.Leave();
|
||||
}
|
||||
// Make sure to release the lock in case an exception above stops this thread, or we won't be able to restart it.
|
||||
System.Threading.Monitor.Exit(_dequeueLock);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -976,7 +988,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
if (MonitorClient != null)
|
||||
MonitorClient.Disconnect();
|
||||
|
||||
this.LogInformation("Program stopping. Closing server");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
|
||||
KillServer();
|
||||
}
|
||||
}
|
||||
|
|
@ -1002,9 +1014,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
{
|
||||
return;
|
||||
}
|
||||
MonitorClientTimer = new Timer(60000) { AutoReset = false };
|
||||
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
|
||||
MonitorClientTimer.Start();
|
||||
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1019,7 +1029,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
|
||||
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
|
||||
|
||||
this.LogInformation("Starting monitor check");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
|
||||
|
||||
MonitorClient.Connect();
|
||||
// From here MonitorCLient either connects or hangs, MonitorClient will call back
|
||||
|
|
@ -1046,7 +1056,7 @@ public class GenericSecureTcpIpServer : Device
|
|||
{
|
||||
if (args.IsReady)
|
||||
{
|
||||
this.LogInformation("Monitor client connection success. Disconnecting in 2s");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s");
|
||||
MonitorClientTimer.Stop();
|
||||
MonitorClientTimer = null;
|
||||
MonitorClientFailureCount = 0;
|
||||
|
|
@ -1067,13 +1077,13 @@ public class GenericSecureTcpIpServer : Device
|
|||
StopMonitorClient();
|
||||
if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
|
||||
{
|
||||
this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
|
||||
StartMonitorClient();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError(
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error,
|
||||
"\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************",
|
||||
MonitorClientMaxFailureCount);
|
||||
|
||||
|
|
@ -1084,4 +1094,5 @@ public class GenericSecureTcpIpServer : Device
|
|||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using System.Threading;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using Org.BouncyCastle.Utilities;
|
||||
using PepperDash.Core.Logging;
|
||||
using Renci.SshNet;
|
||||
using Renci.SshNet.Common;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// SSH Client
|
||||
/// </summary>
|
||||
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
{
|
||||
private const string SPlusKey = "Uninitialized SshClient";
|
||||
|
||||
/// <summary>
|
||||
/// Object to enable stream debugging
|
||||
/// </summary>
|
||||
|
|
@ -36,13 +38,8 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// </summary>
|
||||
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
|
||||
|
||||
///// <summary>
|
||||
/////
|
||||
///// </summary>
|
||||
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
|
||||
|
||||
/// <summary>
|
||||
/// Address of server
|
||||
/// Gets or sets the Hostname
|
||||
/// </summary>
|
||||
public string Hostname { get; set; }
|
||||
|
||||
|
|
@ -52,12 +49,12 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Username for server
|
||||
/// Gets or sets the Username
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// And... Password for server. That was worth documenting!
|
||||
/// Gets or sets the Password
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
|
|
@ -67,7 +64,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
public bool IsConnected
|
||||
{
|
||||
// returns false if no client or not connected
|
||||
get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -79,20 +76,30 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SSH Client
|
||||
/// Socket status change event
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus
|
||||
{
|
||||
get { return _ClientStatus; }
|
||||
get { lock (_stateLock) { return _ClientStatus; } }
|
||||
private set
|
||||
{
|
||||
if (_ClientStatus == value)
|
||||
return;
|
||||
bool shouldFireEvent = false;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_ClientStatus != value)
|
||||
{
|
||||
_ClientStatus = value;
|
||||
shouldFireEvent = true;
|
||||
}
|
||||
}
|
||||
// Fire event outside lock to avoid deadlock
|
||||
if (shouldFireEvent)
|
||||
OnConnectionChange();
|
||||
}
|
||||
}
|
||||
SocketStatus _ClientStatus;
|
||||
|
||||
private SocketStatus _ClientStatus;
|
||||
private bool _ConnectEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
|
||||
|
|
@ -100,7 +107,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// </summary>
|
||||
public ushort UStatus
|
||||
{
|
||||
get { return (ushort)_ClientStatus; }
|
||||
get { lock (_stateLock) { return (ushort)_ClientStatus; } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -111,7 +118,11 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Will be set and unset by connect and disconnect only
|
||||
/// </summary>
|
||||
public bool ConnectEnabled { get; private set; }
|
||||
public bool ConnectEnabled
|
||||
{
|
||||
get { lock (_stateLock) { return _ConnectEnabled; } }
|
||||
private set { lock (_stateLock) { _ConnectEnabled = value; } }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S+ helper for AutoReconnect
|
||||
|
|
@ -123,20 +134,26 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Millisecond value, determines the timeout period in between reconnect attempts.
|
||||
/// Set to 5000 by default
|
||||
/// Gets or sets the AutoReconnectIntervalMs
|
||||
/// </summary>
|
||||
public int AutoReconnectIntervalMs { get; set; }
|
||||
|
||||
SshClient Client;
|
||||
private SshClient client;
|
||||
|
||||
ShellStream TheStream;
|
||||
private ShellStream shellStream;
|
||||
|
||||
Timer ReconnectTimer;
|
||||
private readonly Timer reconnectTimer;
|
||||
|
||||
private System.Threading.SemaphoreSlim connectLock = new System.Threading.SemaphoreSlim(1);
|
||||
//Lock object to prevent simulatneous connect/disconnect operations
|
||||
//private CCriticalSection connectLock = new CCriticalSection();
|
||||
private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1);
|
||||
|
||||
private bool DisconnectLogged = false;
|
||||
// Thread-safety lock for state changes
|
||||
private readonly object _stateLock = new object();
|
||||
|
||||
private volatile bool _isProgramStopping;
|
||||
|
||||
private bool disconnectLogged = false;
|
||||
|
||||
/// <summary>
|
||||
/// When true, turns off echo for the SSH session
|
||||
|
|
@ -158,14 +175,13 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
Password = password;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
|
||||
ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
|
||||
ReconnectTimer.Elapsed += (s, e) =>
|
||||
reconnectTimer = new Timer(o =>
|
||||
{
|
||||
if (ConnectEnabled)
|
||||
if (ConnectEnabled) // Now thread-safe property access
|
||||
{
|
||||
Connect();
|
||||
}
|
||||
};
|
||||
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -177,33 +193,30 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
|
||||
ReconnectTimer = new Timer { AutoReset = false, Enabled = false };
|
||||
ReconnectTimer.Elapsed += (s, e) =>
|
||||
reconnectTimer = new Timer(o =>
|
||||
{
|
||||
if (ConnectEnabled)
|
||||
if (ConnectEnabled) // Now thread-safe property access
|
||||
{
|
||||
Connect();
|
||||
}
|
||||
};
|
||||
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles closing this up when the program shuts down
|
||||
/// </summary>
|
||||
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
if (Client != null)
|
||||
{
|
||||
_isProgramStopping = true;
|
||||
this.LogDebug("Program stopping. Closing connection");
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to the server, using the provided properties.
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
|
|
@ -215,6 +228,12 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
return;
|
||||
}
|
||||
|
||||
if (_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Skipping connect because program is stopping");
|
||||
return;
|
||||
}
|
||||
|
||||
ConnectEnabled = true;
|
||||
|
||||
try
|
||||
|
|
@ -229,13 +248,10 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
this.LogDebug("Attempting connect");
|
||||
|
||||
// Cancel reconnect if running.
|
||||
if (ReconnectTimer != null)
|
||||
{
|
||||
ReconnectTimer.Stop();
|
||||
}
|
||||
StopReconnectTimer();
|
||||
|
||||
// Cleanup the old client if it already exists
|
||||
if (Client != null)
|
||||
if (client != null)
|
||||
{
|
||||
this.LogDebug("Cleaning up disconnected client");
|
||||
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
|
||||
|
|
@ -248,14 +264,14 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
|
||||
this.LogDebug("Creating new SshClient");
|
||||
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
|
||||
Client = new SshClient(connectionInfo);
|
||||
Client.ErrorOccurred += Client_ErrorOccurred;
|
||||
client = new SshClient(connectionInfo);
|
||||
client.ErrorOccurred += Client_ErrorOccurred;
|
||||
|
||||
//Attempt to connect
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
|
||||
try
|
||||
{
|
||||
Client.Connect();
|
||||
client.Connect();
|
||||
|
||||
var modes = new Dictionary<TerminalModes, uint>();
|
||||
|
||||
|
|
@ -264,94 +280,68 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
modes.Add(TerminalModes.ECHO, 0);
|
||||
}
|
||||
|
||||
TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes);
|
||||
if (TheStream.DataAvailable)
|
||||
shellStream = client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes);
|
||||
if (shellStream.DataAvailable)
|
||||
{
|
||||
// empty the buffer if there is data
|
||||
string str = TheStream.Read();
|
||||
shellStream.Read();
|
||||
}
|
||||
TheStream.DataReceived += Stream_DataReceived;
|
||||
shellStream.DataReceived += Stream_DataReceived;
|
||||
this.LogInformation("Connected");
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
|
||||
DisconnectLogged = false;
|
||||
disconnectLogged = false;
|
||||
}
|
||||
catch (SshConnectionException e)
|
||||
{
|
||||
var ie = e.InnerException; // The details are inside!!
|
||||
|
||||
if (ie is SocketException)
|
||||
{
|
||||
this.LogException(ie, "CONNECTION failure: Cannot reach host");
|
||||
}
|
||||
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
|
||||
|
||||
if (ie is System.Net.Sockets.SocketException socketException)
|
||||
{
|
||||
this.LogException(ie, "Connection failure: Cannot reach {host} on {port}",
|
||||
this.LogError("Connection failure: Cannot reach {host} on {port}",
|
||||
Hostname, Port);
|
||||
this.LogVerbose(socketException, "SocketException details: ");
|
||||
}
|
||||
if (ie is SshAuthenticationException)
|
||||
else if (ie is SshAuthenticationException)
|
||||
{
|
||||
this.LogException(ie, "Authentication failure for username {userName}", Username);
|
||||
this.LogError("Authentication failure for username {userName}", Username);
|
||||
this.LogVerbose(ie, "AuthenticationException details: ");
|
||||
}
|
||||
else
|
||||
this.LogException(ie, "Error on connect");
|
||||
{
|
||||
this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
|
||||
this.LogVerbose(ie ?? e, "Exception details: ");
|
||||
}
|
||||
|
||||
DisconnectLogged = true;
|
||||
disconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
catch (SshOperationTimeoutException ex)
|
||||
{
|
||||
this.LogWarning("Connection attempt timed out: {message}", ex.Message);
|
||||
|
||||
DisconnectLogged = true;
|
||||
disconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
}
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException e)
|
||||
{
|
||||
// SSH.NET only wraps socket failures in SshConnectionException once the SSH
|
||||
// protocol handshake begins - a failure before that (e.g. DNS resolution failure,
|
||||
// "Name or service not known", or connection refused) surfaces as a raw
|
||||
// SocketException here instead. This is an expected/routine "device offline"
|
||||
// condition (same as the SshConnectionException case above), so log it at Warning
|
||||
// instead of falling through to the generic handler's LogException/Error-level
|
||||
// stack trace dump.
|
||||
this.LogWarning("Connection failure: Cannot reach {host} on {port} ({message})", Hostname, Port, e.Message);
|
||||
|
||||
DisconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogException(e, "Unhandled exception on connect");
|
||||
DisconnectLogged = true;
|
||||
this.LogError("Unhandled exception on connect: {error}", e.Message);
|
||||
this.LogVerbose(e, "Exception details: ");
|
||||
disconnectLogged = true;
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
if (AutoReconnect)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -369,7 +359,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
{
|
||||
ConnectEnabled = false;
|
||||
// Stop trying reconnects, if we are
|
||||
ReconnectTimer.Stop();
|
||||
StopReconnectTimer();
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
|
||||
}
|
||||
|
|
@ -383,12 +373,12 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
|
||||
try
|
||||
{
|
||||
if (Client != null)
|
||||
if (client != null)
|
||||
{
|
||||
Client.ErrorOccurred -= Client_ErrorOccurred;
|
||||
Client.Disconnect();
|
||||
Client.Dispose();
|
||||
Client = null;
|
||||
client.ErrorOccurred -= Client_ErrorOccurred;
|
||||
client.Disconnect();
|
||||
client.Dispose();
|
||||
client = null;
|
||||
ClientStatus = status;
|
||||
this.LogDebug("Disconnected");
|
||||
}
|
||||
|
|
@ -402,16 +392,16 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Kills the stream
|
||||
/// </summary>
|
||||
void KillStream()
|
||||
private void KillStream()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (TheStream != null)
|
||||
if (shellStream != null)
|
||||
{
|
||||
TheStream.DataReceived -= Stream_DataReceived;
|
||||
TheStream.Close();
|
||||
TheStream.Dispose();
|
||||
TheStream = null;
|
||||
shellStream.DataReceived -= Stream_DataReceived;
|
||||
shellStream.Close();
|
||||
shellStream.Dispose();
|
||||
shellStream = null;
|
||||
this.LogDebug("Disconnected stream");
|
||||
}
|
||||
}
|
||||
|
|
@ -424,7 +414,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Handles the keyboard interactive authentication, should it be required.
|
||||
/// </summary>
|
||||
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
|
||||
private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
|
||||
{
|
||||
foreach (AuthenticationPrompt prompt in e.Prompts)
|
||||
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
|
||||
|
|
@ -434,7 +424,7 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
|
||||
/// </summary>
|
||||
void Stream_DataReceived(object sender, ShellDataEventArgs e)
|
||||
private void Stream_DataReceived(object sender, ShellDataEventArgs e)
|
||||
{
|
||||
if (((ShellStream)sender).Length <= 0L)
|
||||
{
|
||||
|
|
@ -447,18 +437,14 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
if (bytesHandler != null)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(response);
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
this.PrintReceivedBytes(bytes);
|
||||
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
}
|
||||
|
||||
var textHandler = TextReceived;
|
||||
if (textHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response));
|
||||
this.PrintReceivedText(response);
|
||||
|
||||
textHandler(this, new GenericCommMethodReceiveTextArgs(response));
|
||||
}
|
||||
|
|
@ -470,9 +456,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
|
||||
/// event
|
||||
/// </summary>
|
||||
void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
|
||||
private void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
|
||||
{
|
||||
System.Threading.Tasks.Task.Run(() =>
|
||||
CrestronInvoke.BeginInvoke(o =>
|
||||
{
|
||||
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
|
||||
this.LogError("Disconnected by remote");
|
||||
|
|
@ -487,12 +473,10 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
{
|
||||
connectLock.Release();
|
||||
}
|
||||
if (AutoReconnect && ConnectEnabled)
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Interval = AutoReconnectIntervalMs;
|
||||
ReconnectTimer.Start();
|
||||
StartReconnectTimer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -500,10 +484,9 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Helper for ConnectionChange event
|
||||
/// </summary>
|
||||
void OnConnectionChange()
|
||||
private void OnConnectionChange()
|
||||
{
|
||||
if (ConnectionChange != null)
|
||||
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
|
||||
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
|
||||
}
|
||||
|
||||
#region IBasicCommunication Members
|
||||
|
|
@ -511,21 +494,17 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Sends text to the server
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="text">The text to send</param>
|
||||
public void SendText(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Client != null && TheStream != null && IsConnected)
|
||||
if (client != null && shellStream != null && IsConnected)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation(
|
||||
"Sending {length} characters of text: '{text}'",
|
||||
text.Length,
|
||||
ComTextHelper.GetDebugText(text));
|
||||
this.PrintSentText(text);
|
||||
|
||||
TheStream.Write(text);
|
||||
TheStream.Flush();
|
||||
shellStream.Write(text);
|
||||
shellStream.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -537,8 +516,10 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Start();
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -549,18 +530,17 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
/// <summary>
|
||||
/// Sends Bytes to the server
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <param name="bytes">The bytes to send</param>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Client != null && TheStream != null && IsConnected)
|
||||
if (client != null && shellStream != null && IsConnected)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
this.PrintSentBytes(bytes);
|
||||
|
||||
TheStream.Write(bytes, 0, bytes.Length);
|
||||
TheStream.Flush();
|
||||
shellStream.Write(bytes, 0, bytes.Length);
|
||||
shellStream.Flush();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -572,8 +552,10 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||
|
||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||
ReconnectTimer.Stop();
|
||||
ReconnectTimer.Start();
|
||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
||||
{
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -582,10 +564,89 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Safely starts the reconnect timer with exception handling
|
||||
/// </summary>
|
||||
private void StartReconnectTimer()
|
||||
{
|
||||
try
|
||||
{
|
||||
reconnectTimer?.Change(AutoReconnectIntervalMs, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Timer was disposed, ignore
|
||||
this.LogDebug("Attempted to start timer but it was already disposed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Safely stops the reconnect timer with exception handling
|
||||
/// </summary>
|
||||
private void StopReconnectTimer()
|
||||
{
|
||||
try
|
||||
{
|
||||
reconnectTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Timer was disposed, ignore
|
||||
this.LogDebug("Attempted to stop timer but it was already disposed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivate method - properly dispose of resources
|
||||
/// </summary>
|
||||
public override bool Deactivate()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogDebug("Deactivating SSH client - disposing resources");
|
||||
|
||||
// Stop trying reconnects
|
||||
ConnectEnabled = false;
|
||||
StopReconnectTimer();
|
||||
|
||||
// Disconnect and cleanup client
|
||||
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
|
||||
|
||||
// Dispose timer
|
||||
try
|
||||
{
|
||||
reconnectTimer?.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Already disposed, ignore
|
||||
}
|
||||
|
||||
// Dispose semaphore
|
||||
try
|
||||
{
|
||||
connectLock?.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Already disposed, ignore
|
||||
}
|
||||
|
||||
return base.Deactivate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error during SSH client deactivation");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//*****************************************************************************************************
|
||||
//*****************************************************************************************************
|
||||
/// <summary>
|
||||
/// Fired when connection changes
|
||||
/// Represents a SshConnectionChangeEventArgs
|
||||
/// </summary>
|
||||
public class SshConnectionChangeEventArgs : EventArgs
|
||||
{
|
||||
|
|
@ -595,17 +656,17 @@ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoR
|
|||
public bool IsConnected { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection Status represented as a ushort
|
||||
/// Gets or sets the UIsConnected
|
||||
/// </summary>
|
||||
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
|
||||
|
||||
/// <summary>
|
||||
/// The client
|
||||
/// Gets or sets the Client
|
||||
/// </summary>
|
||||
public GenericSshClient Client { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Socket Status as represented by
|
||||
/// Gets or sets the Status
|
||||
/// </summary>
|
||||
public ushort Status { get { return Client.UStatus; } }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,18 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Timer = System.Timers.Timer;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
|
||||
using PepperDash.Core.Logging;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A class to handle basic TCP/IP communications with a server
|
||||
/// </summary>
|
||||
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A class to handle basic TCP/IP communications with a server
|
||||
/// </summary>
|
||||
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
{
|
||||
private const string SplusKey = "Uninitialized TcpIpClient";
|
||||
/// <summary>
|
||||
/// Object to enable stream debugging
|
||||
|
|
@ -66,7 +59,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port on server
|
||||
/// Gets or sets the Port
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
|
|
@ -131,6 +124,12 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
/// </summary>
|
||||
public string ClientStatusText { get { return ClientStatus.ToString(); } }
|
||||
|
||||
/// <summary>
|
||||
/// Ushort representation of client status
|
||||
/// </summary>
|
||||
[Obsolete]
|
||||
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
|
||||
|
||||
/// <summary>
|
||||
/// Connection failure reason
|
||||
/// </summary>
|
||||
|
|
@ -169,10 +168,10 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
}
|
||||
|
||||
//Lock object to prevent simulatneous connect/disconnect operations
|
||||
private readonly object _connectLock = new();
|
||||
private CCriticalSection connectLock = new CCriticalSection();
|
||||
|
||||
// private Timer for auto reconnect
|
||||
private Timer RetryTimer;
|
||||
private CTimer RetryTimer;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
|
|
@ -191,7 +190,10 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
Port = port;
|
||||
BufferSize = bufferSize;
|
||||
|
||||
SetupRetryTimer();
|
||||
RetryTimer = new CTimer(o =>
|
||||
{
|
||||
Reconnect();
|
||||
}, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -206,7 +208,10 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
AutoReconnectIntervalMs = 5000;
|
||||
BufferSize = 2000;
|
||||
|
||||
SetupRetryTimer();
|
||||
RetryTimer = new CTimer(o =>
|
||||
{
|
||||
Reconnect();
|
||||
}, Timeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -220,19 +225,14 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
AutoReconnectIntervalMs = 5000;
|
||||
BufferSize = 2000;
|
||||
|
||||
SetupRetryTimer();
|
||||
}
|
||||
|
||||
private void SetupRetryTimer()
|
||||
RetryTimer = new CTimer(o =>
|
||||
{
|
||||
RetryTimer = new Timer { AutoReset = false, Enabled = false };
|
||||
RetryTimer.Elapsed += (s, e) => Reconnect();
|
||||
Reconnect();
|
||||
}, Timeout.Infinite);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Just to help S+ set the key
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
|
|
@ -246,7 +246,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing connection");
|
||||
Debug.Console(1, this, "Program stopping. Closing connection");
|
||||
Deactivate();
|
||||
}
|
||||
}
|
||||
|
|
@ -255,6 +255,9 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// Deactivate method
|
||||
/// </summary>
|
||||
public override bool Deactivate()
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
|
|
@ -268,28 +271,29 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to connect to the server
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("GenericTcpIpClient '{0}': No address set", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key);
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
{
|
||||
this.LogWarning("GenericTcpIpClient '{0}': Invalid port", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Connection already connected. Exiting Connect()");
|
||||
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -302,6 +306,10 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
_client.ConnectToServerAsync(ConnectToServerCallback);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
private void Reconnect()
|
||||
|
|
@ -310,43 +318,53 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
{
|
||||
return;
|
||||
}
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
if (IsConnected || DisconnectCalledByUser == true)
|
||||
{
|
||||
this.LogInformation("Reconnect no longer needed. Exiting Reconnect()");
|
||||
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogInformation("Attempting reconnect now");
|
||||
Debug.Console(1, this, "Attempting reconnect now");
|
||||
_client.ConnectToServerAsync(ConnectToServerCallback);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to disconnect the client
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
DisconnectCalledByUser = true;
|
||||
|
||||
// Stop trying reconnects, if we are
|
||||
RetryTimer.Stop();
|
||||
DisconnectClient();
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the actual disconnect business
|
||||
/// DisconnectClient method
|
||||
/// </summary>
|
||||
public void DisconnectClient()
|
||||
{
|
||||
if (_client != null)
|
||||
{
|
||||
this.LogInformation("Disconnecting client");
|
||||
Debug.Console(1, this, "Disconnecting client");
|
||||
if (IsConnected)
|
||||
_client.DisconnectFromServer();
|
||||
}
|
||||
|
|
@ -360,12 +378,12 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
{
|
||||
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogInformation("Server connection result: {0}", c.ClientStatus);
|
||||
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus);
|
||||
WaitAndTryReconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogInformation("Server connection result: {0}", c.ClientStatus);
|
||||
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,19 +392,22 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
/// </summary>
|
||||
void WaitAndTryReconnect()
|
||||
{
|
||||
Task.Run(() =>
|
||||
CrestronInvoke.BeginInvoke(o =>
|
||||
{
|
||||
lock (_connectLock)
|
||||
try
|
||||
{
|
||||
connectLock.Enter();
|
||||
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
|
||||
{
|
||||
DisconnectClient();
|
||||
this.LogInformation("Attempting reconnect, status={0}", _client.ClientStatus);
|
||||
RetryTimer.Stop();
|
||||
RetryTimer.Interval = AutoReconnectIntervalMs;
|
||||
RetryTimer.Start();
|
||||
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus);
|
||||
RetryTimer.Reset(AutoReconnectIntervalMs);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
connectLock.Leave();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -405,10 +426,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
var bytesHandler = BytesReceived;
|
||||
if (bytesHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
this.PrintReceivedBytes(bytes);
|
||||
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
}
|
||||
var textHandler = TextReceived;
|
||||
|
|
@ -416,10 +434,7 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
{
|
||||
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
|
||||
}
|
||||
this.PrintReceivedText(str);
|
||||
|
||||
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
|
||||
}
|
||||
|
|
@ -429,14 +444,13 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// General send method
|
||||
/// SendText method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
// Check debug level before processing byte array
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
|
||||
this.PrintSentText(text);
|
||||
if (_client != null)
|
||||
_client.SendData(bytes, bytes.Length);
|
||||
}
|
||||
|
|
@ -458,10 +472,12 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
/// Sends Bytes to the server
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <summary>
|
||||
/// SendBytes method
|
||||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
this.PrintSentBytes(bytes);
|
||||
if (_client != null)
|
||||
_client.SendData(bytes, bytes.Length);
|
||||
}
|
||||
|
|
@ -475,12 +491,12 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
{
|
||||
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
WaitAndTryReconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogDebug("Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
|
||||
_client.ReceiveDataAsync(Receive);
|
||||
}
|
||||
|
||||
|
|
@ -488,13 +504,13 @@ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAut
|
|||
if (handler != null)
|
||||
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration properties for TCP/SSH Connections
|
||||
/// </summary>
|
||||
public class TcpSshPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a TcpSshPropertiesConfig
|
||||
/// </summary>
|
||||
public class TcpSshPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Address to connect to
|
||||
/// </summary>
|
||||
|
|
@ -512,7 +528,7 @@ public class TcpSshPropertiesConfig
|
|||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
/// <summary>
|
||||
/// Passord credential
|
||||
/// Gets or sets the Password
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
|
|
@ -547,6 +563,7 @@ public class TcpSshPropertiesConfig
|
|||
AutoReconnectIntervalMs = 5000;
|
||||
Username = "";
|
||||
Password = "";
|
||||
DisableSshEcho = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,20 +11,21 @@ PepperDash Technology Corporation reserves all rights under applicable laws.
|
|||
------------------------------------ */
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generic TCP/IP client for server
|
||||
/// </summary>
|
||||
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic TCP/IP client for server
|
||||
/// </summary>
|
||||
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
||||
{
|
||||
/// <summary>
|
||||
/// Band aid delegate for choked server
|
||||
/// </summary>
|
||||
|
|
@ -68,7 +69,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
public string Hostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Port on server
|
||||
/// Gets or sets the Port
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
|
|
@ -101,7 +102,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
|
||||
/// Gets or sets the SharedKey
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
|
|
@ -111,7 +112,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
private bool WaitingForSharedKeyResponse { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to 2000
|
||||
/// Gets or sets the BufferSize
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
|
|
@ -209,7 +210,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
/// <summary>
|
||||
/// private Timer for auto reconnect
|
||||
/// </summary>
|
||||
Timer RetryTimer;
|
||||
CTimer RetryTimer;
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -236,13 +237,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
/// </summary>
|
||||
public int HeartbeatInterval = 50000;
|
||||
|
||||
Timer HeartbeatSendTimer;
|
||||
Timer HeartbeatAckTimer;
|
||||
CTimer HeartbeatSendTimer;
|
||||
CTimer HeartbeatAckTimer;
|
||||
/// <summary>
|
||||
/// Used to force disconnection on a dead connect attempt
|
||||
/// </summary>
|
||||
Timer ConnectFailTimer;
|
||||
Timer WaitForSharedKey;
|
||||
CTimer ConnectFailTimer;
|
||||
CTimer WaitForSharedKey;
|
||||
private int ConnectionCount;
|
||||
/// <summary>
|
||||
/// Internal secure client
|
||||
|
|
@ -288,7 +289,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
#region Methods
|
||||
|
||||
/// <summary>
|
||||
/// Just to help S+ set the key
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
|
|
@ -302,7 +303,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
|
||||
{
|
||||
this.LogInformation("Program stopping. Closing Client connection");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
|
||||
ProgramIsStopping = true;
|
||||
Disconnect();
|
||||
}
|
||||
|
|
@ -310,22 +311,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
ConnectionCount++;
|
||||
this.LogDebug("Attempting connect Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
|
||||
|
||||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogInformation("Already connected. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
|
||||
return;
|
||||
}
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
this.LogInformation("Already trying to connect. Ignoring.");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
|
||||
return;
|
||||
}
|
||||
try
|
||||
|
|
@ -338,17 +339,17 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No address set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: Invalid port");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogWarning("DynamicTcpClient: No Shared Key set");
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -369,10 +370,9 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||
|
||||
ConnectFailTimer = new Timer(30000) { AutoReset = false };
|
||||
ConnectFailTimer.Elapsed += (s, e) =>
|
||||
ConnectFailTimer = new CTimer(o =>
|
||||
{
|
||||
this.LogError("Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
|
||||
if (IsTryingToConnect)
|
||||
{
|
||||
IsTryingToConnect = false;
|
||||
|
|
@ -384,13 +384,12 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
//SecureClient.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
}
|
||||
};
|
||||
ConnectFailTimer.Start();
|
||||
}, 30000);
|
||||
|
||||
this.LogDebug("Making Connection Count:{0}", ConnectionCount);
|
||||
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
|
||||
Client.ConnectToServerAsync(o =>
|
||||
{
|
||||
this.LogDebug("ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
|
||||
|
||||
if (ConnectFailTimer != null)
|
||||
{
|
||||
|
|
@ -400,22 +399,22 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
this.LogVerbose("Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
|
||||
o.ReceiveDataAsync(Receive);
|
||||
|
||||
if (SharedKeyRequired)
|
||||
{
|
||||
WaitingForSharedKeyResponse = true;
|
||||
WaitForSharedKey = new Timer(15000) { AutoReset = false };
|
||||
WaitForSharedKey.Elapsed += (s, e) =>
|
||||
WaitForSharedKey = new CTimer(timer =>
|
||||
{
|
||||
this.LogWarning("Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
|
||||
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
|
||||
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
|
||||
o.DisconnectFromServer();
|
||||
//CheckClosedAndTryReconnect();
|
||||
//OnClientReadyForcommunications(false); // Should send false event
|
||||
};
|
||||
WaitForSharedKey.Start();
|
||||
}, 15000);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -429,22 +428,21 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
else
|
||||
{
|
||||
this.LogWarning("Connect attempt failed {0}", o.ClientStatus);
|
||||
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Client connection exception: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
|
||||
IsTryingToConnect = false;
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
|
|
@ -474,7 +472,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
if (Client != null)
|
||||
{
|
||||
//SecureClient.DisconnectFromServer();
|
||||
this.LogVerbose("Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
|
||||
Client.SocketStatusChange -= Client_SocketStatusChange;
|
||||
Client.Dispose();
|
||||
Client = null;
|
||||
|
|
@ -496,22 +494,20 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (Client != null)
|
||||
{
|
||||
this.LogVerbose("Cleaning up remotely closed/failed connection.");
|
||||
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
|
||||
Cleanup();
|
||||
}
|
||||
if (!DisconnectCalledByUser && AutoReconnect)
|
||||
{
|
||||
var halfInterval = AutoReconnectIntervalMs / 2;
|
||||
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
|
||||
this.LogVerbose("Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
|
||||
if (RetryTimer != null)
|
||||
{
|
||||
RetryTimer.Stop();
|
||||
RetryTimer = null;
|
||||
}
|
||||
RetryTimer = new Timer(rndTime) { AutoReset = false };
|
||||
RetryTimer.Elapsed += (s, e) => Connect();
|
||||
RetryTimer.Start();
|
||||
RetryTimer = new CTimer(o => Connect(), rndTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -530,18 +526,18 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
|
||||
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
this.LogVerbose("Client Received:\r--------\r{0}\r--------", str);
|
||||
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
|
||||
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
|
||||
{
|
||||
if (SharedKeyRequired && str == "SharedKey:")
|
||||
{
|
||||
this.LogVerbose("Server asking for shared key, sending");
|
||||
Debug.Console(2, this, "Server asking for shared key, sending");
|
||||
SendText(SharedKey + "\n");
|
||||
}
|
||||
else if (SharedKeyRequired && str == "Shared Key Match")
|
||||
{
|
||||
StopWaitForSharedKeyTimer();
|
||||
this.LogVerbose("Shared key confirmed. Ready for communication");
|
||||
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
|
||||
OnClientReadyForcommunications(true); // Successful key exchange
|
||||
}
|
||||
else
|
||||
|
|
@ -557,8 +553,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
|
||||
}
|
||||
}
|
||||
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
|
|
@ -569,19 +564,15 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (HeartbeatEnabled)
|
||||
{
|
||||
this.LogVerbose("Starting Heartbeat");
|
||||
Debug.Console(2, this, "Starting Heartbeat");
|
||||
if (HeartbeatSendTimer == null)
|
||||
{
|
||||
|
||||
HeartbeatSendTimer = new Timer(HeartbeatInterval) { AutoReset = true };
|
||||
HeartbeatSendTimer.Elapsed += (s, e) => SendHeartbeat(null);
|
||||
HeartbeatSendTimer.Start();
|
||||
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
|
||||
}
|
||||
if (HeartbeatAckTimer == null)
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -591,13 +582,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
if (HeartbeatSendTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Send");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Send");
|
||||
HeartbeatSendTimer.Stop();
|
||||
HeartbeatSendTimer = null;
|
||||
}
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
this.LogVerbose("Stoping Heartbeat Ack");
|
||||
Debug.Console(2, this, "Stoping Heartbeat Ack");
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer = null;
|
||||
}
|
||||
|
|
@ -606,7 +597,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
void SendHeartbeat(object notused)
|
||||
{
|
||||
this.SendText(HeartbeatString);
|
||||
this.LogVerbose("Sending Heartbeat");
|
||||
Debug.Console(2, this, "Sending Heartbeat");
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -625,17 +616,13 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
{
|
||||
if (HeartbeatAckTimer != null)
|
||||
{
|
||||
HeartbeatAckTimer.Stop();
|
||||
HeartbeatAckTimer.Interval = HeartbeatInterval * 2;
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeartbeatAckTimer = new Timer(HeartbeatInterval * 2) { AutoReset = true };
|
||||
HeartbeatAckTimer.Elapsed += (s, e) => HeartbeatAckTimerFail(null);
|
||||
HeartbeatAckTimer.Start();
|
||||
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
|
||||
return remainingText;
|
||||
}
|
||||
}
|
||||
|
|
@ -643,8 +630,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
|
@ -658,7 +644,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
|
||||
if (IsConnected)
|
||||
{
|
||||
this.LogWarning("Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
|
||||
SendText("Heartbeat not received by server, closing connection");
|
||||
CheckClosedAndTryReconnect();
|
||||
}
|
||||
|
|
@ -666,8 +652,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Heartbeat timeout Error on Client: {0}, {1}", Key, ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -684,7 +669,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// General send method
|
||||
/// SendText method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
|
|
@ -700,21 +685,20 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
|
||||
if (n <= 0)
|
||||
{
|
||||
this.LogWarning("[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// SendBytes method
|
||||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
|
|
@ -727,8 +711,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending bytes. Error: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -747,7 +730,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
try
|
||||
{
|
||||
this.LogVerbose("Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
|
||||
|
||||
OnConnectionChange();
|
||||
|
||||
|
|
@ -761,8 +744,7 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in socket status change callback. Error: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace: {0}", ex.StackTrace);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -788,4 +770,6 @@ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
|
|||
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,18 +13,17 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using PepperDash.Core.Logging;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generic TCP/IP server device
|
||||
/// </summary>
|
||||
public class GenericTcpIpServer : Device
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic TCP/IP server device
|
||||
/// </summary>
|
||||
public class GenericTcpIpServer : Device
|
||||
{
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// Event for Receiving text
|
||||
|
|
@ -53,7 +52,7 @@ public class GenericTcpIpServer : Device
|
|||
public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Delegate for ServerHasChokedCallbackDelegate
|
||||
/// </summary>
|
||||
public delegate void ServerHasChokedCallbackDelegate();
|
||||
|
||||
|
|
@ -62,14 +61,9 @@ public class GenericTcpIpServer : Device
|
|||
#region Properties/Variables
|
||||
|
||||
/// <summary>
|
||||
/// Server listen lock
|
||||
///
|
||||
/// </summary>
|
||||
object _serverLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast lock
|
||||
/// </summary>
|
||||
private readonly object _broadcastLock = new();
|
||||
CCriticalSection ServerCCSection = new CCriticalSection();
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -80,7 +74,7 @@ public class GenericTcpIpServer : Device
|
|||
/// <summary>
|
||||
/// Timer to operate the bandaid monitor client in a loop.
|
||||
/// </summary>
|
||||
Timer MonitorClientTimer;
|
||||
CTimer MonitorClientTimer;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
|
@ -88,7 +82,7 @@ public class GenericTcpIpServer : Device
|
|||
int MonitorClientFailureCount;
|
||||
|
||||
/// <summary>
|
||||
/// 3 by default
|
||||
/// Gets or sets the MonitorClientMaxFailureCount
|
||||
/// </summary>
|
||||
public int MonitorClientMaxFailureCount { get; set; }
|
||||
|
||||
|
|
@ -177,7 +171,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port Server should listen on
|
||||
/// Gets or sets the Port
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
|
|
@ -210,8 +204,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module.
|
||||
/// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
|
||||
/// Gets or sets the SharedKey
|
||||
/// </summary>
|
||||
public string SharedKey { get; set; }
|
||||
|
||||
|
|
@ -235,7 +228,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
|
||||
/// Gets or sets the HeartbeatRequiredIntervalMs
|
||||
/// </summary>
|
||||
public int HeartbeatRequiredIntervalMs { get; set; }
|
||||
|
||||
|
|
@ -245,12 +238,12 @@ public class GenericTcpIpServer : Device
|
|||
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
|
||||
|
||||
/// <summary>
|
||||
/// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
|
||||
/// Gets or sets the HeartbeatStringToMatch
|
||||
/// </summary>
|
||||
public string HeartbeatStringToMatch { get; set; }
|
||||
|
||||
//private timers for Heartbeats per client
|
||||
Dictionary<uint, Timer> HeartbeatTimerDictionary = new Dictionary<uint, Timer>();
|
||||
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
|
||||
|
||||
//flags to show the secure server is waiting for client at index to send the shared key
|
||||
List<uint> WaitingForSharedKey = new List<uint>();
|
||||
|
|
@ -263,7 +256,7 @@ public class GenericTcpIpServer : Device
|
|||
public List<uint> ConnectedClientsIndexes = new List<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to 2000
|
||||
/// Gets or sets the BufferSize
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
|
|
@ -326,7 +319,7 @@ public class GenericTcpIpServer : Device
|
|||
|
||||
#region Methods - Server Actions
|
||||
/// <summary>
|
||||
/// Disconnects all clients and stops the server
|
||||
/// KillServer method
|
||||
/// </summary>
|
||||
public void KillServer()
|
||||
{
|
||||
|
|
@ -343,6 +336,9 @@ public class GenericTcpIpServer : Device
|
|||
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -371,32 +367,33 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.LogError("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start listening on the specified port
|
||||
/// Listen method
|
||||
/// </summary>
|
||||
public void Listen()
|
||||
{
|
||||
lock (_serverLock)
|
||||
{
|
||||
ServerCCSection.Enter();
|
||||
try
|
||||
{
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
this.LogError("Server '{0}': Invalid port", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
|
||||
{
|
||||
this.LogError("Server '{0}': No Shared Key set", Key);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
|
||||
ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
|
||||
return;
|
||||
}
|
||||
if (IsListening)
|
||||
|
|
@ -422,36 +419,39 @@ public class GenericTcpIpServer : Device
|
|||
ServerStopped = false;
|
||||
myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
|
||||
OnServerStateChange(myTcpServer.State);
|
||||
this.LogInformation("TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
|
||||
|
||||
// StartMonitorClient();
|
||||
|
||||
|
||||
ServerCCSection.Leave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error with Dynamic Server: {0}", ex.Message);
|
||||
ServerCCSection.Leave();
|
||||
ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop Listening
|
||||
/// StopListening method
|
||||
/// </summary>
|
||||
public void StopListening()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.LogDebug("Stopping Listener");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
|
||||
if (myTcpServer != null)
|
||||
{
|
||||
myTcpServer.Stop();
|
||||
this.LogDebug("Server State: {0}", myTcpServer.State);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State);
|
||||
OnServerStateChange(myTcpServer.State);
|
||||
}
|
||||
ServerStopped = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error stopping server. Error: {0}", ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -459,24 +459,27 @@ public class GenericTcpIpServer : Device
|
|||
/// Disconnects Client
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <summary>
|
||||
/// DisconnectClient method
|
||||
/// </summary>
|
||||
public void DisconnectClient(uint client)
|
||||
{
|
||||
try
|
||||
{
|
||||
myTcpServer.Disconnect(client);
|
||||
this.LogVerbose("Disconnected client index: {0}", client);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", client, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Disconnect All Clients
|
||||
/// DisconnectAllClientsForShutdown method
|
||||
/// </summary>
|
||||
public void DisconnectAllClientsForShutdown()
|
||||
{
|
||||
this.LogInformation("Disconnecting All Clients");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
|
||||
if (myTcpServer != null)
|
||||
{
|
||||
myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
|
||||
|
|
@ -488,17 +491,17 @@ public class GenericTcpIpServer : Device
|
|||
try
|
||||
{
|
||||
myTcpServer.Disconnect(i);
|
||||
this.LogVerbose("Disconnected client index: {0}", i);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Disconnecting client index: {0}. Error: {1}", i, ex.Message);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex);
|
||||
}
|
||||
}
|
||||
this.LogVerbose("Server Status: {0}", myTcpServer.ServerSocketStatus);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus);
|
||||
}
|
||||
|
||||
this.LogInformation("Disconnected All Clients");
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients");
|
||||
ConnectedClientsIndexes.Clear();
|
||||
|
||||
if (!ProgramIsStopping)
|
||||
|
|
@ -514,10 +517,13 @@ public class GenericTcpIpServer : Device
|
|||
/// Broadcast text from server to all connected clients
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <summary>
|
||||
/// BroadcastText method
|
||||
/// </summary>
|
||||
public void BroadcastText(string text)
|
||||
{
|
||||
lock (_broadcastLock)
|
||||
{
|
||||
CCriticalSection CCBroadcast = new CCriticalSection();
|
||||
CCBroadcast.Enter();
|
||||
try
|
||||
{
|
||||
if (ConnectedClientsIndexes.Count > 0)
|
||||
|
|
@ -533,12 +539,13 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
}
|
||||
}
|
||||
CCBroadcast.Leave();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
CCBroadcast.Leave();
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -546,6 +553,9 @@ public class GenericTcpIpServer : Device
|
|||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <summary>
|
||||
/// SendTextToClient method
|
||||
/// </summary>
|
||||
public void SendTextToClient(string text, uint clientIndex)
|
||||
{
|
||||
try
|
||||
|
|
@ -559,7 +569,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -577,19 +587,13 @@ public class GenericTcpIpServer : Device
|
|||
if (noDelimiter.Contains(HeartbeatStringToMatch))
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
|
||||
// Return Heartbeat
|
||||
SendTextToClient(HeartbeatStringToMatch, clientIndex);
|
||||
return remainingText;
|
||||
|
|
@ -598,25 +602,19 @@ public class GenericTcpIpServer : Device
|
|||
else
|
||||
{
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
HeartbeatTimerDictionary[clientIndex].Stop();
|
||||
HeartbeatTimerDictionary[clientIndex].Interval = HeartbeatRequiredIntervalMs;
|
||||
HeartbeatTimerDictionary[clientIndex].Start();
|
||||
}
|
||||
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
|
||||
else
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
|
||||
}
|
||||
this.LogVerbose("Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error checking heartbeat: {0}", ex.Message);
|
||||
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
|
||||
}
|
||||
return received;
|
||||
}
|
||||
|
|
@ -626,13 +624,16 @@ public class GenericTcpIpServer : Device
|
|||
/// </summary>
|
||||
/// <param name="clientIndex"></param>
|
||||
/// <returns>IP address of the client</returns>
|
||||
/// <summary>
|
||||
/// GetClientIPAddress method
|
||||
/// </summary>
|
||||
public string GetClientIPAddress(uint clientIndex)
|
||||
{
|
||||
this.LogVerbose("GetClientIPAddress Index: {0}", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex);
|
||||
if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
|
||||
{
|
||||
var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
this.LogVerbose("GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa);
|
||||
return ipa;
|
||||
|
||||
}
|
||||
|
|
@ -655,13 +656,14 @@ public class GenericTcpIpServer : Device
|
|||
clientIndex = (uint)o;
|
||||
address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
|
||||
|
||||
this.LogWarning("Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
|
||||
address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
|
||||
|
||||
if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
|
||||
|
||||
var discoResult = myTcpServer.Disconnect(clientIndex);
|
||||
//Debug.Console(1, this, "{0}", discoResult);
|
||||
|
||||
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
|
|
@ -672,8 +674,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message);
|
||||
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
|
||||
ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -691,7 +692,7 @@ public class GenericTcpIpServer : Device
|
|||
try
|
||||
{
|
||||
|
||||
this.LogInformation("SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
{
|
||||
if (ConnectedClientsIndexes.Contains(clientIndex))
|
||||
|
|
@ -710,7 +711,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Change Callback. Error: {0}", ex);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex);
|
||||
}
|
||||
onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
|
||||
}
|
||||
|
|
@ -727,7 +728,7 @@ public class GenericTcpIpServer : Device
|
|||
{
|
||||
try
|
||||
{
|
||||
this.LogDebug("ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
|
||||
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
|
||||
clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
|
||||
if (clientIndex != 0)
|
||||
|
|
@ -747,21 +748,17 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
|
||||
server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
|
||||
this.LogDebug("Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogDebug("Client at index {0} is ready for communications", clientIndex);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
}
|
||||
if (HeartbeatRequired)
|
||||
{
|
||||
if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
|
||||
{
|
||||
var heartbeatTimer = new Timer(HeartbeatRequiredIntervalMs) { AutoReset = false };
|
||||
heartbeatTimer.Elapsed += (s, e) => HeartbeatTimer_CallbackFunction(clientIndex);
|
||||
heartbeatTimer.Start();
|
||||
HeartbeatTimerDictionary.Add(clientIndex, heartbeatTimer);
|
||||
HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -770,7 +767,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
else
|
||||
{
|
||||
this.LogError("Client attempt faulty.");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
|
||||
if (!ServerStopped)
|
||||
{
|
||||
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
|
||||
|
|
@ -780,15 +777,15 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error in Socket Status Connect Callback. Error: {0}", ex.Message);
|
||||
this.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex);
|
||||
}
|
||||
//Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))",
|
||||
// server.State,
|
||||
// MaxClients,
|
||||
// ServerStopped);
|
||||
if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
|
||||
{
|
||||
this.LogDebug("Waiting for next connection");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection");
|
||||
server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
|
||||
|
||||
}
|
||||
|
|
@ -819,7 +816,7 @@ public class GenericTcpIpServer : Device
|
|||
if (received != SharedKey)
|
||||
{
|
||||
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
|
||||
this.LogWarning("Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
|
||||
myTCPServer.SendData(clientIndex, b, b.Length);
|
||||
myTCPServer.Disconnect(clientIndex);
|
||||
return;
|
||||
|
|
@ -829,7 +826,7 @@ public class GenericTcpIpServer : Device
|
|||
byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
|
||||
myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
|
||||
OnServerClientReadyForCommunications(clientIndex);
|
||||
this.LogDebug("Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
|
||||
}
|
||||
|
||||
else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
|
||||
|
|
@ -837,7 +834,7 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Error Receiving data: {0}. Error: {1}", received, ex);
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
|
||||
}
|
||||
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
|
||||
myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
|
||||
|
|
@ -918,7 +915,7 @@ public class GenericTcpIpServer : Device
|
|||
if (MonitorClient != null)
|
||||
MonitorClient.Disconnect();
|
||||
|
||||
this.LogInformation("Program stopping. Closing server");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
|
||||
KillServer();
|
||||
}
|
||||
}
|
||||
|
|
@ -944,9 +941,7 @@ public class GenericTcpIpServer : Device
|
|||
{
|
||||
return;
|
||||
}
|
||||
MonitorClientTimer = new Timer(60000) { AutoReset = false };
|
||||
MonitorClientTimer.Elapsed += (s, e) => RunMonitorClient();
|
||||
MonitorClientTimer.Start();
|
||||
MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -961,7 +956,7 @@ public class GenericTcpIpServer : Device
|
|||
//MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
|
||||
MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
|
||||
|
||||
this.LogDebug("Starting monitor check");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
|
||||
|
||||
MonitorClient.Connect();
|
||||
// From here MonitorCLient either connects or hangs, MonitorClient will call back
|
||||
|
|
@ -988,7 +983,7 @@ public class GenericTcpIpServer : Device
|
|||
{
|
||||
if (args.IsReady)
|
||||
{
|
||||
this.LogInformation("Monitor client connection success. Disconnecting in 2s");
|
||||
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s");
|
||||
MonitorClientTimer.Stop();
|
||||
MonitorClientTimer = null;
|
||||
MonitorClientFailureCount = 0;
|
||||
|
|
@ -1009,13 +1004,13 @@ public class GenericTcpIpServer : Device
|
|||
StopMonitorClient();
|
||||
if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
|
||||
{
|
||||
this.LogWarning("Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}",
|
||||
MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
|
||||
StartMonitorClient();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LogError(
|
||||
Debug.Console(2, this, Debug.ErrorLogLevel.Error,
|
||||
"\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************",
|
||||
MonitorClientMaxFailureCount);
|
||||
|
||||
|
|
@ -1026,4 +1021,5 @@ public class GenericTcpIpServer : Device
|
|||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
463
src/PepperDash.Core/Comm/GenericUdpClient.cs
Normal file
463
src/PepperDash.Core/Comm/GenericUdpClient.cs
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using ThreadingTimeout = System.Threading.Timeout;
|
||||
using NetSocketException = System.Net.Sockets.SocketException;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// A class to handle basic UDP communications to a remote endpoint
|
||||
/// </summary>
|
||||
public class GenericUdpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
|
||||
{
|
||||
private const string SplusKey = "Uninitialized UdpClient";
|
||||
|
||||
private readonly object stateLock = new object();
|
||||
private readonly Timer reconnectTimer;
|
||||
|
||||
private UdpClient client;
|
||||
private CancellationTokenSource receiveCancellationTokenSource;
|
||||
private bool connectEnabled;
|
||||
private bool connectionRefusedLogged;
|
||||
private SocketStatus clientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||
|
||||
/// <summary>
|
||||
/// Object to enable stream debugging
|
||||
/// </summary>
|
||||
public CommunicationStreamDebugging StreamDebugging { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fires when data is received from the remote endpoint and returns it as a byte array
|
||||
/// </summary>
|
||||
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when data is received from the remote endpoint and returns it as text
|
||||
/// </summary>
|
||||
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the socket status changes
|
||||
/// </summary>
|
||||
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
|
||||
|
||||
/// <summary>
|
||||
/// Address of remote endpoint
|
||||
/// </summary>
|
||||
public string Hostname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Port on remote endpoint
|
||||
/// </summary>
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Another S+ helper because large port numbers can be treated as signed ints
|
||||
/// </summary>
|
||||
public ushort UPort
|
||||
{
|
||||
get { return Convert.ToUInt16(Port); }
|
||||
set { Port = Convert.ToInt32(value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defaults to 2000
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when the local socket is created and associated with the configured remote endpoint
|
||||
/// </summary>
|
||||
public bool IsConnected
|
||||
{
|
||||
get { return ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S+ helper for IsConnected
|
||||
/// </summary>
|
||||
public ushort UIsConnected
|
||||
{
|
||||
get { return (ushort)(IsConnected ? 1 : 0); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current socket status of the client
|
||||
/// </summary>
|
||||
public SocketStatus ClientStatus
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
return clientStatus;
|
||||
}
|
||||
}
|
||||
private set
|
||||
{
|
||||
var shouldFireEvent = false;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (clientStatus != value)
|
||||
{
|
||||
clientStatus = value;
|
||||
shouldFireEvent = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldFireEvent)
|
||||
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ushort representation of client status
|
||||
/// </summary>
|
||||
public ushort UStatus
|
||||
{
|
||||
get { return (ushort)ClientStatus; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AutoReconnect
|
||||
/// </summary>
|
||||
public bool AutoReconnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// S+ helper for AutoReconnect
|
||||
/// </summary>
|
||||
public ushort UAutoReconnect
|
||||
{
|
||||
get { return (ushort)(AutoReconnect ? 1 : 0); }
|
||||
set { AutoReconnect = value == 1; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
|
||||
/// </summary>
|
||||
public int AutoReconnectIntervalMs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public GenericUdpClient(string key, string address, int port, int bufferSize)
|
||||
: base(key)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
Hostname = address;
|
||||
Port = port;
|
||||
BufferSize = bufferSize;
|
||||
|
||||
reconnectTimer = new Timer(o =>
|
||||
{
|
||||
if (connectEnabled)
|
||||
Connect();
|
||||
}, null, ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for S+
|
||||
/// </summary>
|
||||
public GenericUdpClient()
|
||||
: base(SplusKey)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
|
||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||
AutoReconnectIntervalMs = 5000;
|
||||
BufferSize = 2000;
|
||||
|
||||
reconnectTimer = new Timer(o =>
|
||||
{
|
||||
if (connectEnabled)
|
||||
Connect();
|
||||
}, null, ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
Debug.Console(1, this, "Program stopping. Closing connection");
|
||||
Deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivate method
|
||||
/// </summary>
|
||||
public override bool Deactivate()
|
||||
{
|
||||
Disconnect();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': No address set", Key);
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': Invalid port", Key);
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||
return;
|
||||
}
|
||||
|
||||
var hostname = Hostname;
|
||||
var port = Port;
|
||||
var bufferSize = BufferSize;
|
||||
UdpClient newClient = null;
|
||||
CancellationTokenSource newReceiveCancellationTokenSource = null;
|
||||
CancellationToken startReceiveToken = default(CancellationToken);
|
||||
var shouldStartReceive = false;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
connectEnabled = true;
|
||||
|
||||
if (client != null)
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
newReceiveCancellationTokenSource = new CancellationTokenSource();
|
||||
newClient = new UdpClient();
|
||||
newClient.Client.ReceiveBufferSize = bufferSize;
|
||||
newClient.Client.SendBufferSize = bufferSize;
|
||||
newClient.Connect(hostname, port);
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (!connectEnabled || client != null)
|
||||
{
|
||||
newClient.Close();
|
||||
newReceiveCancellationTokenSource.Cancel();
|
||||
newReceiveCancellationTokenSource.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
receiveCancellationTokenSource = newReceiveCancellationTokenSource;
|
||||
client = newClient;
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
|
||||
reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||
startReceiveToken = receiveCancellationTokenSource.Token;
|
||||
shouldStartReceive = true;
|
||||
}
|
||||
|
||||
if (shouldStartReceive)
|
||||
StartReceive(startReceiveToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Error connecting UDP client {0}", this, Key);
|
||||
|
||||
if (newClient != null)
|
||||
newClient.Close();
|
||||
|
||||
if (newReceiveCancellationTokenSource != null)
|
||||
{
|
||||
newReceiveCancellationTokenSource.Cancel();
|
||||
newReceiveCancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (connectEnabled && client == null)
|
||||
{
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
connectEnabled = false;
|
||||
reconnectTimer.Change(ThreadingTimeout.Infinite, ThreadingTimeout.Infinite);
|
||||
CleanupClient();
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SendText method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
this.PrintSentText(text);
|
||||
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
SendBytes(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SendBytes method
|
||||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
this.PrintSentBytes(bytes);
|
||||
|
||||
if (!IsConnected || client == null)
|
||||
Connect();
|
||||
|
||||
var udpClient = client;
|
||||
if (!IsConnected || udpClient == null)
|
||||
{
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpClient '{0}': Cannot send bytes because the client is not connected", Key);
|
||||
return;
|
||||
}
|
||||
|
||||
udpClient.Send(bytes, bytes.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Error sending UDP bytes for {0}", this, Key);
|
||||
HandleDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartReceive(CancellationToken token)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var udpClient = client;
|
||||
if (udpClient == null)
|
||||
return;
|
||||
|
||||
var result = await udpClient.ReceiveAsync().ConfigureAwait(false);
|
||||
var bytes = result.Buffer;
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
continue;
|
||||
|
||||
connectionRefusedLogged = false;
|
||||
|
||||
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
|
||||
|
||||
this.PrintReceivedBytes(bytes);
|
||||
this.PrintReceivedText(text);
|
||||
|
||||
BytesReceived?.Invoke(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
TextReceived?.Invoke(this, new GenericCommMethodReceiveTextArgs(text));
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (NetSocketException ex)
|
||||
{
|
||||
if (ex.SocketErrorCode == SocketError.ConnectionRefused)
|
||||
{
|
||||
if (!connectionRefusedLogged)
|
||||
{
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning,
|
||||
"GenericUdpClient '{0}': Remote endpoint refused UDP traffic or is no longer listening",
|
||||
Key);
|
||||
connectionRefusedLogged = true;
|
||||
}
|
||||
|
||||
HandleDisconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogMessage(ex, "UDP receive error for {0}", this, Key);
|
||||
|
||||
if (AutoReconnect)
|
||||
{
|
||||
HandleDisconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Unexpected UDP receive error for {0}", this, Key);
|
||||
|
||||
if (AutoReconnect)
|
||||
{
|
||||
HandleDisconnected();
|
||||
return;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
|
||||
private void HandleDisconnected()
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
CleanupClient();
|
||||
ClientStatus = SocketStatus.SOCKET_STATUS_NO_CONNECT;
|
||||
StartReconnectTimer();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartReconnectTimer()
|
||||
{
|
||||
if (AutoReconnect && connectEnabled)
|
||||
reconnectTimer.Change(AutoReconnectIntervalMs, ThreadingTimeout.Infinite);
|
||||
}
|
||||
|
||||
private void CleanupClient()
|
||||
{
|
||||
if (receiveCancellationTokenSource != null)
|
||||
{
|
||||
receiveCancellationTokenSource.Cancel();
|
||||
receiveCancellationTokenSource.Dispose();
|
||||
receiveCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
if (client != null)
|
||||
{
|
||||
client.Close();
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,20 @@
|
|||
|
||||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core.Logging;
|
||||
using Required = NewtonsoftJson::Newtonsoft.Json.Required;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generic UDP Server device
|
||||
/// </summary>
|
||||
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic UDP Server device
|
||||
/// </summary>
|
||||
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
||||
{
|
||||
private const string SplusKey = "Uninitialized Udp Server";
|
||||
/// <summary>
|
||||
/// Object to enable stream debugging
|
||||
|
|
@ -60,7 +57,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a GenericUdpReceiveTextExtraArgs
|
||||
///
|
||||
/// </summary>
|
||||
public ushort UStatus
|
||||
{
|
||||
|
|
@ -134,14 +131,14 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
/// <param name="key"></param>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="buffefSize"></param>
|
||||
public GenericUdpServer(string key, string address, int port, int buffefSize)
|
||||
/// <param name="bufferSize"></param>
|
||||
public GenericUdpServer(string key, string address, int port, int bufferSize)
|
||||
: base(key)
|
||||
{
|
||||
StreamDebugging = new CommunicationStreamDebugging(key);
|
||||
Hostname = address;
|
||||
Port = port;
|
||||
BufferSize = buffefSize;
|
||||
BufferSize = bufferSize;
|
||||
|
||||
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
|
||||
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
|
||||
|
|
@ -153,6 +150,9 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
/// <param name="key"></param>
|
||||
/// <param name="address"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key, string address, ushort port)
|
||||
{
|
||||
Key = key;
|
||||
|
|
@ -183,36 +183,50 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
if (programEventType != eProgramStatusEventType.Stopping)
|
||||
return;
|
||||
|
||||
this.LogInformation("Program stopping. Disabling Server");
|
||||
Debug.Console(1, this, "Program stopping. Disabling Server");
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the UDP Server
|
||||
/// Connect method
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
if (Server == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var address = IPAddress.Parse(Hostname);
|
||||
|
||||
Server = new UDPServer(address, Port, BufferSize);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError("Error parsing IP Address '{ipAddress}': message: {message}", Hostname, ex.Message);
|
||||
this.LogInformation("Creating UDPServer with default buffersize");
|
||||
|
||||
Server = new UDPServer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Hostname))
|
||||
{
|
||||
this.LogWarning("GenericUdpServer '{0}': No address set", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key);
|
||||
return;
|
||||
}
|
||||
if (Port < 1 || Port > 65535)
|
||||
{
|
||||
{
|
||||
this.LogWarning("GenericUdpServer '{0}': Invalid port", Key);
|
||||
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var status = Server.EnableUDPServer(Hostname, Port);
|
||||
|
||||
this.LogVerbose("SocketErrorCode: {0}", status);
|
||||
Debug.Console(2, this, "SocketErrorCode: {0}", status);
|
||||
if (status == SocketErrorCodes.SOCKET_OK)
|
||||
IsConnected = true;
|
||||
|
||||
|
|
@ -225,11 +239,11 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disabled the UDP Server
|
||||
/// Disconnect method
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
if(Server != null)
|
||||
if (Server != null)
|
||||
Server.DisableUDPServer();
|
||||
|
||||
IsConnected = false;
|
||||
|
|
@ -247,7 +261,7 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
/// <param name="numBytes"></param>
|
||||
void Receive(UDPServer server, int numBytes)
|
||||
{
|
||||
this.LogVerbose("Received {0} bytes", numBytes);
|
||||
Debug.Console(2, this, "Received {0} bytes", numBytes);
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -263,21 +277,17 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
if (dataRecivedExtra != null)
|
||||
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
|
||||
|
||||
this.LogVerbose("Bytes: {0}", bytes.ToString());
|
||||
Debug.Console(2, this, "Bytes: {0}", bytes.ToString());
|
||||
var bytesHandler = BytesReceived;
|
||||
if (bytesHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
{
|
||||
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
|
||||
}
|
||||
this.PrintReceivedBytes(bytes);
|
||||
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
|
||||
}
|
||||
var textHandler = TextReceived;
|
||||
if (textHandler != null)
|
||||
{
|
||||
if (StreamDebugging.RxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
|
||||
this.PrintReceivedText(str);
|
||||
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
|
||||
}
|
||||
}
|
||||
|
|
@ -295,14 +305,16 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
/// General send method
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <summary>
|
||||
/// SendText method
|
||||
/// </summary>
|
||||
public void SendText(string text)
|
||||
{
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
|
||||
if (IsConnected && Server != null)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogVerbose("Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
|
||||
this.PrintSentText(text);
|
||||
|
||||
Server.SendData(bytes, bytes.Length);
|
||||
}
|
||||
|
|
@ -312,20 +324,22 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <summary>
|
||||
/// SendBytes method
|
||||
/// </summary>
|
||||
public void SendBytes(byte[] bytes)
|
||||
{
|
||||
if (StreamDebugging.TxStreamDebuggingIsEnabled)
|
||||
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
|
||||
this.PrintSentBytes(bytes);
|
||||
|
||||
if (IsConnected && Server != null)
|
||||
Server.SendData(bytes, bytes.Length);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a GenericUdpReceiveTextExtraArgs
|
||||
/// </summary>
|
||||
public class GenericUdpReceiveTextExtraArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -366,11 +380,11 @@ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
|
|||
public GenericUdpReceiveTextExtraArgs() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class UdpServerPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class UdpServerPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
|
@ -395,4 +409,5 @@ public class UdpServerPropertiesConfig
|
|||
{
|
||||
BufferSize = 32768;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
extern alias NewtonsoftJson;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat
|
||||
/// </summary>
|
||||
public class TcpClientConfigObject
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a TcpClientConfigObject
|
||||
/// </summary>
|
||||
public class TcpClientConfigObject
|
||||
{
|
||||
/// <summary>
|
||||
/// TcpSsh Properties
|
||||
/// </summary>
|
||||
|
|
@ -57,4 +55,5 @@ public class TcpClientConfigObject
|
|||
/// </summary>
|
||||
[JsonProperty("receiveQueueSize")]
|
||||
public int ReceiveQueueSize { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
|
||||
/// </summary>
|
||||
public class TcpServerConfigObject
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
|
||||
/// </summary>
|
||||
public class TcpServerConfigObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Uique key
|
||||
/// </summary>
|
||||
|
|
@ -56,4 +56,5 @@ public class TcpServerConfigObject
|
|||
/// Receive Queue size must be greater than 20 or defaults to 20
|
||||
/// </summary>
|
||||
public int ReceiveQueueSize { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,13 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Crestron Control Methods for a comm object
|
||||
/// </summary>
|
||||
public enum eControlMethod
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Crestron Control Methods for a comm object
|
||||
/// </summary>
|
||||
public enum eControlMethod
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
|
@ -18,69 +18,75 @@ public enum eControlMethod
|
|||
/// <summary>
|
||||
/// RS232/422/485
|
||||
/// </summary>
|
||||
Com,
|
||||
Com = 1,
|
||||
/// <summary>
|
||||
/// Crestron IpId (most Crestron ethernet devices)
|
||||
/// </summary>
|
||||
IpId,
|
||||
IpId = 2,
|
||||
/// <summary>
|
||||
/// Crestron IpIdTcp (HD-MD series, etc.)
|
||||
/// </summary>
|
||||
IpidTcp,
|
||||
IpidTcp = 3,
|
||||
/// <summary>
|
||||
/// Crestron IR control
|
||||
/// </summary>
|
||||
IR,
|
||||
IR = 4,
|
||||
/// <summary>
|
||||
/// SSH client
|
||||
/// </summary>
|
||||
Ssh,
|
||||
Ssh = 5,
|
||||
/// <summary>
|
||||
/// TCP/IP client
|
||||
/// </summary>
|
||||
Tcpip,
|
||||
Tcpip = 6,
|
||||
/// <summary>
|
||||
/// Telnet
|
||||
/// </summary>
|
||||
Telnet,
|
||||
Telnet = 7,
|
||||
/// <summary>
|
||||
/// Crestnet device
|
||||
/// </summary>
|
||||
Cresnet,
|
||||
Cresnet = 8,
|
||||
/// <summary>
|
||||
/// CEC Control, via a DM HDMI port
|
||||
/// </summary>
|
||||
Cec,
|
||||
Cec = 9,
|
||||
/// <summary>
|
||||
/// UDP Server
|
||||
/// </summary>
|
||||
Udp,
|
||||
Udp = 10,
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client
|
||||
/// </summary>
|
||||
Http,
|
||||
Http = 11,
|
||||
/// <summary>
|
||||
/// HTTPS client
|
||||
/// </summary>
|
||||
Https,
|
||||
Https = 12,
|
||||
/// <summary>
|
||||
/// Websocket client
|
||||
/// </summary>
|
||||
Ws,
|
||||
Ws = 13,
|
||||
/// <summary>
|
||||
/// Secure Websocket client
|
||||
/// </summary>
|
||||
Wss,
|
||||
Wss = 14,
|
||||
/// <summary>
|
||||
/// Secure TCP/IP
|
||||
/// </summary>
|
||||
SecureTcpIp,
|
||||
SecureTcpIp = 15,
|
||||
/// <summary>
|
||||
/// Crestron COM bridge
|
||||
/// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction
|
||||
/// </summary>
|
||||
ComBridge,
|
||||
ComBridge = 16,
|
||||
/// <summary>
|
||||
/// Crestron Infinet EX device
|
||||
/// InfinetEX control
|
||||
/// </summary>
|
||||
InfinetEx
|
||||
InfinetEx = 17,
|
||||
/// <summary>
|
||||
/// UDP client
|
||||
/// </summary>
|
||||
UdpClient = 18,
|
||||
}
|
||||
}
|
||||
28
src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs
Normal file
28
src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The available settings for stream debugging
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum eStreamDebuggingSetting
|
||||
{
|
||||
/// <summary>
|
||||
/// Debug off
|
||||
/// </summary>
|
||||
Off = 0,
|
||||
/// <summary>
|
||||
/// Debug received data
|
||||
/// </summary>
|
||||
Rx = 1,
|
||||
/// <summary>
|
||||
/// Debug transmitted data
|
||||
/// </summary>
|
||||
Tx = 2,
|
||||
/// <summary>
|
||||
/// Debug both received and transmitted data
|
||||
/// </summary>
|
||||
Both = Rx | Tx
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,16 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronSockets;
|
||||
using JsonConverter = NewtonsoftJson::Newtonsoft.Json.JsonConverterAttribute;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using StringEnumConverter = NewtonsoftJson::Newtonsoft.Json.Converters.StringEnumConverter;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// An incoming communication stream
|
||||
/// </summary>
|
||||
public interface ICommunicationReceiver : IKeyed
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// An incoming communication stream
|
||||
/// </summary>
|
||||
public interface ICommunicationReceiver : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Notifies of bytes received
|
||||
/// </summary>
|
||||
|
|
@ -40,12 +33,12 @@ public interface ICommunicationReceiver : IKeyed
|
|||
/// Disconnect from the device
|
||||
/// </summary>
|
||||
void Disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extends <see cref="ICommunicationReceiver"/> with methods for sending text and bytes to a device.
|
||||
/// Defines the contract for IBasicCommunication
|
||||
/// </summary>
|
||||
public interface IBasicCommunication : ICommunicationReceiver
|
||||
public interface IBasicCommunication : ICommunicationReceiver
|
||||
{
|
||||
/// <summary>
|
||||
/// Send text to the device
|
||||
|
|
@ -60,25 +53,25 @@ public interface IBasicCommunication : ICommunicationReceiver
|
|||
void SendBytes(byte[] bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a device that implements IBasicCommunication and IStreamDebugging
|
||||
/// </summary>
|
||||
public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a device that implements IBasicCommunication and IStreamDebugging
|
||||
/// </summary>
|
||||
public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a device with stream debugging capablities
|
||||
/// </summary>
|
||||
public interface IStreamDebugging : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a device with stream debugging capablities
|
||||
/// </summary>
|
||||
public interface IStreamDebugging : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Object to enable stream debugging
|
||||
/// </summary>
|
||||
[JsonProperty("streamDebugging")]
|
||||
CommunicationStreamDebugging StreamDebugging { get; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
|
||||
|
|
@ -95,21 +88,21 @@ public interface IStreamDebugging : IKeyed
|
|||
/// The current socket status of the client
|
||||
/// </summary>
|
||||
[JsonProperty("clientStatus")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
SocketStatus ClientStatus { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a device that implements ISocketStatus and IStreamDebugging
|
||||
/// </summary>
|
||||
public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a device that implements ISocketStatus and IStreamDebugging
|
||||
/// </summary>
|
||||
public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a device that can automatically attempt to reconnect
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Describes a device that can automatically attempt to reconnect
|
||||
/// </summary>
|
||||
public interface IAutoReconnect
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -147,17 +140,17 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
|||
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
|
||||
|
||||
/// <summary>
|
||||
/// Event args for bytes received from a communication method
|
||||
///
|
||||
/// </summary>
|
||||
public class GenericCommMethodReceiveBytesArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The bytes received
|
||||
/// Gets or sets the Bytes
|
||||
/// </summary>
|
||||
public byte[] Bytes { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
public GenericCommMethodReceiveBytesArgs(byte[] bytes)
|
||||
|
|
@ -172,21 +165,20 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event args for text received
|
||||
///
|
||||
/// </summary>
|
||||
public class GenericCommMethodReceiveTextArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The text received
|
||||
///
|
||||
/// </summary>
|
||||
public string Text { get; private set; }
|
||||
/// <summary>
|
||||
/// The delimiter used to determine the end of a message, if applicable
|
||||
///
|
||||
/// </summary>
|
||||
public string Delimiter { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
public GenericCommMethodReceiveTextArgs(string text)
|
||||
|
|
@ -200,7 +192,7 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
|||
/// <param name="text"></param>
|
||||
/// <param name="delimiter"></param>
|
||||
public GenericCommMethodReceiveTextArgs(string text, string delimiter)
|
||||
:this(text)
|
||||
: this(text)
|
||||
{
|
||||
Delimiter = delimiter;
|
||||
}
|
||||
|
|
@ -210,42 +202,4 @@ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugg
|
|||
/// </summary>
|
||||
public GenericCommMethodReceiveTextArgs() { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to get escaped text for debugging communication streams
|
||||
/// </summary>
|
||||
public class ComTextHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets escaped text for a byte array
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetEscapedText(byte[] bytes)
|
||||
{
|
||||
return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets escaped text for a string
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetEscapedText(string text)
|
||||
{
|
||||
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
|
||||
return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets debug text for a string
|
||||
/// </summary>
|
||||
/// <param name="text"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetDebugText(string text)
|
||||
{
|
||||
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,19 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
|
||||
using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using JToken = NewtonsoftJson::Newtonsoft.Json.Linq.JToken;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Core.Config;
|
||||
namespace PepperDash.Core.Config
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Reads a Portal formatted config file
|
||||
/// </summary>
|
||||
|
||||
/// <summary>
|
||||
/// Reads a Portal formatted config file
|
||||
/// </summary>
|
||||
public class PortalConfigReader
|
||||
{
|
||||
const string template = "template";
|
||||
|
|
@ -58,12 +56,12 @@ namespace PepperDash.Core.Config;
|
|||
var merged = MergeConfigs(jsonObj);
|
||||
if (jsonObj[systemUrl] != null)
|
||||
{
|
||||
merged[systemUrl] = (string)jsonObj[systemUrl];
|
||||
merged[systemUrl] = jsonObj[systemUrl].Value<string>();
|
||||
}
|
||||
|
||||
if (jsonObj[templateUrl] != null)
|
||||
{
|
||||
merged[templateUrl] = (string)jsonObj[templateUrl];
|
||||
merged[templateUrl] = jsonObj[templateUrl].Value<string>();
|
||||
}
|
||||
|
||||
jsonObj = merged;
|
||||
|
|
@ -124,37 +122,38 @@ namespace PepperDash.Core.Config;
|
|||
Merge(template[destinationLists], system[destinationLists], destinationLists));
|
||||
|
||||
|
||||
if (system["cameraLists"] == null)
|
||||
merged.Add("cameraLists", template["cameraLists"]);
|
||||
if (system[cameraLists] == null)
|
||||
merged.Add(cameraLists, template[cameraLists]);
|
||||
else
|
||||
merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists"));
|
||||
merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists));
|
||||
|
||||
if (system["audioControlPointLists"] == null)
|
||||
merged.Add("audioControlPointLists", template["audioControlPointLists"]);
|
||||
if (system[audioControlPointLists] == null)
|
||||
merged.Add(audioControlPointLists, template[audioControlPointLists]);
|
||||
else
|
||||
merged.Add("audioControlPointLists",
|
||||
Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists"));
|
||||
merged.Add(audioControlPointLists,
|
||||
Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists));
|
||||
|
||||
|
||||
// Template tie lines take precedence. Config tool doesn't do them at system
|
||||
// level anyway...
|
||||
if (template["tieLines"] != null)
|
||||
merged.Add("tieLines", template["tieLines"]);
|
||||
else if (system["tieLines"] != null)
|
||||
merged.Add("tieLines", system["tieLines"]);
|
||||
if (template[tieLines] != null)
|
||||
merged.Add(tieLines, template[tieLines]);
|
||||
else if (system[tieLines] != null)
|
||||
merged.Add(tieLines, system[tieLines]);
|
||||
else
|
||||
merged.Add(tieLines, new JArray());
|
||||
|
||||
if (template["joinMaps"] != null)
|
||||
merged.Add("joinMaps", template["joinMaps"]);
|
||||
if (template[joinMaps] != null)
|
||||
merged.Add(joinMaps, template[joinMaps]);
|
||||
else
|
||||
merged.Add("joinMaps", new JObject());
|
||||
merged.Add(joinMaps, new JObject());
|
||||
|
||||
if (system[global] != null)
|
||||
merged.Add(global, Merge(template[global], system[global], global));
|
||||
else
|
||||
merged.Add(global, template[global]);
|
||||
|
||||
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
|
@ -255,3 +254,4 @@ namespace PepperDash.Core.Config;
|
|||
return o1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,18 +4,28 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
public class EncodingHelper
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a EncodingHelper
|
||||
/// </summary>
|
||||
public class EncodingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// ConvertUtf8ToAscii method
|
||||
/// </summary>
|
||||
public static string ConvertUtf8ToAscii(string utf8String)
|
||||
{
|
||||
return Encoding.ASCII.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ConvertUtf8ToUtf16 method
|
||||
/// </summary>
|
||||
public static string ConvertUtf8ToUtf16(string utf8String)
|
||||
{
|
||||
return Encoding.Unicode.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,35 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Unique key interface to require a unique key for the class
|
||||
/// </summary>
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique key interface to require a unique key for the class
|
||||
/// </summary>
|
||||
public interface IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique key associated with the object.
|
||||
/// Unique Key
|
||||
/// </summary>
|
||||
[JsonProperty("key")]
|
||||
string Key { get; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Named Keyed device interface. Forces the device to have a Unique Key and a name.
|
||||
/// </summary>
|
||||
public interface IKeyName : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name associated with the current object.
|
||||
/// Named Keyed device interface. Forces the device to have a Unique Key and a name.
|
||||
/// </summary>
|
||||
public interface IKeyName : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Isn't it obvious :)
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
string Name { get; }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,14 +2,14 @@
|
|||
using System.Collections.Generic;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
//*********************************************************************************************************
|
||||
/// <summary>
|
||||
/// Represents a Device
|
||||
/// </summary>
|
||||
public class Device : IKeyName
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
//*********************************************************************************************************
|
||||
/// <summary>
|
||||
/// Represents a Device
|
||||
/// </summary>
|
||||
public class Device : IKeyName
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Unique Key
|
||||
|
|
@ -52,6 +52,7 @@ public class Device : IKeyName
|
|||
public Device(string key, string name) : this(key)
|
||||
{
|
||||
Name = name;
|
||||
|
||||
}
|
||||
|
||||
//public Device(DeviceConfig config)
|
||||
|
|
@ -64,7 +65,7 @@ public class Device : IKeyName
|
|||
/// Adds a pre activation action
|
||||
/// </summary>
|
||||
/// <param name="act"></param>
|
||||
protected void AddPreActivationAction(Action act)
|
||||
public void AddPreActivationAction(Action act)
|
||||
{
|
||||
if (_PreActivationActions == null)
|
||||
_PreActivationActions = new List<Action>();
|
||||
|
|
@ -78,7 +79,7 @@ public class Device : IKeyName
|
|||
/// <summary>
|
||||
/// AddPostActivationAction method
|
||||
/// </summary>
|
||||
protected void AddPostActivationAction(Action act)
|
||||
public void AddPostActivationAction(Action act)
|
||||
{
|
||||
if (_PostActivationActions == null)
|
||||
_PostActivationActions = new List<Action>();
|
||||
|
|
@ -145,7 +146,7 @@ public class Device : IKeyName
|
|||
/// <summary>
|
||||
/// CustomActivate method
|
||||
/// </summary>
|
||||
protected virtual bool CustomActivate() { return true; }
|
||||
public virtual bool CustomActivate() { return true; }
|
||||
|
||||
/// <summary>
|
||||
/// Call to deactivate device - unlink events, etc. Overriding classes do not
|
||||
|
|
@ -157,7 +158,7 @@ public class Device : IKeyName
|
|||
/// <summary>
|
||||
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
|
||||
/// </summary>
|
||||
protected virtual void Initialize()
|
||||
public virtual void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -177,8 +178,12 @@ public class Device : IKeyName
|
|||
/// <remarks>The returned string is formatted as "{Key} - {Name}". If the <c>Name</c> property is
|
||||
/// null or empty, "---" is used in place of the name.</remarks>
|
||||
/// <returns>A string that represents the object, containing the key and name in the format "{Key} - {Name}".</returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", Key, string.IsNullOrEmpty(Name) ? "---" : Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using Crestron.SimplSharp;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Crestron.SimplSharp;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an EthernetHelper.
|
||||
/// </summary>
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a EthernetHelper
|
||||
/// </summary>
|
||||
public class EthernetHelper
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -116,3 +114,4 @@ namespace PepperDash.Core;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Bool change event args
|
||||
/// </summary>
|
||||
|
|
@ -169,3 +169,4 @@ namespace PepperDash.Core;
|
|||
Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.GenericRESTfulCommunications
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants
|
||||
/// </summary>
|
||||
public class GenericRESTfulConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic boolean change
|
||||
/// </summary>
|
||||
public const ushort BoolValueChange = 1;
|
||||
/// <summary>
|
||||
/// Generic Ushort change
|
||||
/// </summary>
|
||||
public const ushort UshrtValueChange = 101;
|
||||
/// <summary>
|
||||
/// Response Code Ushort change
|
||||
/// </summary>
|
||||
public const ushort ResponseCodeChange = 102;
|
||||
/// <summary>
|
||||
/// Generic String chagne
|
||||
/// </summary>
|
||||
public const ushort StringValueChange = 201;
|
||||
/// <summary>
|
||||
/// Response string change
|
||||
/// </summary>
|
||||
public const ushort ResponseStringChange = 202;
|
||||
/// <summary>
|
||||
/// Error string change
|
||||
/// </summary>
|
||||
public const ushort ErrorStringChange = 203;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.Net.Http;
|
||||
using Crestron.SimplSharp.Net.Https;
|
||||
|
||||
namespace PepperDash.Core.GenericRESTfulCommunications
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic RESTful communication class
|
||||
/// </summary>
|
||||
public class GenericRESTfulClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Boolean event handler
|
||||
/// </summary>
|
||||
public event EventHandler<BoolChangeEventArgs> BoolChange;
|
||||
/// <summary>
|
||||
/// Ushort event handler
|
||||
/// </summary>
|
||||
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
|
||||
/// <summary>
|
||||
/// String event handler
|
||||
/// </summary>
|
||||
public event EventHandler<StringChangeEventArgs> StringChange;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public GenericRESTfulClient()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic RESTful submit request
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="requestType"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <param name="contentType"></param>
|
||||
public void SubmitRequest(string url, ushort port, ushort requestType, string contentType, string username, string password)
|
||||
{
|
||||
if (url.StartsWith("https:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SubmitRequestHttps(url, port, requestType, contentType, username, password);
|
||||
}
|
||||
else if (url.StartsWith("http:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SubmitRequestHttp(url, port, requestType, contentType, username, password);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnStringChange(string.Format("Invalid URL {0}", url), 0, GenericRESTfulConstants.ErrorStringChange);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Private HTTP submit request
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="requestType"></param>
|
||||
/// <param name="contentType"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="password"></param>
|
||||
private void SubmitRequestHttp(string url, ushort port, ushort requestType, string contentType, string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClient client = new HttpClient();
|
||||
HttpClientRequest request = new HttpClientRequest();
|
||||
HttpClientResponse response;
|
||||
|
||||
client.KeepAlive = false;
|
||||
|
||||
if(port >= 1 || port <= 65535)
|
||||
client.Port = port;
|
||||
else
|
||||
client.Port = 80;
|
||||
|
||||
var authorization = "";
|
||||
if (!string.IsNullOrEmpty(username))
|
||||
authorization = EncodeBase64(username, password);
|
||||
|
||||
if (!string.IsNullOrEmpty(authorization))
|
||||
request.Header.SetHeaderValue("Authorization", authorization);
|
||||
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
request.Header.ContentType = contentType;
|
||||
|
||||
request.Url.Parse(url);
|
||||
request.RequestType = (Crestron.SimplSharp.Net.Http.RequestType)requestType;
|
||||
|
||||
response = client.Dispatch(request);
|
||||
|
||||
CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString()));
|
||||
|
||||
if (!string.IsNullOrEmpty(response.ContentString.ToString()))
|
||||
OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange);
|
||||
|
||||
if (response.Code > 0)
|
||||
OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//var msg = string.Format("SubmitRequestHttp({0}, {1}, {2}) failed:{3}", url, port, requestType, e.Message);
|
||||
//CrestronConsole.PrintLine(msg);
|
||||
//ErrorLog.Error(msg);
|
||||
|
||||
CrestronConsole.PrintLine(e.Message);
|
||||
OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Private HTTPS submit request
|
||||
/// </summary>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="port"></param>
|
||||
/// <param name="requestType"></param>
|
||||
/// <param name="contentType"></param>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="password"></param>
|
||||
private void SubmitRequestHttps(string url, ushort port, ushort requestType, string contentType, string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpsClient client = new HttpsClient();
|
||||
HttpsClientRequest request = new HttpsClientRequest();
|
||||
HttpsClientResponse response;
|
||||
|
||||
client.KeepAlive = false;
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
|
||||
var authorization = "";
|
||||
if (!string.IsNullOrEmpty(username))
|
||||
authorization = EncodeBase64(username, password);
|
||||
|
||||
if (!string.IsNullOrEmpty(authorization))
|
||||
request.Header.SetHeaderValue("Authorization", authorization);
|
||||
|
||||
if (!string.IsNullOrEmpty(contentType))
|
||||
request.Header.ContentType = contentType;
|
||||
|
||||
request.Url.Parse(url);
|
||||
request.RequestType = (Crestron.SimplSharp.Net.Https.RequestType)requestType;
|
||||
|
||||
response = client.Dispatch(request);
|
||||
|
||||
CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString()));
|
||||
|
||||
if(!string.IsNullOrEmpty(response.ContentString.ToString()))
|
||||
OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange);
|
||||
|
||||
if(response.Code > 0)
|
||||
OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
//var msg = string.Format("SubmitRequestHttps({0}, {1}, {2}, {3}, {4}) failed:{5}", url, port, requestType, username, password, e.Message);
|
||||
//CrestronConsole.PrintLine(msg);
|
||||
//ErrorLog.Error(msg);
|
||||
|
||||
CrestronConsole.PrintLine(e.Message);
|
||||
OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Private method to encode username and password to Base64 string
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns>authorization</returns>
|
||||
private string EncodeBase64(string username, string password)
|
||||
{
|
||||
var authorization = "";
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrEmpty(username))
|
||||
{
|
||||
string base64String = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(string.Format("{0}:{1}", username, password)));
|
||||
authorization = string.Format("Basic {0}", base64String);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var msg = string.Format("EncodeBase64({0}, {1}) failed:\r{2}", username, password, e);
|
||||
CrestronConsole.PrintLine(msg);
|
||||
ErrorLog.Error(msg);
|
||||
return "" ;
|
||||
}
|
||||
|
||||
return authorization;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protected method to handle boolean change events
|
||||
/// </summary>
|
||||
/// <param name="state"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="type"></param>
|
||||
protected void OnBoolChange(bool state, ushort index, ushort type)
|
||||
{
|
||||
var handler = BoolChange;
|
||||
if (handler != null)
|
||||
{
|
||||
var args = new BoolChangeEventArgs(state, type);
|
||||
args.Index = index;
|
||||
BoolChange(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protected mehtod to handle ushort change events
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="type"></param>
|
||||
protected void OnUshrtChange(ushort value, ushort index, ushort type)
|
||||
{
|
||||
var handler = UshrtChange;
|
||||
if (handler != null)
|
||||
{
|
||||
var args = new UshrtChangeEventArgs(value, type);
|
||||
args.Index = index;
|
||||
UshrtChange(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protected method to handle string change events
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="type"></param>
|
||||
protected void OnStringChange(string value, ushort index, ushort type)
|
||||
{
|
||||
var handler = StringChange;
|
||||
if (handler != null)
|
||||
{
|
||||
var args = new StringChangeEventArgs(value, type);
|
||||
args.Index = index;
|
||||
StringChange(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.JsonStandardObjects;
|
||||
|
||||
namespace PepperDash.Core.JsonStandardObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants for simpl modules
|
||||
/// </summary>
|
||||
|
|
@ -74,3 +74,4 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using Crestron.SimplSharp;
|
|||
using PepperDash.Core.JsonToSimpl;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Core.JsonStandardObjects;
|
||||
|
||||
namespace PepperDash.Core.JsonStandardObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Device class
|
||||
/// </summary>
|
||||
|
|
@ -183,3 +183,4 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
|
||||
#endregion EventHandler Helpers
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.JsonStandardObjects;
|
||||
|
||||
namespace PepperDash.Core.JsonStandardObjects
|
||||
{
|
||||
/*
|
||||
Convert JSON snippt to C#: http://json2csharp.com/#
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
public class ComParamsConfig
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the baudRate
|
||||
/// </summary>
|
||||
public int baudRate { get; set; }
|
||||
/// <summary>
|
||||
|
|
@ -87,11 +87,11 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
|
||||
// convert properties for simpl
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplBaudRate
|
||||
/// </summary>
|
||||
public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } }
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplDataBits
|
||||
/// </summary>
|
||||
public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } }
|
||||
/// <summary>
|
||||
|
|
@ -144,11 +144,11 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
|
||||
// convert properties for simpl
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplPort
|
||||
/// </summary>
|
||||
public ushort simplPort { get { return Convert.ToUInt16(port); } }
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplAutoReconnect
|
||||
/// </summary>
|
||||
public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } }
|
||||
/// <summary>
|
||||
|
|
@ -193,7 +193,7 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
|
||||
// convert properties for simpl
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplControlPortNumber
|
||||
/// </summary>
|
||||
public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } }
|
||||
|
||||
|
|
@ -227,11 +227,11 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
|
||||
// convert properties for simpl
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplDeviceId
|
||||
/// </summary>
|
||||
public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } }
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the simplEnabled
|
||||
/// </summary>
|
||||
public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } }
|
||||
|
||||
|
|
@ -254,3 +254,4 @@ namespace PepperDash.Core.JsonStandardObjects;
|
|||
/// </summary>
|
||||
public List<DeviceConfig> devices { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants for Simpl modules
|
||||
/// </summary>
|
||||
|
|
@ -89,7 +89,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
public class SPlusValueWrapper
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the ValueType
|
||||
/// </summary>
|
||||
public SPlusType ValueType { get; private set; }
|
||||
/// <summary>
|
||||
|
|
@ -140,3 +140,4 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
String
|
||||
}
|
||||
}
|
||||
|
|
@ -7,11 +7,11 @@ using Serilog.Events;
|
|||
|
||||
//using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// The global class to manage all the instances of JsonToSimplMaster
|
||||
/// </summary>
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// The global class to manage all the instances of JsonToSimplMaster
|
||||
/// </summary>
|
||||
public class J2SGlobal
|
||||
{
|
||||
static List<JsonToSimplMaster> Masters = new List<JsonToSimplMaster>();
|
||||
|
|
@ -23,6 +23,9 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="master">New master to add</param>
|
||||
///
|
||||
/// <summary>
|
||||
/// AddMaster method
|
||||
/// </summary>
|
||||
public static void AddMaster(JsonToSimplMaster master)
|
||||
{
|
||||
if (master == null)
|
||||
|
|
@ -57,3 +60,4 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// Used to interact with an array of values with the S+ modules
|
||||
/// </summary>
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a JsonToSimplArrayLookupChild
|
||||
/// </summary>
|
||||
public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -79,8 +77,9 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process all values
|
||||
/// ProcessAll method
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public override void ProcessAll()
|
||||
{
|
||||
if (FindInArray())
|
||||
|
|
@ -130,7 +129,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
var item = array.FirstOrDefault(o =>
|
||||
{
|
||||
var prop = o[SearchPropertyName];
|
||||
return prop != null && ((string)prop)
|
||||
return prop != null && prop.Value<string>()
|
||||
.Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
if (item == null)
|
||||
|
|
@ -161,3 +160,4 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PepperDash.Core.Logging;
|
||||
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for JSON objects
|
||||
/// </summary>
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for JSON objects
|
||||
/// </summary>
|
||||
public abstract class JsonToSimplChildObjectBase : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -37,7 +34,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
public SPlusValuesDelegate SetAllPathsDelegate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier for instance
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
public string Key { get; protected set; }
|
||||
|
||||
|
|
@ -53,7 +50,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
public string PathSuffix { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the instance is linked to an object
|
||||
/// Gets or sets the LinkedToObject
|
||||
/// </summary>
|
||||
public bool LinkedToObject { get; protected set; }
|
||||
|
||||
|
|
@ -92,13 +89,16 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
if (Master != null)
|
||||
Master.AddChild(this);
|
||||
else
|
||||
this.LogWarning("JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
|
||||
Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the path prefix for the object
|
||||
/// </summary>
|
||||
/// <param name="pathPrefix"></param>
|
||||
/// <summary>
|
||||
/// SetPathPrefix method
|
||||
/// </summary>
|
||||
public void SetPathPrefix(string pathPrefix)
|
||||
{
|
||||
PathPrefix = pathPrefix;
|
||||
|
|
@ -108,7 +108,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
public void SetBoolPath(ushort index, string path)
|
||||
{
|
||||
this.LogDebug("JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
|
||||
Debug.Console(1, "JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
|
||||
if (path == null || path.Trim() == string.Empty) return;
|
||||
BoolPaths[index] = path;
|
||||
}
|
||||
|
|
@ -118,7 +118,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
public void SetUshortPath(ushort index, string path)
|
||||
{
|
||||
this.LogDebug("JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
|
||||
Debug.Console(1, "JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
|
||||
if (path == null || path.Trim() == string.Empty) return;
|
||||
UshortPaths[index] = path;
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
public void SetStringPath(ushort index, string path)
|
||||
{
|
||||
this.LogDebug("JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
|
||||
Debug.Console(1, "JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
|
||||
if (path == null || path.Trim() == string.Empty) return;
|
||||
StringPaths[index] = path;
|
||||
}
|
||||
|
|
@ -141,13 +141,13 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
{
|
||||
if (!LinkedToObject)
|
||||
{
|
||||
this.LogDebug("Not linked to object in file. Skipping");
|
||||
Debug.Console(1, this, "Not linked to object in file. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetAllPathsDelegate == null)
|
||||
{
|
||||
this.LogDebug("No SetAllPathsDelegate set. Ignoring ProcessAll");
|
||||
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring ProcessAll");
|
||||
return;
|
||||
}
|
||||
SetAllPathsDelegate();
|
||||
|
|
@ -207,11 +207,11 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
bool Process(string path, out string response)
|
||||
{
|
||||
path = GetFullPath(path);
|
||||
this.LogDebug("JSON Child[{0}] Processing {1}", Key, path);
|
||||
Debug.Console(1, "JSON Child[{0}] Processing {1}", Key, path);
|
||||
response = "";
|
||||
if (Master == null)
|
||||
{
|
||||
this.LogWarning("JSONChild[{0}] cannot process without Master attached", Key);
|
||||
Debug.Console(1, "JSONChild[{0}] cannot process without Master attached", Key);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -233,8 +233,8 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
if (isCount)
|
||||
response = (t.HasValues ? t.Children().Count() : 0).ToString();
|
||||
else
|
||||
response = (string)t;
|
||||
this.LogDebug(" ='{0}'", response);
|
||||
response = t.Value<string>();
|
||||
Debug.Console(1, " ='{0}'", response);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -260,13 +260,13 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
{
|
||||
if (!LinkedToObject)
|
||||
{
|
||||
this.LogDebug("Not linked to object in file. Skipping");
|
||||
Debug.Console(1, this, "Not linked to object in file. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetAllPathsDelegate == null)
|
||||
{
|
||||
this.LogDebug("No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
|
||||
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
|
||||
return;
|
||||
}
|
||||
SetAllPathsDelegate();
|
||||
|
|
@ -280,6 +280,9 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="theValue"></param>
|
||||
/// <summary>
|
||||
/// USetBoolValue method
|
||||
/// </summary>
|
||||
public void USetBoolValue(ushort key, ushort theValue)
|
||||
{
|
||||
SetBoolValue(key, theValue == 1);
|
||||
|
|
@ -290,6 +293,9 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="theValue"></param>
|
||||
/// <summary>
|
||||
/// SetBoolValue method
|
||||
/// </summary>
|
||||
public void SetBoolValue(ushort key, bool theValue)
|
||||
{
|
||||
if (BoolPaths.ContainsKey(key))
|
||||
|
|
@ -301,6 +307,9 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="theValue"></param>
|
||||
/// <summary>
|
||||
/// SetUShortValue method
|
||||
/// </summary>
|
||||
public void SetUShortValue(ushort key, ushort theValue)
|
||||
{
|
||||
if (UshortPaths.ContainsKey(key))
|
||||
|
|
@ -312,6 +321,9 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="theValue"></param>
|
||||
/// <summary>
|
||||
/// SetStringValue method
|
||||
/// </summary>
|
||||
public void SetStringValue(ushort key, string theValue)
|
||||
{
|
||||
if (StringPaths.ContainsKey(key))
|
||||
|
|
@ -323,12 +335,15 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="keyPath"></param>
|
||||
/// <param name="valueToSave"></param>
|
||||
/// <summary>
|
||||
/// SetValueOnMaster method
|
||||
/// </summary>
|
||||
public void SetValueOnMaster(string keyPath, JValue valueToSave)
|
||||
{
|
||||
var path = GetFullPath(keyPath);
|
||||
try
|
||||
{
|
||||
this.LogDebug("JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
|
||||
Debug.Console(1, "JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
|
||||
|
||||
//var token = Master.JsonObject.SelectToken(path);
|
||||
//if (token != null) // The path exists in the file
|
||||
|
|
@ -336,7 +351,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogDebug("JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
|
||||
Debug.Console(1, "JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -404,3 +419,4 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,31 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using PepperDash.Core.Logging;
|
||||
using Formatting = NewtonsoftJson::Newtonsoft.Json.Formatting;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a JSON file that can be read and written to
|
||||
/// </summary>
|
||||
public class JsonToSimplFileMaster : JsonToSimplMaster
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a JSON file that can be read and written to
|
||||
/// </summary>
|
||||
public class JsonToSimplFileMaster : JsonToSimplMaster
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the filepath as well as registers this with the Global.Masters list
|
||||
/// </summary>
|
||||
public string Filepath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filepath to the actual file that will be read (Portal or local)
|
||||
/// Gets or sets the ActualFilePath
|
||||
/// </summary>
|
||||
public string ActualFilePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the Filename
|
||||
/// </summary>
|
||||
public string Filename { get; private set; }
|
||||
/// <summary>
|
||||
|
|
@ -130,7 +125,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
var fileName = Path.GetFileName(Filepath);
|
||||
|
||||
OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange);
|
||||
this.LogInformation("Checking '{0}' for '{1}'", fileDirectory, fileName);
|
||||
Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName);
|
||||
|
||||
if (Directory.Exists(fileDirectory))
|
||||
{
|
||||
|
|
@ -144,7 +139,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
var msg = string.Format("JSON file not found: {0}", Filepath);
|
||||
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
|
||||
CrestronConsole.PrintLine(msg);
|
||||
this.LogError(msg);
|
||||
ErrorLog.Error(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -153,18 +148,18 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
ActualFilePath = actualFile.FullName;
|
||||
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
|
||||
OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange);
|
||||
this.LogInformation("Actual JSON file is {0}", ActualFilePath);
|
||||
Debug.Console(1, "Actual JSON file is {0}", ActualFilePath);
|
||||
|
||||
Filename = actualFile.Name;
|
||||
OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange);
|
||||
OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange);
|
||||
this.LogInformation("JSON Filename is {0}", Filename);
|
||||
Debug.Console(1, "JSON Filename is {0}", Filename);
|
||||
|
||||
|
||||
FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator);
|
||||
OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange);
|
||||
OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange);
|
||||
this.LogInformation("JSON File Path is {0}", FilePathName);
|
||||
Debug.Console(1, "JSON File Path is {0}", FilePathName);
|
||||
|
||||
var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
|
||||
|
||||
|
|
@ -177,7 +172,7 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
else
|
||||
{
|
||||
OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange);
|
||||
this.LogError("'{0}' not found", fileDirectory);
|
||||
Debug.Console(1, "'{0}' not found", fileDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -185,12 +180,12 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message);
|
||||
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
|
||||
CrestronConsole.PrintLine(msg);
|
||||
this.LogException(e, "EvaluateFile Exception: {0}", e.Message);
|
||||
ErrorLog.Error(msg);
|
||||
|
||||
var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
|
||||
OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange);
|
||||
CrestronConsole.PrintLine(stackTrace);
|
||||
this.LogVerbose("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
|
||||
ErrorLog.Error(stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,6 +194,9 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
/// Sets the debug level
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <summary>
|
||||
/// setDebugLevel method
|
||||
/// </summary>
|
||||
public void setDebugLevel(uint level)
|
||||
{
|
||||
Debug.SetDebugLevel(level);
|
||||
|
|
@ -214,31 +212,63 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
// Make each child update their values into master object
|
||||
foreach (var child in Children)
|
||||
{
|
||||
this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
|
||||
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
|
||||
child.UpdateInputsForMaster();
|
||||
}
|
||||
|
||||
if (UnsavedValues == null || UnsavedValues.Count == 0)
|
||||
{
|
||||
this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID);
|
||||
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
|
||||
return;
|
||||
}
|
||||
lock (FileLock)
|
||||
{
|
||||
this.LogInformation("Saving");
|
||||
Debug.Console(1, "Saving");
|
||||
foreach (var path in UnsavedValues.Keys)
|
||||
{
|
||||
var tokenToReplace = JsonObject.SelectToken(path);
|
||||
if (tokenToReplace != null)
|
||||
{// It's found
|
||||
tokenToReplace.Replace(UnsavedValues[path]);
|
||||
this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path);
|
||||
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
|
||||
}
|
||||
else // No token. Let's make one
|
||||
{
|
||||
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
|
||||
this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
|
||||
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
|
||||
|
||||
// JContainer jpart = JsonObject;
|
||||
// // walk down the path and find where it goes
|
||||
//#warning Does not handle arrays.
|
||||
// foreach (var part in path.Split('.'))
|
||||
// {
|
||||
|
||||
// var openPos = part.IndexOf('[');
|
||||
// if (openPos > -1)
|
||||
// {
|
||||
// openPos++; // move to number
|
||||
// var closePos = part.IndexOf(']');
|
||||
// var arrayName = part.Substring(0, openPos - 1); // get the name
|
||||
// var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos));
|
||||
|
||||
// // Check if the array itself exists and add the item if so
|
||||
// if (jpart[arrayName] != null)
|
||||
// {
|
||||
// var arrayObj = jpart[arrayName] as JArray;
|
||||
// var item = arrayObj[index];
|
||||
// if (item == null)
|
||||
// arrayObj.Add(new JObject());
|
||||
// }
|
||||
|
||||
// Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW");
|
||||
// continue;
|
||||
// }
|
||||
// // Build the
|
||||
// if (jpart[part] == null)
|
||||
// jpart.Add(new JProperty(part, new JObject()));
|
||||
// jpart = jpart[part] as JContainer;
|
||||
// }
|
||||
// jpart.Replace(UnsavedValues[path]);
|
||||
}
|
||||
}
|
||||
using (StreamWriter sw = new StreamWriter(ActualFilePath))
|
||||
|
|
@ -251,13 +281,12 @@ public class JsonToSimplFileMaster : JsonToSimplMaster
|
|||
catch (Exception e)
|
||||
{
|
||||
string err = string.Format("Error writing JSON file:\r{0}", e);
|
||||
this.LogException(e, "Error writing JSON file: {0}", e.Message);
|
||||
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
|
||||
Debug.Console(0, err);
|
||||
ErrorLog.Warn(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a JsonToSimplFixedPathObject
|
||||
/// </summary>
|
||||
public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -15,3 +15,4 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
this.LinkedToObject = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,15 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core.Logging;
|
||||
using Renci.SshNet.Messages;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// Generic Master
|
||||
/// </summary>
|
||||
public class JsonToSimplGenericMaster : JsonToSimplMaster
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a JsonToSimplGenericMaster
|
||||
/// </summary>
|
||||
public class JsonToSimplGenericMaster : JsonToSimplMaster
|
||||
{
|
||||
/*****************************************************************************************/
|
||||
/** Privates **/
|
||||
|
||||
|
|
@ -26,7 +21,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
static object WriteLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Callback action for saving
|
||||
/// Gets or sets the SaveCallback
|
||||
/// </summary>
|
||||
public Action<string> SaveCallback { get; set; }
|
||||
|
||||
|
|
@ -76,8 +71,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.LogException(e, "JSON parsing failed:\r{0}", e.Message);
|
||||
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
|
||||
Debug.Console(0, this, "JSON parsing failed:\r{0}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,36 +86,37 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
// Make each child update their values into master object
|
||||
foreach (var child in Children)
|
||||
{
|
||||
this.LogDebug("Master. checking child [{0}] for updates to save", child.Key);
|
||||
Debug.Console(1, this, "Master. checking child [{0}] for updates to save", child.Key);
|
||||
child.UpdateInputsForMaster();
|
||||
}
|
||||
|
||||
if (UnsavedValues == null || UnsavedValues.Count == 0)
|
||||
{
|
||||
this.LogDebug("Master. No updated values to save. Skipping");
|
||||
Debug.Console(1, this, "Master. No updated values to save. Skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
lock (WriteLock)
|
||||
{
|
||||
this.LogDebug("Saving");
|
||||
Debug.Console(1, this, "Saving");
|
||||
foreach (var path in UnsavedValues.Keys)
|
||||
{
|
||||
var tokenToReplace = JsonObject.SelectToken(path);
|
||||
if (tokenToReplace != null)
|
||||
{// It's found
|
||||
tokenToReplace.Replace(UnsavedValues[path]);
|
||||
this.LogDebug("Master Updating '{0}'", path);
|
||||
Debug.Console(1, this, "Master Updating '{0}'", path);
|
||||
}
|
||||
else // No token. Let's make one
|
||||
{
|
||||
this.LogDebug("Master Cannot write value onto missing property: '{0}'", path);
|
||||
Debug.Console(1, "Master Cannot write value onto missing property: '{0}'", path);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (SaveCallback != null)
|
||||
SaveCallback(JsonObject.ToString());
|
||||
else
|
||||
this.LogDebug("WARNING: No save callback defined.");
|
||||
Debug.Console(0, this, "WARNING: No save callback defined.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,16 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using JArray = NewtonsoftJson::Newtonsoft.Json.Linq.JArray;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
|
||||
using JsonSerializationException = NewtonsoftJson::Newtonsoft.Json.JsonSerializationException;
|
||||
using JsonTextReader = NewtonsoftJson::Newtonsoft.Json.JsonTextReader;
|
||||
using PepperDash.Core.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class for JsonToSimpl interactions
|
||||
/// </summary>
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract base class for JsonToSimpl interactions
|
||||
/// </summary>
|
||||
public abstract class JsonToSimplMaster : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -45,7 +39,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
public string Key { get { return UniqueID; } }
|
||||
|
||||
/// <summary>
|
||||
/// A unique ID
|
||||
/// Gets or sets the UniqueID
|
||||
/// </summary>
|
||||
public string UniqueID { get; protected set; }
|
||||
|
||||
|
|
@ -89,7 +83,7 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// Gets or sets the JsonObject
|
||||
/// </summary>
|
||||
public JObject JsonObject { get; protected set; }
|
||||
|
||||
|
|
@ -143,10 +137,11 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
{
|
||||
if (UnsavedValues.ContainsKey(path))
|
||||
{
|
||||
this.LogWarning("Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path);
|
||||
Debug.Console(0, "Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path);
|
||||
}
|
||||
else
|
||||
UnsavedValues.Add(path, value);
|
||||
//Debug.Console(0, "Master[{0}] Unsaved size={1}", UniqueID, UnsavedValues.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -167,7 +162,11 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// <returns></returns>
|
||||
public static JObject ParseObject(string json)
|
||||
{
|
||||
#if NET6_0
|
||||
using (var reader = new JsonTextReader(new System.IO.StringReader(json)))
|
||||
#else
|
||||
using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json)))
|
||||
#endif
|
||||
{
|
||||
var startDepth = reader.Depth;
|
||||
var obj = JObject.Load(reader);
|
||||
|
|
@ -182,10 +181,16 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// ParseArray method
|
||||
/// </summary>
|
||||
public static JArray ParseArray(string json)
|
||||
{
|
||||
|
||||
#if NET6_0
|
||||
using (var reader = new JsonTextReader(new System.IO.StringReader(json)))
|
||||
#else
|
||||
using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json)))
|
||||
#endif
|
||||
{
|
||||
var startDepth = reader.Depth;
|
||||
var obj = JArray.Load(reader);
|
||||
|
|
@ -244,3 +249,4 @@ namespace PepperDash.Core.JsonToSimpl;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,25 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using JValue = NewtonsoftJson::Newtonsoft.Json.Linq.JValue;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core.Config;
|
||||
using PepperDash.Core.Logging;
|
||||
|
||||
namespace PepperDash.Core.JsonToSimpl;
|
||||
|
||||
/// <summary>
|
||||
/// Portal File Master
|
||||
/// </summary>
|
||||
public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
||||
namespace PepperDash.Core.JsonToSimpl
|
||||
{
|
||||
/// <summary>
|
||||
/// Portal File Master
|
||||
/// </summary>
|
||||
public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the filepath as well as registers this with the Global.Masters list
|
||||
/// </summary>
|
||||
public string PortalFilepath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// File path of the actual file being read (Portal or local)
|
||||
/// Gets or sets the ActualFilePath
|
||||
/// </summary>
|
||||
public string ActualFilePath { get; private set; }
|
||||
|
||||
|
|
@ -62,7 +58,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
|||
// If the portal file is xyz.json, then
|
||||
// the file we want to check for first will be called xyz.local.json
|
||||
var localFilepath = Path.ChangeExtension(PortalFilepath, "local.json");
|
||||
this.LogInformation("Checking for local file {0}", localFilepath);
|
||||
Debug.Console(0, this, "Checking for local file {0}", localFilepath);
|
||||
var actualLocalFile = GetActualFileInfoFromPath(localFilepath);
|
||||
|
||||
if (actualLocalFile != null)
|
||||
|
|
@ -74,7 +70,7 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
|||
// and create the local.
|
||||
else
|
||||
{
|
||||
this.LogInformation("Local JSON file not found {0}\rLoading portal JSON file", localFilepath);
|
||||
Debug.Console(1, this, "Local JSON file not found {0}\rLoading portal JSON file", localFilepath);
|
||||
var actualPortalFile = GetActualFileInfoFromPath(portalFilepath);
|
||||
if (actualPortalFile != null)
|
||||
{
|
||||
|
|
@ -87,13 +83,14 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
|||
else
|
||||
{
|
||||
var msg = string.Format("Portal JSON file not found: {0}", PortalFilepath);
|
||||
this.LogError(msg);
|
||||
Debug.Console(1, this, msg);
|
||||
ErrorLog.Error(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// At this point we should have a local file. Do it.
|
||||
this.LogInformation("Reading local JSON file {0}", ActualFilePath);
|
||||
Debug.Console(1, "Reading local JSON file {0}", ActualFilePath);
|
||||
|
||||
string json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
|
||||
|
||||
|
|
@ -107,7 +104,8 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
|||
catch (Exception e)
|
||||
{
|
||||
var msg = string.Format("JSON parsing failed:\r{0}", e);
|
||||
this.LogError(msg);
|
||||
CrestronConsole.PrintLine(msg);
|
||||
ErrorLog.Error(msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -148,30 +146,30 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
|||
// Make each child update their values into master object
|
||||
foreach (var child in Children)
|
||||
{
|
||||
this.LogInformation("Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
|
||||
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
|
||||
child.UpdateInputsForMaster();
|
||||
}
|
||||
|
||||
if (UnsavedValues == null || UnsavedValues.Count == 0)
|
||||
{
|
||||
this.LogInformation("Master [{0}] No updated values to save. Skipping", UniqueID);
|
||||
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
|
||||
return;
|
||||
}
|
||||
lock (FileLock)
|
||||
{
|
||||
this.LogInformation("Saving");
|
||||
Debug.Console(1, "Saving");
|
||||
foreach (var path in UnsavedValues.Keys)
|
||||
{
|
||||
var tokenToReplace = JsonObject.SelectToken(path);
|
||||
if (tokenToReplace != null)
|
||||
{// It's found
|
||||
tokenToReplace.Replace(UnsavedValues[path]);
|
||||
this.LogInformation("JSON Master[{0}] Updating '{1}'", UniqueID, path);
|
||||
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
|
||||
}
|
||||
else // No token. Let's make one
|
||||
{
|
||||
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
|
||||
this.LogWarning("JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
|
||||
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -185,11 +183,12 @@ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
|
|||
catch (Exception e)
|
||||
{
|
||||
string err = string.Format("Error writing JSON file:\r{0}", e);
|
||||
this.LogException(e, "Error writing JSON file: {0}", e.Message);
|
||||
this.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
|
||||
Debug.Console(0, err);
|
||||
ErrorLog.Warn(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Enriches log events with Crestron-specific context properties, such as the application name based on the device platform.
|
||||
/// </summary>
|
||||
public class CrestronEnricher : ILogEventEnricher
|
||||
namespace PepperDash.Core.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a CrestronEnricher
|
||||
/// </summary>
|
||||
public class CrestronEnricher : ILogEventEnricher
|
||||
{
|
||||
static readonly string _appName;
|
||||
|
||||
static CrestronEnricher()
|
||||
|
|
@ -31,14 +31,13 @@ public class CrestronEnricher : ILogEventEnricher
|
|||
|
||||
|
||||
/// <summary>
|
||||
/// Enriches the log event with Crestron-specific properties.
|
||||
/// Enrich method
|
||||
/// </summary>
|
||||
/// <param name="logEvent"></param>
|
||||
/// <param name="propertyFactory"></param>
|
||||
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||
{
|
||||
var property = propertyFactory.CreateProperty("App", _appName);
|
||||
|
||||
logEvent.AddOrUpdateProperty(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,12 +9,18 @@ using System.IO;
|
|||
using System.Text;
|
||||
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
public class DebugConsoleSink : ILogEventSink
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a DebugConsoleSink
|
||||
/// </summary>
|
||||
public class DebugConsoleSink : ILogEventSink
|
||||
{
|
||||
private readonly ITextFormatter _textFormatter;
|
||||
|
||||
/// <summary>
|
||||
/// Emit method
|
||||
/// </summary>
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
if (!Debug.IsRunningOnAppliance) return;
|
||||
|
|
@ -40,14 +46,19 @@ public class DebugConsoleSink : ILogEventSink
|
|||
_textFormatter = formatProvider ?? new JsonFormatter();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public static class DebugConsoleSinkExtensions
|
||||
{
|
||||
public static class DebugConsoleSinkExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// DebugConsoleSink method
|
||||
/// </summary>
|
||||
public static LoggerConfiguration DebugConsoleSink(
|
||||
this LoggerSinkConfiguration loggerConfiguration,
|
||||
ITextFormatter formatProvider = null)
|
||||
{
|
||||
return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
294
src/PepperDash.Core/Logging/DebugContext.cs
Normal file
294
src/PepperDash.Core/Logging/DebugContext.cs
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a debugging context
|
||||
/// </summary>
|
||||
[Obsolete("DebugContext is no longer supported and will be removed in a future release.")]
|
||||
public class DebugContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the folder location where a given program stores it's debug level memory. By default, the
|
||||
/// file written will be named appNdebug where N is 1-10.
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the file containing the current debug settings.
|
||||
/// </summary>
|
||||
//string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber);
|
||||
|
||||
DebugContextSaveData SaveData;
|
||||
|
||||
int SaveTimeoutMs = 30000;
|
||||
|
||||
CTimer SaveTimer;
|
||||
|
||||
|
||||
static List<DebugContext> Contexts = new List<DebugContext>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates or gets a debug context
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// GetDebugContext method
|
||||
/// </summary>
|
||||
public static DebugContext GetDebugContext(string key)
|
||||
{
|
||||
var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
|
||||
if (context == null)
|
||||
{
|
||||
context = new DebugContext(key);
|
||||
Contexts.Add(context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not use. For S+ access.
|
||||
/// </summary>
|
||||
public DebugContext() { }
|
||||
|
||||
DebugContext(string key)
|
||||
{
|
||||
Key = key;
|
||||
if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro)
|
||||
{
|
||||
// Add command to console
|
||||
CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug",
|
||||
"appdebug:P [0-2]: Sets the application's console debug message level",
|
||||
ConsoleAccessLevelEnum.AccessOperator);
|
||||
}
|
||||
|
||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||
|
||||
LoadMemory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to save memory when shutting down
|
||||
/// </summary>
|
||||
/// <param name="programEventType"></param>
|
||||
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
|
||||
{
|
||||
if (programEventType == eProgramStatusEventType.Stopping)
|
||||
{
|
||||
if (SaveTimer != null)
|
||||
{
|
||||
SaveTimer.Stop();
|
||||
SaveTimer = null;
|
||||
}
|
||||
Console(0, "Saving debug settings");
|
||||
SaveMemory();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for console command
|
||||
/// </summary>
|
||||
/// <param name="levelString"></param>
|
||||
/// <summary>
|
||||
/// SetDebugFromConsole method
|
||||
/// </summary>
|
||||
public void SetDebugFromConsole(string levelString)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(levelString.Trim()))
|
||||
{
|
||||
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level);
|
||||
return;
|
||||
}
|
||||
|
||||
SetDebugLevel(Convert.ToInt32(levelString));
|
||||
}
|
||||
catch
|
||||
{
|
||||
CrestronConsole.PrintLine("Usage: appdebug:P [0-2]");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the debug level
|
||||
/// </summary>
|
||||
/// <param name="level"> Valid values 0 (no debug), 1 (critical), 2 (all messages)</param>
|
||||
/// <summary>
|
||||
/// SetDebugLevel method
|
||||
/// </summary>
|
||||
public void SetDebugLevel(int level)
|
||||
{
|
||||
if (level <= 2)
|
||||
{
|
||||
SaveData.Level = level;
|
||||
SaveMemoryOnTimeout();
|
||||
|
||||
CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}",
|
||||
InitialParametersClass.ApplicationNumber, SaveData.Level);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints message to console if current debug level is equal to or higher than the level of this message.
|
||||
/// Uses CrestronConsole.PrintLine.
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="format">Console format string</param>
|
||||
/// <param name="items">Object parameters</param>
|
||||
public void Console(uint level, string format, params object[] items)
|
||||
{
|
||||
if (SaveData.Level >= level)
|
||||
CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber,
|
||||
string.Format(format, items));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Console method
|
||||
/// </summary>
|
||||
public void Console(uint level, IKeyed dev, string format, params object[] items)
|
||||
{
|
||||
if (SaveData.Level >= level)
|
||||
Console(level, "[{0}] {1}", dev.Key, string.Format(format, items));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="dev"></param>
|
||||
/// <param name="errorLogLevel"></param>
|
||||
/// <param name="format"></param>
|
||||
/// <param name="items"></param>
|
||||
public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel,
|
||||
string format, params object[] items)
|
||||
{
|
||||
if (SaveData.Level >= level)
|
||||
{
|
||||
var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items));
|
||||
Console(level, str);
|
||||
LogError(errorLogLevel, str);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="level"></param>
|
||||
/// <param name="errorLogLevel"></param>
|
||||
/// <param name="format"></param>
|
||||
/// <param name="items"></param>
|
||||
public void Console(uint level, Debug.ErrorLogLevel errorLogLevel,
|
||||
string format, params object[] items)
|
||||
{
|
||||
if (SaveData.Level >= level)
|
||||
{
|
||||
var str = string.Format(format, items);
|
||||
Console(level, str);
|
||||
LogError(errorLogLevel, str);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="errorLogLevel"></param>
|
||||
/// <param name="str"></param>
|
||||
/// <summary>
|
||||
/// LogError method
|
||||
/// </summary>
|
||||
public void LogError(Debug.ErrorLogLevel errorLogLevel, string str)
|
||||
{
|
||||
string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str);
|
||||
switch (errorLogLevel)
|
||||
{
|
||||
case Debug.ErrorLogLevel.Error:
|
||||
ErrorLog.Error(msg);
|
||||
break;
|
||||
case Debug.ErrorLogLevel.Warning:
|
||||
ErrorLog.Warn(msg);
|
||||
break;
|
||||
case Debug.ErrorLogLevel.Notice:
|
||||
ErrorLog.Notice(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the memory object after timeout
|
||||
/// </summary>
|
||||
void SaveMemoryOnTimeout()
|
||||
{
|
||||
if (SaveTimer == null)
|
||||
SaveTimer = new CTimer(o =>
|
||||
{
|
||||
SaveTimer = null;
|
||||
SaveMemory();
|
||||
}, SaveTimeoutMs);
|
||||
else
|
||||
SaveTimer.Reset(SaveTimeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the memory - use SaveMemoryOnTimeout
|
||||
/// </summary>
|
||||
void SaveMemory()
|
||||
{
|
||||
using (StreamWriter sw = new StreamWriter(GetMemoryFileName()))
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(SaveData);
|
||||
sw.Write(json);
|
||||
sw.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
void LoadMemory()
|
||||
{
|
||||
var file = GetMemoryFileName();
|
||||
if (File.Exists(file))
|
||||
{
|
||||
using (StreamReader sr = new StreamReader(file))
|
||||
{
|
||||
var data = JsonConvert.DeserializeObject<DebugContextSaveData>(sr.ReadToEnd());
|
||||
if (data != null)
|
||||
{
|
||||
SaveData = data;
|
||||
Debug.Console(1, "Debug memory restored from file");
|
||||
return;
|
||||
}
|
||||
else
|
||||
SaveData = new DebugContextSaveData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to get the file path for this app's debug memory
|
||||
/// </summary>
|
||||
string GetMemoryFileName()
|
||||
{
|
||||
return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class DebugContextSaveData
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,16 @@ using Crestron.SimplSharp.CrestronLogger;
|
|||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace PepperDash.Core.Logging;
|
||||
|
||||
public class DebugCrestronLoggerSink : ILogEventSink
|
||||
namespace PepperDash.Core.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a DebugCrestronLoggerSink
|
||||
/// </summary>
|
||||
public class DebugCrestronLoggerSink : ILogEventSink
|
||||
{
|
||||
/// <summary>
|
||||
/// Emit method
|
||||
/// </summary>
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
if (!Debug.IsRunningOnAppliance) return;
|
||||
|
|
@ -25,4 +31,5 @@ public class DebugCrestronLoggerSink : ILogEventSink
|
|||
{
|
||||
CrestronLogger.Initialize(1, LoggerModeEnum.RM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Core.Logging;
|
||||
|
||||
public class DebugErrorLogSink : ILogEventSink
|
||||
namespace PepperDash.Core.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a DebugErrorLogSink
|
||||
/// </summary>
|
||||
public class DebugErrorLogSink : ILogEventSink
|
||||
{
|
||||
private ITextFormatter _formatter;
|
||||
|
||||
private Dictionary<LogEventLevel, Action<string>> _errorLogMap = new Dictionary<LogEventLevel, Action<string>>
|
||||
|
|
@ -24,6 +27,9 @@ public class DebugErrorLogSink : ILogEventSink
|
|||
{LogEventLevel.Error, (msg) => ErrorLog.Error(msg) },
|
||||
{LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) }
|
||||
};
|
||||
/// <summary>
|
||||
/// Emit method
|
||||
/// </summary>
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
string message;
|
||||
|
|
@ -61,4 +67,5 @@ public class DebugErrorLogSink : ILogEventSink
|
|||
{
|
||||
_formatter = formatter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,72 +2,112 @@
|
|||
using Serilog.Events;
|
||||
using Log = PepperDash.Core.Debug;
|
||||
|
||||
namespace PepperDash.Core.Logging;
|
||||
|
||||
public static class DebugExtensions
|
||||
namespace PepperDash.Core.Logging
|
||||
{
|
||||
public static class DebugExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// LogException method
|
||||
/// </summary>
|
||||
public static void LogException(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(ex, message, device, args);
|
||||
Log.LogMessage(ex, message, device: device, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogVerbose method
|
||||
/// </summary>
|
||||
public static void LogVerbose(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Verbose, ex, message, device, args);
|
||||
Log.LogVerbose(ex, device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogVerbose method
|
||||
/// </summary>
|
||||
public static void LogVerbose(this IKeyed device, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Verbose, device, message, args);
|
||||
Log.LogVerbose(device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogDebug method
|
||||
/// </summary>
|
||||
public static void LogDebug(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Debug, ex, message, device, args);
|
||||
Log.LogDebug(ex, device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogDebug method
|
||||
/// </summary>
|
||||
public static void LogDebug(this IKeyed device, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Debug, device, message, args);
|
||||
Log.LogDebug(device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogInformation method
|
||||
/// </summary>
|
||||
public static void LogInformation(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Information, ex, message, device, args);
|
||||
Log.LogInformation(ex, device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogInformation method
|
||||
/// </summary>
|
||||
public static void LogInformation(this IKeyed device, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Information, device, message, args);
|
||||
Log.LogInformation(device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogWarning method
|
||||
/// </summary>
|
||||
public static void LogWarning(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Warning, ex, message, device, args);
|
||||
Log.LogWarning(ex, device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogWarning method
|
||||
/// </summary>
|
||||
public static void LogWarning(this IKeyed device, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Warning, device, message, args);
|
||||
Log.LogWarning(device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogError method
|
||||
/// </summary>
|
||||
public static void LogError(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Error, ex, message, device, args);
|
||||
Log.LogError(ex, device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogError method
|
||||
/// </summary>
|
||||
public static void LogError(this IKeyed device, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Error, device, message, args);
|
||||
Log.LogError(device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogFatal method
|
||||
/// </summary>
|
||||
public static void LogFatal(this IKeyed device, Exception ex, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Fatal, ex, message, device, args);
|
||||
Log.LogFatal(ex, device, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LogFatal method
|
||||
/// </summary>
|
||||
public static void LogFatal(this IKeyed device, string message, params object[] args)
|
||||
{
|
||||
Log.LogMessage(LogEventLevel.Fatal, device, message, args);
|
||||
Log.LogFatal(device, message, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,26 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using Crestron.SimplSharp;
|
||||
using JsonProperty = NewtonsoftJson::Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Class to persist current Debug settings across program restarts
|
||||
/// </summary>
|
||||
public class DebugContextCollection
|
||||
namespace PepperDash.Core.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a DebugContextCollection
|
||||
/// </summary>
|
||||
public class DebugContextCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// To prevent threading issues with the DeviceDebugSettings collection
|
||||
/// </summary>
|
||||
private readonly object _deviceDebugSettingsLock = new();
|
||||
private readonly CCriticalSection _deviceDebugSettingsLock;
|
||||
|
||||
[JsonProperty("items")]
|
||||
private readonly Dictionary<string, DebugContextItem> _items = new Dictionary<string, DebugContextItem>();
|
||||
[JsonProperty("items")] private readonly Dictionary<string, DebugContextItem> _items;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of the debug settings for each device where the dictionary key is the device key
|
||||
/// </summary>
|
||||
[JsonProperty("deviceDebugSettings")]
|
||||
private Dictionary<string, object> DeviceDebugSettings { get; set; } = new Dictionary<string, object>();
|
||||
private Dictionary<string, object> DeviceDebugSettings { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -32,7 +28,9 @@ public class DebugContextCollection
|
|||
/// </summary>
|
||||
public DebugContextCollection()
|
||||
{
|
||||
|
||||
_deviceDebugSettingsLock = new CCriticalSection();
|
||||
DeviceDebugSettings = new Dictionary<string, object>();
|
||||
_items = new Dictionary<string, DebugContextItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -73,10 +71,15 @@ public class DebugContextCollection
|
|||
/// <param name="deviceKey"></param>
|
||||
/// <param name="settings"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// SetDebugSettingsForKey method
|
||||
/// </summary>
|
||||
public void SetDebugSettingsForKey(string deviceKey, object settings)
|
||||
{
|
||||
lock (_deviceDebugSettingsLock)
|
||||
try
|
||||
{
|
||||
_deviceDebugSettingsLock.Enter();
|
||||
|
||||
if (DeviceDebugSettings.ContainsKey(deviceKey))
|
||||
{
|
||||
DeviceDebugSettings[deviceKey] = settings;
|
||||
|
|
@ -84,6 +87,10 @@ public class DebugContextCollection
|
|||
else
|
||||
DeviceDebugSettings.Add(deviceKey, settings);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deviceDebugSettingsLock.Leave();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -91,17 +98,20 @@ public class DebugContextCollection
|
|||
/// </summary>
|
||||
/// <param name="deviceKey"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// GetDebugSettingsForKey method
|
||||
/// </summary>
|
||||
public object GetDebugSettingsForKey(string deviceKey)
|
||||
{
|
||||
return DeviceDebugSettings[deviceKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains information about
|
||||
/// </summary>
|
||||
public class DebugContextItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information about
|
||||
/// </summary>
|
||||
public class DebugContextItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of debug messages to print
|
||||
/// </summary>
|
||||
|
|
@ -113,4 +123,5 @@ public class DebugContextItem
|
|||
/// </summary>
|
||||
[JsonProperty("doNotLoadOnNextBoot")]
|
||||
public bool DoNotLoadOnNextBoot { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,44 +1,44 @@
|
|||
extern alias NewtonsoftJson;
|
||||
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Crestron.SimplSharp;
|
||||
using System;
|
||||
using Serilog;
|
||||
using Serilog.Configuration;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Formatting;
|
||||
using JObject = NewtonsoftJson::Newtonsoft.Json.Linq.JObject;
|
||||
using Serilog.Formatting.Json;
|
||||
using System.IO;
|
||||
using WebSocketSharp;
|
||||
using Serilog.Configuration;
|
||||
using WebSocketSharp.Server;
|
||||
using Crestron.SimplSharp;
|
||||
using WebSocketSharp;
|
||||
using System.Security.Authentication;
|
||||
using WebSocketSharp.Net;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.IO;
|
||||
using Org.BouncyCastle.Asn1;
|
||||
using Org.BouncyCastle.Asn1.X509;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Generators;
|
||||
using Org.BouncyCastle.Crypto.Operators;
|
||||
using Org.BouncyCastle.Math;
|
||||
using Org.BouncyCastle.Pkcs;
|
||||
using Org.BouncyCastle.Security;
|
||||
using Org.BouncyCastle.X509;
|
||||
using Serilog.Formatting;
|
||||
using Serilog.Formatting.Json;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a WebSocket-based logging sink for debugging purposes, allowing log events to be broadcast to connected
|
||||
/// WebSocket clients.
|
||||
/// </summary>
|
||||
/// <remarks>This class implements the <see cref="ILogEventSink"/> interface and is designed to send
|
||||
/// formatted log events to WebSocket clients connected to a secure WebSocket server. The server is hosted locally
|
||||
/// and uses a self-signed certificate for SSL/TLS encryption.</remarks>
|
||||
public class DebugWebsocketSink : ILogEventSink, IKeyed
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a DebugWebsocketSink
|
||||
/// </summary>
|
||||
public class DebugWebsocketSink : ILogEventSink
|
||||
{
|
||||
private HttpServer _httpsServer;
|
||||
|
||||
private readonly string _path = "/debug/join/";
|
||||
private string _path = "/debug/join/";
|
||||
private const string _certificateName = "selfCres";
|
||||
private const string _certificatePassword = "cres12345";
|
||||
|
||||
private static string CertPath =>
|
||||
$"{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}{_certificateName}.pfx";
|
||||
|
||||
/// <summary>
|
||||
/// Gets the port number on which the HTTPS server is currently running.
|
||||
/// </summary>
|
||||
|
||||
public int Port
|
||||
{
|
||||
get
|
||||
|
|
@ -49,11 +49,6 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the WebSocket URL for the current server instance.
|
||||
/// </summary>
|
||||
/// <remarks>The URL is dynamically constructed based on the server's current IP address, port,
|
||||
/// and WebSocket path.</remarks>
|
||||
public string Url
|
||||
{
|
||||
get
|
||||
|
|
@ -63,15 +58,17 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
if (service == null) return "";
|
||||
|
||||
// Use CSLAN IP if available, otherwise fallback to primary IP. This ensures we provide a reachable URL in dual-stack environments.
|
||||
if (!string.IsNullOrEmpty(CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1)))
|
||||
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1)}:{_httpsServer.Port}{service.Path}";
|
||||
var cslanIp = CrestronEthernetHelper.GetEthernetParameter(
|
||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 1);
|
||||
if (!string.IsNullOrEmpty(cslanIp) && cslanIp != "Invalid Value")
|
||||
return $"wss://{cslanIp}:{_httpsServer.Port}{service.Path}";
|
||||
else
|
||||
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{service.Path}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the HTTPS server is currently listening for incoming connections.
|
||||
/// Gets or sets the IsRunning
|
||||
/// </summary>
|
||||
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
|
||||
|
||||
|
|
@ -89,8 +86,6 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Key => "DebugWebsocketSink";
|
||||
|
||||
private readonly ITextFormatter _textFormatter;
|
||||
|
||||
|
|
@ -140,45 +135,68 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
var subjectName = string.Format("CN={0}.{1}", hostName, domainName);
|
||||
var fqdn = string.Format("{0}.{1}", hostName, domainName);
|
||||
|
||||
using var rsa = RSA.Create(2048);
|
||||
var random = new SecureRandom();
|
||||
|
||||
var request = new CertificateRequest(
|
||||
subjectName,
|
||||
rsa,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
// Generate RSA 2048 key pair
|
||||
var keyPairGenerator = new RsaKeyPairGenerator();
|
||||
keyPairGenerator.Init(new KeyGenerationParameters(random, 2048));
|
||||
var keyPair = keyPairGenerator.GenerateKeyPair();
|
||||
|
||||
// Subject Key Identifier
|
||||
request.CertificateExtensions.Add(
|
||||
new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||
// Build certificate
|
||||
var certGenerator = new X509V3CertificateGenerator();
|
||||
certGenerator.SetSerialNumber(BigInteger.ValueOf(Math.Abs(DateTime.UtcNow.Ticks)));
|
||||
certGenerator.SetIssuerDN(new X509Name(subjectName));
|
||||
certGenerator.SetSubjectDN(new X509Name(subjectName));
|
||||
certGenerator.SetNotBefore(DateTime.UtcNow);
|
||||
certGenerator.SetNotAfter(DateTime.UtcNow.AddYears(2));
|
||||
certGenerator.SetPublicKey(keyPair.Public);
|
||||
|
||||
// Extended Key Usage: server + client auth
|
||||
request.CertificateExtensions.Add(
|
||||
new X509EnhancedKeyUsageExtension(
|
||||
new OidCollection
|
||||
{
|
||||
new Oid("1.3.6.1.5.5.7.3.1"), // id-kp-serverAuth
|
||||
new Oid("1.3.6.1.5.5.7.3.2") // id-kp-clientAuth
|
||||
},
|
||||
false));
|
||||
certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false,
|
||||
new ExtendedKeyUsage(new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth }));
|
||||
|
||||
// Subject Alternative Names: DNS + IP
|
||||
var sanBuilder = new SubjectAlternativeNameBuilder();
|
||||
sanBuilder.AddDnsName(fqdn);
|
||||
if (System.Net.IPAddress.TryParse(ipAddress, out var ip))
|
||||
sanBuilder.AddIpAddress(ip);
|
||||
request.CertificateExtensions.Add(sanBuilder.Build());
|
||||
System.Net.IPAddress parsedIp;
|
||||
if (System.Net.IPAddress.TryParse(ipAddress, out parsedIp))
|
||||
{
|
||||
certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false,
|
||||
new GeneralNames(new GeneralName[] {
|
||||
new GeneralName(GeneralName.DnsName, fqdn),
|
||||
new GeneralName(GeneralName.IPAddress, ipAddress)
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false,
|
||||
new GeneralNames(new GeneralName(GeneralName.DnsName, fqdn)));
|
||||
}
|
||||
|
||||
var notBefore = DateTimeOffset.UtcNow;
|
||||
var notAfter = notBefore.AddYears(2);
|
||||
// Sign with SHA256withRSA
|
||||
var signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", keyPair.Private, random);
|
||||
var certificate = certGenerator.Generate(signatureFactory);
|
||||
|
||||
using var cert = request.CreateSelfSigned(notBefore, notAfter);
|
||||
// Export as PKCS12/PFX
|
||||
var pkcs12Store = new Pkcs12StoreBuilder().Build();
|
||||
var certEntry = new X509CertificateEntry(certificate);
|
||||
pkcs12Store.SetCertificateEntry(_certificateName, certEntry);
|
||||
pkcs12Store.SetKeyEntry(_certificateName, new AsymmetricKeyEntry(keyPair.Private), new[] { certEntry });
|
||||
|
||||
var separator = Path.DirectorySeparatorChar;
|
||||
var outputPath = string.Format("{0}user{1}{2}.pfx", separator, separator, _certificateName);
|
||||
|
||||
var pfxBytes = cert.Export(X509ContentType.Pfx, _certificatePassword);
|
||||
File.WriteAllBytes(outputPath, pfxBytes);
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
var passwordChars = _certificatePassword.ToCharArray();
|
||||
try
|
||||
{
|
||||
pkcs12Store.Save(ms, passwordChars, random);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(passwordChars, 0, passwordChars.Length);
|
||||
}
|
||||
File.WriteAllBytes(outputPath, ms.ToArray());
|
||||
}
|
||||
|
||||
CrestronConsole.PrintLine(string.Format("CreateCert: Certificate written to {0}", outputPath));
|
||||
}
|
||||
|
|
@ -189,12 +207,8 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a log event to all connected WebSocket clients.
|
||||
/// Emit method
|
||||
/// </summary>
|
||||
/// <remarks>The log event is formatted using the configured text formatter and then broadcasted
|
||||
/// to all clients connected to the WebSocket server. If the WebSocket server is not initialized or not
|
||||
/// listening, the method exits without performing any action.</remarks>
|
||||
/// <param name="logEvent">The log event to be formatted and broadcasted. Cannot be null.</param>
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
if (_httpsServer == null || !_httpsServer.IsListening) return;
|
||||
|
|
@ -202,7 +216,8 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
var sw = new StringWriter();
|
||||
_textFormatter.Format(logEvent, sw);
|
||||
|
||||
_httpsServer.WebSocketServices[_path].Sessions.Broadcast(sw.ToString());
|
||||
_httpsServer.WebSocketServices.Broadcast(sw.ToString());
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -217,25 +232,65 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
|
||||
|
||||
|
||||
|
||||
|
||||
Start(port, CertPath, _certificatePassword);
|
||||
}
|
||||
|
||||
private static X509Certificate2 LoadOrRecreateCert(string certPath, string certPassword)
|
||||
{
|
||||
if (!File.Exists(certPath))
|
||||
CreateCert();
|
||||
|
||||
try
|
||||
{
|
||||
// EphemeralKeySet is required on Linux/OpenSSL (Crestron 4-series) to avoid
|
||||
// key-container persistence failures, and avoids the private key export restriction.
|
||||
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
||||
return LoadCertFromBouncyCastle(certPath, certPassword);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Cert is stale or was generated by an incompatible library (e.g. old BouncyCastle output).
|
||||
// Delete it, regenerate with the BCL path, and retry once.
|
||||
// Cert is corrupt or was written by an incompatible tool — delete and regenerate once.
|
||||
CrestronConsole.PrintLine(string.Format("SSL cert load failed ({0}); regenerating...", ex.Message));
|
||||
try { File.Delete(certPath); } catch { }
|
||||
CreateCert();
|
||||
return new X509Certificate2(certPath, certPassword, X509KeyStorageFlags.EphemeralKeySet);
|
||||
return LoadCertFromBouncyCastle(certPath, certPassword);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a PKCS#12 file written by BouncyCastle and returns an <see cref="X509Certificate2"/> with
|
||||
/// private key attached.
|
||||
/// The PFX is parsed and re-encoded by BouncyCastle (ensuring format compatibility), then passed as
|
||||
/// raw bytes to <see cref="X509Certificate2"/> so neither <c>RSACryptoServiceProvider</c> nor the
|
||||
/// <c>EphemeralKeySet</c> flag (unsupported on the Crestron/Mono runtime) is needed.
|
||||
/// </summary>
|
||||
private static X509Certificate2 LoadCertFromBouncyCastle(string certPath, string certPassword)
|
||||
{
|
||||
var passwordChars = certPassword.ToCharArray();
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(certPath))
|
||||
{
|
||||
var store = new Pkcs12StoreBuilder().Build();
|
||||
store.Load(stream, passwordChars);
|
||||
|
||||
// Re-encode through BouncyCastle to guarantee PKCS#12 format compatibility,
|
||||
// then hand raw bytes to X509Certificate2 — no RSACryptoServiceProvider needed.
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
store.Save(ms, passwordChars, new SecureRandom());
|
||||
var cert = new X509Certificate2(ms.ToArray(), certPassword);
|
||||
|
||||
if (!cert.HasPrivateKey)
|
||||
throw new InvalidOperationException(
|
||||
string.Format("Certificate loaded from '{0}' does not contain a private key and cannot be used as a server certificate.", certPath));
|
||||
|
||||
return cert;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Array.Clear(passwordChars, 0, passwordChars.Length);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +317,6 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
}
|
||||
Debug.LogInformation("Adding Debug Client Service");
|
||||
_httpsServer.AddWebSocketService<DebugClient>(_path);
|
||||
_httpsServer.OnGet += HandleHttpGet;
|
||||
Debug.LogInformation("Assigning Log Info");
|
||||
_httpsServer.Log.Level = LogLevel.Trace;
|
||||
_httpsServer.Log.Output = WriteWebSocketInternalLog;
|
||||
|
|
@ -280,21 +334,9 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
}
|
||||
}
|
||||
|
||||
private void HandleHttpGet(object sender, HttpRequestEventArgs e)
|
||||
{
|
||||
var res = e.Response;
|
||||
var body = System.Text.Encoding.UTF8.GetBytes(
|
||||
"<html><body><h2>Certificate accepted.</h2><p>You can close this tab and return to the application.</p></body></html>");
|
||||
res.ContentType = "text/html";
|
||||
res.ContentLength64 = body.Length;
|
||||
res.Close(body, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the WebSocket server if it is currently running.
|
||||
/// StopServer method
|
||||
/// </summary>
|
||||
/// <remarks>This method halts the WebSocket server and releases any associated resources. After
|
||||
/// calling this method, the server will no longer accept or process incoming connections.</remarks>
|
||||
public void StopServer()
|
||||
{
|
||||
Debug.LogInformation("Stopping Websocket Server");
|
||||
|
|
@ -360,46 +402,28 @@ public class DebugWebsocketSink : ILogEventSink, IKeyed
|
|||
// Never throw from websocket log callback.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the logger to write log events to a debug WebSocket sink.
|
||||
/// </summary>
|
||||
/// <remarks>This extension method allows you to direct log events to a WebSocket sink for debugging
|
||||
/// purposes.</remarks>
|
||||
public static class DebugWebsocketSinkExtensions
|
||||
{
|
||||
public static class DebugWebsocketSinkExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configures a logger to write log events to a debug WebSocket sink.
|
||||
/// DebugWebsocketSink method
|
||||
/// </summary>
|
||||
/// <remarks>This method adds a sink that writes log events to a WebSocket for debugging purposes.
|
||||
/// It is typically used during development to stream log events in real-time.</remarks>
|
||||
/// <param name="loggerConfiguration">The logger sink configuration to apply the WebSocket sink to.</param>
|
||||
/// <param name="formatProvider">An optional text formatter to format the log events. If not provided, a default formatter will be used.</param>
|
||||
/// <returns>A <see cref="LoggerConfiguration"/> object that can be used to further configure the logger.</returns>
|
||||
public static LoggerConfiguration DebugWebsocketSink(
|
||||
this LoggerSinkConfiguration loggerConfiguration,
|
||||
ITextFormatter formatProvider = null)
|
||||
{
|
||||
return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a WebSocket client for debugging purposes, providing connection lifecycle management and message
|
||||
/// handling functionality.
|
||||
/// </summary>
|
||||
/// <remarks>The <see cref="DebugClient"/> class extends <see cref="WebSocketBehavior"/> to handle
|
||||
/// WebSocket connections, including events for opening, closing, receiving messages, and errors. It tracks the
|
||||
/// duration of the connection and logs relevant events for debugging.</remarks>
|
||||
public class DebugClient : WebSocketBehavior
|
||||
{
|
||||
private DateTime _connectionTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration of time the WebSocket connection has been active.
|
||||
/// Represents a DebugClient
|
||||
/// </summary>
|
||||
public class DebugClient : WebSocketBehavior
|
||||
{
|
||||
private DateTime _connectionTime;
|
||||
|
||||
public TimeSpan ConnectedDuration
|
||||
{
|
||||
get
|
||||
|
|
@ -415,47 +439,41 @@ public class DebugClient : WebSocketBehavior
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DebugClient"/> class.
|
||||
/// </summary>
|
||||
public DebugClient()
|
||||
{
|
||||
Debug.LogInformation("DebugClient Created");
|
||||
Debug.Console(0, "DebugClient Created");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnOpen()
|
||||
{
|
||||
base.OnOpen();
|
||||
|
||||
var url = Context.WebSocket.Url;
|
||||
Debug.LogInformation("New WebSocket Connection from: {0}", url);
|
||||
Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url);
|
||||
|
||||
_connectionTime = DateTime.Now;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnMessage(MessageEventArgs e)
|
||||
{
|
||||
base.OnMessage(e);
|
||||
|
||||
Debug.LogVerbose("WebSocket UiClient Message: {0}", e.Data);
|
||||
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnClose(CloseEventArgs e)
|
||||
{
|
||||
base.OnClose(e);
|
||||
|
||||
Debug.LogDebug("WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
|
||||
Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
|
||||
{
|
||||
base.OnError(e);
|
||||
|
||||
Debug.LogError(e.Exception, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
|
||||
Debug.LogVerbose("Stack Trace:\r{0}", e.Exception.StackTrace);
|
||||
Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Not in use
|
||||
/// </summary>
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Not in use
|
||||
/// </summary>
|
||||
public static class NetworkComm
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -18,3 +18,5 @@ namespace PepperDash.Core;
|
|||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.PasswordManagement;
|
||||
|
||||
namespace PepperDash.Core.PasswordManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON password configuration
|
||||
/// </summary>
|
||||
|
|
@ -23,3 +23,4 @@ namespace PepperDash.Core.PasswordManagement;
|
|||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.PasswordManagement;
|
||||
|
||||
namespace PepperDash.Core.PasswordManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants
|
||||
/// </summary>
|
||||
|
|
@ -54,3 +54,4 @@ namespace PepperDash.Core.PasswordManagement;
|
|||
/// </summary>
|
||||
public const ushort StringValueChange = 201;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
using System;
|
||||
|
||||
namespace PepperDash.Core.PasswordManagement;
|
||||
|
||||
/// <summary>
|
||||
/// A class to allow user interaction with the PasswordManager
|
||||
/// </summary>
|
||||
namespace PepperDash.Core.PasswordManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a PasswordClient
|
||||
/// </summary>
|
||||
public class PasswordClient
|
||||
{
|
||||
/// <summary>
|
||||
|
|
@ -193,3 +193,4 @@ namespace PepperDash.Core.PasswordManagement;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Timers;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.PasswordManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Allows passwords to be stored and managed
|
||||
/// </summary>
|
||||
public class PasswordManager
|
||||
namespace PepperDash.Core.PasswordManagement
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a PasswordManager
|
||||
/// </summary>
|
||||
public class PasswordManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Public dictionary of known passwords
|
||||
/// </summary>
|
||||
|
|
@ -21,7 +21,7 @@ public class PasswordManager
|
|||
/// <summary>
|
||||
/// Timer used to wait until password changes have stopped before updating the dictionary
|
||||
/// </summary>
|
||||
Timer PasswordTimer;
|
||||
CTimer PasswordTimer;
|
||||
/// <summary>
|
||||
/// Timer length
|
||||
/// </summary>
|
||||
|
|
@ -79,65 +79,60 @@ public class PasswordManager
|
|||
// validate the parameters
|
||||
if (key > 0 && string.IsNullOrEmpty(password))
|
||||
{
|
||||
Debug.LogDebug("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password);
|
||||
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// if key exists, update the value
|
||||
if (_passwords.ContainsKey(key))
|
||||
if(_passwords.ContainsKey(key))
|
||||
_passwords[key] = password;
|
||||
// else add the key & value
|
||||
else
|
||||
_passwords.Add(key, password);
|
||||
|
||||
Debug.LogDebug("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]);
|
||||
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]));
|
||||
|
||||
if (PasswordTimer == null)
|
||||
{
|
||||
PasswordTimer = new Timer(PasswordTimerElapsedMs) { AutoReset = false };
|
||||
PasswordTimer.Elapsed += (s, e) => PasswordTimerElapsed(s, e);
|
||||
PasswordTimer.Start();
|
||||
Debug.LogDebug("PasswordManager.UpdatePassword: Timer Started");
|
||||
PasswordTimer = new CTimer((o) => PasswordTimerElapsed(), PasswordTimerElapsedMs);
|
||||
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started"));
|
||||
OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
|
||||
}
|
||||
else
|
||||
{
|
||||
PasswordTimer.Stop();
|
||||
PasswordTimer.Interval = PasswordTimerElapsedMs;
|
||||
PasswordTimer.Start();
|
||||
Debug.LogDebug("PasswordManager.UpdatePassword: Timer Reset");
|
||||
PasswordTimer.Reset(PasswordTimerElapsedMs);
|
||||
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset"));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e);
|
||||
Debug.LogError(e, msg);
|
||||
Debug.LogVerbose("Stack Trace:\r{0}", e.StackTrace);
|
||||
Debug.Console(1, msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timer callback function
|
||||
/// CTimer callback function
|
||||
/// </summary>
|
||||
private void PasswordTimerElapsed(object sender, ElapsedEventArgs e)
|
||||
private void PasswordTimerElapsed()
|
||||
{
|
||||
try
|
||||
{
|
||||
PasswordTimer.Stop();
|
||||
Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Timer Stopped");
|
||||
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: CTimer Stopped"));
|
||||
OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
|
||||
foreach (var pw in _passwords)
|
||||
{
|
||||
// if key exists, continue
|
||||
if (Passwords.ContainsKey(pw.Key))
|
||||
{
|
||||
Debug.LogDebug("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value);
|
||||
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value));
|
||||
if (Passwords[pw.Key] != _passwords[pw.Key])
|
||||
{
|
||||
Passwords[pw.Key] = _passwords[pw.Key];
|
||||
Debug.LogDebug("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]);
|
||||
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]));
|
||||
OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange);
|
||||
}
|
||||
}
|
||||
|
|
@ -149,11 +144,10 @@ public class PasswordManager
|
|||
}
|
||||
OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception e)
|
||||
{
|
||||
var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", ex.Message);
|
||||
Debug.LogError(ex, msg);
|
||||
Debug.LogVerbose("Stack Trace:\r{0}", ex.StackTrace);
|
||||
var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", e);
|
||||
Debug.Console(1, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,12 +168,12 @@ public class PasswordManager
|
|||
/// </summary>
|
||||
public void ListPasswords()
|
||||
{
|
||||
Debug.LogInformation("PasswordManager.ListPasswords:\r");
|
||||
Debug.Console(0, "PasswordManager.ListPasswords:\r");
|
||||
foreach (var pw in Passwords)
|
||||
Debug.LogInformation("Passwords[{0}]: {1}\r", pw.Key, pw.Value);
|
||||
Debug.LogInformation("\n");
|
||||
Debug.Console(0, "Passwords[{0}]: {1}\r", pw.Key, pw.Value);
|
||||
Debug.Console(0, "\n");
|
||||
foreach (var pw in _passwords)
|
||||
Debug.LogInformation("_passwords[{0}]: {1}\r", pw.Key, pw.Value);
|
||||
Debug.Console(0, "_passwords[{0}]: {1}\r", pw.Key, pw.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -249,4 +243,5 @@ public class PasswordManager
|
|||
PasswordChange(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<PropertyGroup>
|
||||
<RootNamespace>PepperDash.Core</RootNamespace>
|
||||
<AssemblyName>PepperDashCore</AssemblyName>
|
||||
<TargetFramework>net8</TargetFramework>
|
||||
<TargetFramework>net472</TargetFramework>
|
||||
<Deterministic>true</Deterministic>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
|
|
@ -37,21 +37,23 @@
|
|||
<None Remove="Properties\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.1" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.128" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4">
|
||||
<Aliases>global,NewtonsoftJson</Aliases>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2025.0.0" />
|
||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PepperDash.Core.Abstractions\PepperDash.Core.Abstractions.csproj" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.274" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="SSH.NET" Version="2024.2.0" />
|
||||
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net6'">
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Comm\._GenericSshClient.cs" />
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.SystemInfo;
|
||||
|
||||
namespace PepperDash.Core.SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Constants
|
||||
/// </summary>
|
||||
|
|
@ -261,3 +261,4 @@ namespace PepperDash.Core.SystemInfo;
|
|||
Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.SystemInfo;
|
||||
|
||||
namespace PepperDash.Core.SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Processor info class
|
||||
/// </summary>
|
||||
|
|
@ -201,3 +201,4 @@ namespace PepperDash.Core.SystemInfo;
|
|||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.SystemInfo;
|
||||
|
||||
namespace PepperDash.Core.SystemInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// System Info class
|
||||
/// </summary>
|
||||
|
|
@ -468,3 +468,4 @@ namespace PepperDash.Core.SystemInfo;
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,13 +20,13 @@ using Org.BouncyCastle.Crypto.Operators;
|
|||
using BigInteger = Org.BouncyCastle.Math.BigInteger;
|
||||
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
|
||||
|
||||
namespace PepperDash.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
|
||||
/// </summary>
|
||||
internal class BouncyCertificate
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
|
||||
/// </summary>
|
||||
internal class BouncyCertificate
|
||||
{
|
||||
public string CertificatePassword { get; set; } = "password";
|
||||
public X509Certificate2 LoadCertificate(string issuerFileName, string password)
|
||||
{
|
||||
|
|
@ -35,6 +35,9 @@ internal class BouncyCertificate
|
|||
return issuerCertificate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IssueCertificate method
|
||||
/// </summary>
|
||||
public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||
{
|
||||
// It's self-signed, so these are the same.
|
||||
|
|
@ -56,6 +59,9 @@ internal class BouncyCertificate
|
|||
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CreateCertificateAuthorityCertificate method
|
||||
/// </summary>
|
||||
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||
{
|
||||
// It's self-signed, so these are the same.
|
||||
|
|
@ -78,6 +84,9 @@ internal class BouncyCertificate
|
|||
return ConvertCertificate(certificate, subjectKeyPair, random);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CreateSelfSignedCertificate method
|
||||
/// </summary>
|
||||
public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
|
||||
{
|
||||
// It's self-signed, so these are the same.
|
||||
|
|
@ -305,6 +314,9 @@ internal class BouncyCertificate
|
|||
return convertedCertificate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WriteCertificate method
|
||||
/// </summary>
|
||||
public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName)
|
||||
{
|
||||
// This password is the one attached to the PFX file. Use 'null' for no password.
|
||||
|
|
@ -332,6 +344,9 @@ internal class BouncyCertificate
|
|||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// AddCertToStore method
|
||||
/// </summary>
|
||||
public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl)
|
||||
{
|
||||
bool bRet = false;
|
||||
|
|
@ -352,4 +367,5 @@ internal class BouncyCertificate
|
|||
|
||||
return bRet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using Crestron.SimplSharp.WebScripting;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Web API default request handler
|
||||
/// Represents a DefaultRequestHandler
|
||||
/// </summary>
|
||||
public class DefaultRequestHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
|
|
@ -14,3 +14,4 @@ namespace PepperDash.Core.Web.RequestHandlers;
|
|||
: base(true)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
|
||||
namespace PepperDash.Core.Web.RequestHandlers
|
||||
{
|
||||
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
|
||||
{
|
||||
private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers;
|
||||
protected readonly bool EnableCors;
|
||||
|
||||
|
|
@ -142,6 +142,9 @@ public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
|
|||
/// Process request
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <summary>
|
||||
/// ProcessRequest method
|
||||
/// </summary>
|
||||
public void ProcessRequest(HttpCwsContext context)
|
||||
{
|
||||
if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func<HttpCwsContext, Task> handler))
|
||||
|
|
@ -159,4 +162,5 @@ public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
|
|||
|
||||
handlerTask.GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
using System.Collections.Generic;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// CWS Base Handler, implements IHttpCwsHandler
|
||||
/// </summary>
|
||||
|
|
@ -165,3 +165,4 @@ namespace PepperDash.Core.Web.RequestHandlers;
|
|||
handler(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,29 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core.Logging;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Core.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Web API server
|
||||
/// </summary>
|
||||
public class WebApiServer : IKeyName
|
||||
namespace PepperDash.Core.Web
|
||||
{
|
||||
/// <summary>
|
||||
/// Web API server
|
||||
/// </summary>
|
||||
public class WebApiServer : IKeyName
|
||||
{
|
||||
private const string SplusKey = "Uninitialized Web API Server";
|
||||
private const string DefaultName = "Web API Server";
|
||||
private const string DefaultBasePath = "/api";
|
||||
|
||||
private readonly object _serverLock = new();
|
||||
private const uint DebugTrace = 0;
|
||||
private const uint DebugInfo = 1;
|
||||
private const uint DebugVerbose = 2;
|
||||
|
||||
private readonly CCriticalSection _serverLock = new CCriticalSection();
|
||||
private HttpCwsServer _server;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -41,6 +46,22 @@ public class WebApiServer : IKeyName
|
|||
/// </summary>
|
||||
public bool IsRegistered { get; private set; }
|
||||
|
||||
//public IHttpCwsHandler HttpRequestHandler
|
||||
//{
|
||||
// get { return _server.HttpRequestHandler; }
|
||||
// set
|
||||
// {
|
||||
// if (_server == null) return;
|
||||
// _server.HttpRequestHandler = value;
|
||||
// }
|
||||
//}
|
||||
|
||||
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
|
||||
//{
|
||||
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
|
||||
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for S+. Make sure to set necessary properties using init method
|
||||
/// </summary>
|
||||
|
|
@ -71,15 +92,12 @@ public class WebApiServer : IKeyName
|
|||
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
|
||||
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
|
||||
|
||||
this.LogInformation("Creating Web API Server with Key: {Key}, Name: {Name}, BasePath: {BasePath}", Key, Name, BasePath);
|
||||
|
||||
if (_server == null) _server = new HttpCwsServer(BasePath);
|
||||
|
||||
_server.AuthenticateAllRoutes = false;
|
||||
|
||||
_server.setProcessName(Key);
|
||||
_server.HttpRequestHandler = new DefaultRequestHandler();
|
||||
_server.ReceivedRequestEvent += ReceivedRequestEventHandler;
|
||||
|
||||
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
|
||||
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
|
||||
|
|
@ -104,14 +122,10 @@ public class WebApiServer : IKeyName
|
|||
/// <param name="ethernetEventArgs"></param>
|
||||
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
|
||||
{
|
||||
if (ethernetEventArgs.EthernetEventType != eEthernetEventType.LinkUp)
|
||||
// Re-enable the server if the link comes back up and the status should be connected
|
||||
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRegistered)
|
||||
{
|
||||
this.LogInformation("Ethernet link up. Server is already registered.");
|
||||
this.LogInformation("Ethernet link up. Server is alreedy registered.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -120,14 +134,14 @@ public class WebApiServer : IKeyName
|
|||
Start();
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// Initialize method
|
||||
// /// </summary>
|
||||
// public void Initialize(string key, string basePath)
|
||||
// {
|
||||
// Key = key;
|
||||
// BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
|
||||
// }
|
||||
/// <summary>
|
||||
/// Initialize method
|
||||
/// </summary>
|
||||
public void Initialize(string key, string basePath)
|
||||
{
|
||||
Key = key;
|
||||
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a route to CWS
|
||||
|
|
@ -148,9 +162,6 @@ public class WebApiServer : IKeyName
|
|||
/// Removes a route from CWS
|
||||
/// </summary>
|
||||
/// <param name="route"></param>
|
||||
/// <summary>
|
||||
/// RemoveRoute method
|
||||
/// </summary>
|
||||
public void RemoveRoute(HttpCwsRoute route)
|
||||
{
|
||||
if (route == null)
|
||||
|
|
@ -190,60 +201,69 @@ public class WebApiServer : IKeyName
|
|||
/// Starts CWS instance
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
lock (_serverLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverLock.Enter();
|
||||
|
||||
if (_server == null)
|
||||
{
|
||||
this.LogDebug("Server is null, unable to start");
|
||||
this.LogWarning("Server is null, unable to start");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsRegistered)
|
||||
{
|
||||
this.LogDebug("Server has already been started");
|
||||
this.LogWarning("Server has already been started");
|
||||
return;
|
||||
}
|
||||
|
||||
IsRegistered = _server.Register();
|
||||
|
||||
this.LogDebug("Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
|
||||
this.LogInformation("Starting server, registration {registrationResult}", IsRegistered ? "was successful" : "failed");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Start Exception Message: {0}", ex.Message);
|
||||
this.LogVerbose("Start Exception StackTrace: {0}", ex.StackTrace);
|
||||
this.LogError("Start Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "Start Exception StackTrace");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serverLock.Leave();
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop method
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
lock (_serverLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverLock.Enter();
|
||||
|
||||
if (_server == null)
|
||||
{
|
||||
this.LogDebug("Server is null or has already been stopped");
|
||||
this.LogWarning("Server is null or has already been stopped");
|
||||
return;
|
||||
}
|
||||
|
||||
var unregistered = _server.Unregister();
|
||||
IsRegistered = !unregistered;
|
||||
IsRegistered = _server.Unregister() == false;
|
||||
|
||||
this.LogDebug("Stopping server, unregistration {0}", unregistered ? "was successful" : "failed");
|
||||
this.LogInformation("Stopping server, unregistration {unregistrationResult}", IsRegistered ? "failed" : "was successful");
|
||||
|
||||
_server.Dispose();
|
||||
_server = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "Server Stop Exception Message: {0}", ex.Message);
|
||||
this.LogError("Server Stop Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "Server Stop Exception StackTrace");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serverLock.Leave();
|
||||
}
|
||||
} // end lock
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -258,12 +278,14 @@ public class WebApiServer : IKeyName
|
|||
{
|
||||
try
|
||||
{
|
||||
var req = args.Context?.Request;
|
||||
this.LogVerbose("ReceivedRequestEventHandler: {Method} {Path}", req?.HttpMethod, req?.Path);
|
||||
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
|
||||
this.LogVerbose("RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogException(ex, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
|
||||
this.LogError("ReceivedRequestEventHandler Exception Message: {message}", ex.Message);
|
||||
this.LogDebug(ex, "ReceivedRequestEventHandler Exception StackTrace: {stackTrace}", ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/PepperDash.Core/WebApi/Presets/Preset.cs
Normal file
87
src/PepperDash.Core/WebApi/Presets/Preset.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
|
||||
namespace PepperDash.Core.WebApi.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Preset
|
||||
/// </summary>
|
||||
public class Preset
|
||||
{
|
||||
/// <summary>
|
||||
/// ID of preset
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UserId
|
||||
/// </summary>
|
||||
public int UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RoomTypeId
|
||||
/// </summary>
|
||||
public int RoomTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PresetName
|
||||
/// </summary>
|
||||
public string PresetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PresetNumber
|
||||
/// </summary>
|
||||
public int PresetNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Data
|
||||
/// </summary>
|
||||
public string Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public Preset()
|
||||
{
|
||||
PresetName = "";
|
||||
PresetNumber = 1;
|
||||
Data = "{}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a PresetReceivedEventArgs
|
||||
/// </summary>
|
||||
public class PresetReceivedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True when the preset is found
|
||||
/// </summary>
|
||||
public bool LookupSuccess { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ULookupSuccess
|
||||
/// </summary>
|
||||
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Preset
|
||||
/// </summary>
|
||||
public Preset Preset { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// For Simpl+
|
||||
/// </summary>
|
||||
public PresetReceivedEventArgs() { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="preset"></param>
|
||||
/// <param name="success"></param>
|
||||
public PresetReceivedEventArgs(Preset preset, bool success)
|
||||
{
|
||||
LookupSuccess = success;
|
||||
Preset = preset;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
src/PepperDash.Core/WebApi/Presets/User.cs
Normal file
93
src/PepperDash.Core/WebApi/Presets/User.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp;
|
||||
|
||||
namespace PepperDash.Core.WebApi.Presets
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class User
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ExternalId
|
||||
/// </summary>
|
||||
public string ExternalId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FirstName
|
||||
/// </summary>
|
||||
public string FirstName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LastName
|
||||
/// </summary>
|
||||
public string LastName { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class UserReceivedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True when user is found
|
||||
/// </summary>
|
||||
public bool LookupSuccess { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ULookupSuccess
|
||||
/// </summary>
|
||||
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the User
|
||||
/// </summary>
|
||||
public User User { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// For Simpl+
|
||||
/// </summary>
|
||||
public UserReceivedEventArgs() { }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="success"></param>
|
||||
public UserReceivedEventArgs(User user, bool success)
|
||||
{
|
||||
LookupSuccess = success;
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a UserAndRoomMessage
|
||||
/// </summary>
|
||||
public class UserAndRoomMessage
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public int UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the RoomTypeId
|
||||
/// </summary>
|
||||
public int RoomTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PresetNumber
|
||||
/// </summary>
|
||||
public int PresetNumber { get; set; }
|
||||
}
|
||||
}
|
||||
283
src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs
Normal file
283
src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
using System;
|
||||
using Crestron.SimplSharp; // For Basic SIMPL# Classes
|
||||
using Crestron.SimplSharp.CrestronIO;
|
||||
using Crestron.SimplSharp.Net.Http;
|
||||
using Crestron.SimplSharp.Net.Https;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using PepperDash.Core.JsonToSimpl;
|
||||
|
||||
|
||||
namespace PepperDash.Core.WebApi.Presets
|
||||
{
|
||||
/// <summary>
|
||||
/// Passcode client for the WebApi
|
||||
/// </summary>
|
||||
[Obsolete("WebApiPasscodeClient is no longer supported and will be removed in a future release.")]
|
||||
public class WebApiPasscodeClient : IKeyed
|
||||
{
|
||||
/// <summary>
|
||||
/// Notifies when user received
|
||||
/// </summary>
|
||||
public event EventHandler<UserReceivedEventArgs> UserReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Notifies when Preset received
|
||||
/// </summary>
|
||||
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Key
|
||||
/// </summary>
|
||||
public string Key { get; private set; }
|
||||
|
||||
//string JsonMasterKey;
|
||||
|
||||
/// <summary>
|
||||
/// An embedded JsonToSimpl master object.
|
||||
/// </summary>
|
||||
JsonToSimplGenericMaster J2SMaster;
|
||||
|
||||
string UrlBase;
|
||||
|
||||
string DefaultPresetJsonFilePath;
|
||||
|
||||
User CurrentUser;
|
||||
|
||||
Preset CurrentPreset;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please
|
||||
/// use an Initialize method
|
||||
/// </summary>
|
||||
public WebApiPasscodeClient()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the instance
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="jsonMasterKey"></param>
|
||||
/// <param name="urlBase"></param>
|
||||
/// <param name="defaultPresetJsonFilePath"></param>
|
||||
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
|
||||
{
|
||||
Key = key;
|
||||
//JsonMasterKey = jsonMasterKey;
|
||||
UrlBase = urlBase;
|
||||
DefaultPresetJsonFilePath = defaultPresetJsonFilePath;
|
||||
|
||||
J2SMaster = new JsonToSimplGenericMaster();
|
||||
J2SMaster.SaveCallback = this.SaveCallback;
|
||||
J2SMaster.Initialize(jsonMasterKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user for a passcode
|
||||
/// </summary>
|
||||
/// <param name="passcode"></param>
|
||||
/// <summary>
|
||||
/// GetUserForPasscode method
|
||||
/// </summary>
|
||||
public void GetUserForPasscode(string passcode)
|
||||
{
|
||||
// Bullshit duplicate code here... These two cases should be the same
|
||||
// except for https/http and the certificate ignores
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
var jo = new JObject();
|
||||
jo.Add("pin", passcode);
|
||||
req.ContentString = jo.ToString();
|
||||
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
var resp = client.Dispatch(req);
|
||||
var handler = UserReceived;
|
||||
if (resp.Code == 200)
|
||||
{
|
||||
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
|
||||
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
|
||||
CurrentUser = user;
|
||||
if (handler != null)
|
||||
handler(this, new UserReceivedEventArgs(user, true));
|
||||
}
|
||||
else
|
||||
if (handler != null)
|
||||
handler(this, new UserReceivedEventArgs(null, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="roomTypeId"></param>
|
||||
/// <param name="presetNumber"></param>
|
||||
/// <summary>
|
||||
/// GetPresetForThisUser method
|
||||
/// </summary>
|
||||
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
|
||||
{
|
||||
if (CurrentUser == null)
|
||||
{
|
||||
CrestronConsole.PrintLine("GetPresetForThisUser no user loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = new UserAndRoomMessage
|
||||
{
|
||||
UserId = CurrentUser.Id,
|
||||
RoomTypeId = roomTypeId,
|
||||
PresetNumber = presetNumber
|
||||
};
|
||||
|
||||
var handler = PresetReceived;
|
||||
try
|
||||
{
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
req.ContentString = JsonConvert.SerializeObject(msg);
|
||||
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
|
||||
// ask for the preset
|
||||
var resp = client.Dispatch(req);
|
||||
if (resp.Code == 200) // got it
|
||||
{
|
||||
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
|
||||
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
|
||||
CurrentPreset = preset;
|
||||
|
||||
//if there's no preset data, load the template
|
||||
if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0)
|
||||
{
|
||||
//Debug.Console(1, this, "Loaded preset has no data. Loading default template.");
|
||||
LoadDefaultPresetData();
|
||||
return;
|
||||
}
|
||||
|
||||
J2SMaster.LoadWithJson(preset.Data);
|
||||
if (handler != null)
|
||||
handler(this, new PresetReceivedEventArgs(preset, true));
|
||||
}
|
||||
else // no existing preset
|
||||
{
|
||||
CurrentPreset = new Preset();
|
||||
LoadDefaultPresetData();
|
||||
if (handler != null)
|
||||
handler(this, new PresetReceivedEventArgs(null, false));
|
||||
}
|
||||
}
|
||||
catch (HttpException e)
|
||||
{
|
||||
var resp = e.Response;
|
||||
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
|
||||
LoadDefaultPresetData();
|
||||
if (handler != null)
|
||||
PresetReceived(this, new PresetReceivedEventArgs(null, false));
|
||||
}
|
||||
}
|
||||
|
||||
void LoadDefaultPresetData()
|
||||
{
|
||||
CurrentPreset = null;
|
||||
if (!File.Exists(DefaultPresetJsonFilePath))
|
||||
{
|
||||
Debug.Console(0, this, "Cannot load default preset file. Saving will not work");
|
||||
return;
|
||||
}
|
||||
using (StreamReader sr = new StreamReader(DefaultPresetJsonFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = sr.ReadToEnd();
|
||||
J2SMaster.SetJsonWithoutEvaluating(data);
|
||||
CurrentPreset = new Preset() { Data = data, UserId = CurrentUser.Id };
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Console(0, this, "Error reading default preset JSON: \r{0}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="roomTypeId"></param>
|
||||
/// <param name="presetNumber"></param>
|
||||
/// <summary>
|
||||
/// SavePresetForThisUser method
|
||||
/// </summary>
|
||||
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
|
||||
{
|
||||
if (CurrentPreset == null)
|
||||
LoadDefaultPresetData();
|
||||
//return;
|
||||
|
||||
//// A new preset needs to have its numbers set
|
||||
//if (CurrentPreset.IsNewPreset)
|
||||
//{
|
||||
CurrentPreset.UserId = CurrentUser.Id;
|
||||
CurrentPreset.RoomTypeId = roomTypeId;
|
||||
CurrentPreset.PresetNumber = presetNumber;
|
||||
//}
|
||||
J2SMaster.Save(); // Will trigger callback when ready
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After save operation on JSON master happens, send it to server
|
||||
/// </summary>
|
||||
/// <param name="json"></param>
|
||||
void SaveCallback(string json)
|
||||
{
|
||||
CurrentPreset.Data = json;
|
||||
|
||||
if (!UrlBase.StartsWith("https"))
|
||||
return;
|
||||
var req = new HttpsClientRequest();
|
||||
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
|
||||
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
|
||||
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
|
||||
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
|
||||
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
|
||||
|
||||
var client = new HttpsClient();
|
||||
client.HostVerification = false;
|
||||
client.PeerVerification = false;
|
||||
try
|
||||
{
|
||||
var resp = client.Dispatch(req);
|
||||
|
||||
// 201=created
|
||||
// 204=empty content
|
||||
if (resp.Code == 201)
|
||||
CrestronConsole.PrintLine("Preset added");
|
||||
else if (resp.Code == 204)
|
||||
CrestronConsole.PrintLine("Preset updated");
|
||||
else if (resp.Code == 209)
|
||||
CrestronConsole.PrintLine("Preset already exists. Cannot save as new.");
|
||||
else
|
||||
CrestronConsole.PrintLine("Preset save failed: {0}\r", resp.Code, resp.ContentString);
|
||||
}
|
||||
catch (HttpException e)
|
||||
{
|
||||
|
||||
CrestronConsole.PrintLine("Preset save exception {0}", e.Response.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
using System.Collections.Generic;
|
||||
using PepperDash.Core.Intersystem.Tokens;
|
||||
|
||||
namespace PepperDash.Core.Intersystem.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Interface to determine XSig serialization for an object.
|
||||
/// </summary>
|
||||
public interface IXSigSerialization
|
||||
namespace PepperDash.Core.Intersystem.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface to determine XSig serialization for an object.
|
||||
/// </summary>
|
||||
public interface IXSigSerialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize the sig data
|
||||
/// </summary>
|
||||
|
|
@ -21,4 +21,5 @@ public interface IXSigSerialization
|
|||
/// <param name="tokens"></param>
|
||||
/// <returns></returns>
|
||||
T Deserialize<T>(IEnumerable<XSigToken> tokens) where T : class, IXSigSerialization;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace PepperDash.Core.Intersystem.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Class to handle this specific exception type
|
||||
/// </summary>
|
||||
public class XSigSerializationException : Exception
|
||||
namespace PepperDash.Core.Intersystem.Serialization
|
||||
{
|
||||
/// <summary>
|
||||
/// Class to handle this specific exception type
|
||||
/// </summary>
|
||||
public class XSigSerializationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// default constructor
|
||||
/// </summary>
|
||||
|
|
@ -24,4 +24,5 @@ public class XSigSerializationException : Exception
|
|||
/// <param name="message"></param>
|
||||
/// <param name="inner"></param>
|
||||
public XSigSerializationException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace PepperDash.Core.Intersystem.Tokens;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an XSigAnalogToken
|
||||
/// </summary>
|
||||
public sealed class XSigAnalogToken : XSigToken, IFormattable
|
||||
namespace PepperDash.Core.Intersystem.Tokens
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an XSigAnalogToken
|
||||
/// </summary>
|
||||
public sealed class XSigAnalogToken : XSigToken, IFormattable
|
||||
{
|
||||
private readonly ushort _value;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -59,6 +59,9 @@ public sealed class XSigAnalogToken : XSigToken, IFormattable
|
|||
/// </summary>
|
||||
/// <param name="offset"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// GetTokenWithOffset method
|
||||
/// </summary>
|
||||
public override XSigToken GetTokenWithOffset(int offset)
|
||||
{
|
||||
if (offset == 0) return this;
|
||||
|
|
@ -69,6 +72,10 @@ public sealed class XSigAnalogToken : XSigToken, IFormattable
|
|||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Index + " = 0x" + Value.ToString("X4");
|
||||
|
|
@ -80,8 +87,12 @@ public sealed class XSigAnalogToken : XSigToken, IFormattable
|
|||
/// <param name="format"></param>
|
||||
/// <param name="formatProvider"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
public string ToString(string format, IFormatProvider formatProvider)
|
||||
{
|
||||
return Value.ToString(format, formatProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
using System;
|
||||
|
||||
namespace PepperDash.Core.Intersystem.Tokens;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an XSigDigitalToken
|
||||
/// </summary>
|
||||
public sealed class XSigDigitalToken : XSigToken
|
||||
namespace PepperDash.Core.Intersystem.Tokens
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an XSigDigitalToken
|
||||
/// </summary>
|
||||
public sealed class XSigDigitalToken : XSigToken
|
||||
{
|
||||
private readonly bool _value;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -57,6 +57,9 @@ public sealed class XSigDigitalToken : XSigToken
|
|||
/// </summary>
|
||||
/// <param name="offset"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// GetTokenWithOffset method
|
||||
/// </summary>
|
||||
public override XSigToken GetTokenWithOffset(int offset)
|
||||
{
|
||||
if (offset == 0) return this;
|
||||
|
|
@ -67,6 +70,10 @@ public sealed class XSigDigitalToken : XSigToken
|
|||
///
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Index + " = " + (Value ? "High" : "Low");
|
||||
|
|
@ -77,8 +84,12 @@ public sealed class XSigDigitalToken : XSigToken
|
|||
/// </summary>
|
||||
/// <param name="formatProvider"></param>
|
||||
/// <returns></returns>
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
public string ToString(IFormatProvider formatProvider)
|
||||
{
|
||||
return Value.ToString(formatProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue