From 7fe6d523637b61ada41d1544f41a3cb8d1cdb328 Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 17 Feb 2021 19:48:23 +0000 Subject: [PATCH 1/4] Version 1 of the LynxAstro.DewController.Switch driver --- .gitignore | 222 ++++++ ...xAstro.DewController.Switch.Validation.txt | 349 +++++++++ .../AscomLocalServer.wxs | 42 ++ .../AscomSwitchDriver.wxs | 24 + LynxAstro.DewController.Setup/Config.wxi | 44 ++ LynxAstro.DewController.Setup/Directories.wxs | 13 + LynxAstro.DewController.Setup/FeatureTree.wxs | 14 + .../InstallationUI.wxs | 81 ++ LynxAstro.DewController.Setup/License.rtf | Bin 0 -> 1457 bytes .../LynxAstro.DewController.Setup.wixproj | 74 ++ LynxAstro.DewController.Setup/Product.wxs | 48 ++ ...stro.DewController.Switch.UnitTests.csproj | 120 +++ .../Properties/AssemblyInfo.cs | 36 + .../SwitchUnitTests.cs | 27 + .../packages.config | 9 + LynxAstro.DewController.Switch/ASCOM.ico | Bin 0 -> 125783 bytes LynxAstro.DewController.Switch/ASCOM.png | Bin 0 -> 1922 bytes .../ASCOMDriverTemplate.snk | Bin 0 -> 596 bytes .../LynxAstro.DewController.Switch.csproj | 151 ++++ .../Properties/AssemblyInfo.cs | 38 + .../Properties/Resources.Designer.cs | 96 +++ .../Properties/Resources.resx | 127 ++++ .../Properties/Settings.Designer.cs | 30 + .../Properties/Settings.settings | 5 + LynxAstro.DewController.Switch/ReadMe.htm | 147 ++++ .../Resources/ASCOM.bmp | Bin 0 -> 3382 bytes LynxAstro.DewController.Switch/Switch.cs | 695 ++++++++++++++++++ LynxAstro.DewController.Switch/app.config | 8 + ...LynxAstro.DewController.TestConsole.csproj | 64 ++ .../Program.cs | 78 ++ .../Properties/AssemblyInfo.cs | 36 + .../app.config | 3 + .../LynxAstro.DewController.UnitTests.csproj | 115 +++ .../Properties/AssemblyInfo.cs | 35 + .../SharedResourcesUnitTests.cs | 189 +++++ .../packages.config | 9 + LynxAstro.DewController.sln | 88 +++ LynxAstro.DewController/AssemblyInfo.cs | 91 +++ LynxAstro.DewController/ClassFactory.cs | 244 ++++++ LynxAstro.DewController/ConnectionInfo.cs | 8 + LynxAstro.DewController/GarbageCollection.cs | 57 ++ LynxAstro.DewController/IProfileFactory.cs | 7 + LynxAstro.DewController/IProfileWrapper.cs | 10 + .../ISharedResourcesWrapper.cs | 28 + LynxAstro.DewController/LocalServer.cs | 640 ++++++++++++++++ LynxAstro.DewController/LocalServer.snk | Bin 0 -> 596 bytes .../LynxAstro.DewController.csproj | 191 +++++ LynxAstro.DewController/ProfileFactory.cs | 10 + LynxAstro.DewController/ProfileProperties.cs | 12 + LynxAstro.DewController/ProfileWrapper.cs | 132 ++++ .../Properties/AssemblyInfo.cs | 26 + .../Properties/Resources.Designer.cs | 82 +++ .../Properties/Resources.resx | 127 ++++ LynxAstro.DewController/ReadMe.htm | 666 +++++++++++++++++ .../ReferenceCountedObject.cs | 23 + LynxAstro.DewController/Resources/ASCOM.png | Bin 0 -> 1922 bytes LynxAstro.DewController/SetupDialogForm.cs | 89 +++ .../SetupDialogForm.designer.cs | 149 ++++ LynxAstro.DewController/SetupDialogForm.resx | 120 +++ LynxAstro.DewController/SharedResources.cs | 379 ++++++++++ .../SharedResourcesWrapper.cs | 70 ++ LynxAstro.DewController/frmMain.Designer.cs | 63 ++ LynxAstro.DewController/frmMain.cs | 15 + LynxAstro.DewController/frmMain.resx | 120 +++ LynxAstro.DewController/packages.config | 5 + 65 files changed, 6381 insertions(+) create mode 100644 .gitignore create mode 100644 ASCOM.LynxAstro.DewController.Switch.Validation.txt create mode 100644 LynxAstro.DewController.Setup/AscomLocalServer.wxs create mode 100644 LynxAstro.DewController.Setup/AscomSwitchDriver.wxs create mode 100644 LynxAstro.DewController.Setup/Config.wxi create mode 100644 LynxAstro.DewController.Setup/Directories.wxs create mode 100644 LynxAstro.DewController.Setup/FeatureTree.wxs create mode 100644 LynxAstro.DewController.Setup/InstallationUI.wxs create mode 100644 LynxAstro.DewController.Setup/License.rtf create mode 100644 LynxAstro.DewController.Setup/LynxAstro.DewController.Setup.wixproj create mode 100644 LynxAstro.DewController.Setup/Product.wxs create mode 100644 LynxAstro.DewController.Switch.UnitTests/LynxAstro.DewController.Switch.UnitTests.csproj create mode 100644 LynxAstro.DewController.Switch.UnitTests/Properties/AssemblyInfo.cs create mode 100644 LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs create mode 100644 LynxAstro.DewController.Switch.UnitTests/packages.config create mode 100644 LynxAstro.DewController.Switch/ASCOM.ico create mode 100644 LynxAstro.DewController.Switch/ASCOM.png create mode 100644 LynxAstro.DewController.Switch/ASCOMDriverTemplate.snk create mode 100644 LynxAstro.DewController.Switch/LynxAstro.DewController.Switch.csproj create mode 100644 LynxAstro.DewController.Switch/Properties/AssemblyInfo.cs create mode 100644 LynxAstro.DewController.Switch/Properties/Resources.Designer.cs create mode 100644 LynxAstro.DewController.Switch/Properties/Resources.resx create mode 100644 LynxAstro.DewController.Switch/Properties/Settings.Designer.cs create mode 100644 LynxAstro.DewController.Switch/Properties/Settings.settings create mode 100644 LynxAstro.DewController.Switch/ReadMe.htm create mode 100644 LynxAstro.DewController.Switch/Resources/ASCOM.bmp create mode 100644 LynxAstro.DewController.Switch/Switch.cs create mode 100644 LynxAstro.DewController.Switch/app.config create mode 100644 LynxAstro.DewController.TestConsole/LynxAstro.DewController.TestConsole.csproj create mode 100644 LynxAstro.DewController.TestConsole/Program.cs create mode 100644 LynxAstro.DewController.TestConsole/Properties/AssemblyInfo.cs create mode 100644 LynxAstro.DewController.TestConsole/app.config create mode 100644 LynxAstro.DewController.UnitTests/LynxAstro.DewController.UnitTests.csproj create mode 100644 LynxAstro.DewController.UnitTests/Properties/AssemblyInfo.cs create mode 100644 LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs create mode 100644 LynxAstro.DewController.UnitTests/packages.config create mode 100644 LynxAstro.DewController.sln create mode 100644 LynxAstro.DewController/AssemblyInfo.cs create mode 100644 LynxAstro.DewController/ClassFactory.cs create mode 100644 LynxAstro.DewController/ConnectionInfo.cs create mode 100644 LynxAstro.DewController/GarbageCollection.cs create mode 100644 LynxAstro.DewController/IProfileFactory.cs create mode 100644 LynxAstro.DewController/IProfileWrapper.cs create mode 100644 LynxAstro.DewController/ISharedResourcesWrapper.cs create mode 100644 LynxAstro.DewController/LocalServer.cs create mode 100644 LynxAstro.DewController/LocalServer.snk create mode 100644 LynxAstro.DewController/LynxAstro.DewController.csproj create mode 100644 LynxAstro.DewController/ProfileFactory.cs create mode 100644 LynxAstro.DewController/ProfileProperties.cs create mode 100644 LynxAstro.DewController/ProfileWrapper.cs create mode 100644 LynxAstro.DewController/Properties/AssemblyInfo.cs create mode 100644 LynxAstro.DewController/Properties/Resources.Designer.cs create mode 100644 LynxAstro.DewController/Properties/Resources.resx create mode 100644 LynxAstro.DewController/ReadMe.htm create mode 100644 LynxAstro.DewController/ReferenceCountedObject.cs create mode 100644 LynxAstro.DewController/Resources/ASCOM.png create mode 100644 LynxAstro.DewController/SetupDialogForm.cs create mode 100644 LynxAstro.DewController/SetupDialogForm.designer.cs create mode 100644 LynxAstro.DewController/SetupDialogForm.resx create mode 100644 LynxAstro.DewController/SharedResources.cs create mode 100644 LynxAstro.DewController/SharedResourcesWrapper.cs create mode 100644 LynxAstro.DewController/frmMain.Designer.cs create mode 100644 LynxAstro.DewController/frmMain.cs create mode 100644 LynxAstro.DewController/frmMain.resx create mode 100644 LynxAstro.DewController/packages.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6bb49f --- /dev/null +++ b/.gitignore @@ -0,0 +1,222 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* +*ncrunchsolution.user + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# nCrunch items +*.ncrunchsolution +*.DotSettings +*.ncrunchproject diff --git a/ASCOM.LynxAstro.DewController.Switch.Validation.txt b/ASCOM.LynxAstro.DewController.Switch.Validation.txt new file mode 100644 index 0000000..c87bb2f --- /dev/null +++ b/ASCOM.LynxAstro.DewController.Switch.Validation.txt @@ -0,0 +1,349 @@ +Conform Report Hash (V1): 5A431672269EA8E8AC10FDAB37C418D8AC06BDA9D013E7B57D8D46B4494A80B03209D002BC052DA6FDDC36320956A15ECA4AA1ED59B40D7BE27F35854C5B8CAF + + +ConformanceCheck ASCOM Device Conformance Checker Version 6.5.7500.22514, Build time: 14/07/2020 13:30:28 +ConformanceCheck Running on: ASCOM Platform 6.5 SP1 6.5.1.3234 + +ConformanceCheck Driver ProgID: ASCOM.LynxAstro.DewController.Switch + +Error handling +Error number for "Not Implemented" is: 80040400 +Error number for "Invalid Value 1" is: 80040405 +Error number for "Value Not Set 1" is: 80040402 +Error number for "Value Not Set 2" is: 80040403 +Error messages will not be interpreted to infer state. + +19:28:43.452 Driver Access Checks OK +19:28:44.135 AccessChecks OK Successfully created driver using late binding +19:28:44.176 AccessChecks OK Successfully connected using late binding +19:28:44.179 AccessChecks INFO The driver is a .NET object +19:28:44.183 AccessChecks INFO The AssemblyQualifiedName is: ASCOM.LynxAstro.DewController.Switch, ASCOM.LynxAstro.DewController.Switch, +19:28:44.186 AccessChecks INFO The driver implements interface: ASCOM.DeviceInterface.ISwitchV2 +19:28:44.850 AccessChecks INFO Device does not expose interface ISwitch +19:28:44.876 AccessChecks INFO Device exposes interface ISwitchV2 +19:28:45.546 AccessChecks OK Successfully created driver using driver access toolkit +19:28:45.574 AccessChecks OK Successfully connected using driver access toolkit + +Conform is using ASCOM.DriverAccess.Switch to get a Switch object +19:28:45.612 ConformanceCheck OK Driver instance created successfully +19:28:45.700 ConformanceCheck OK Connected OK + +Common Driver Methods +19:28:45.750 InterfaceVersion OK 2 +19:28:45.790 Connected OK True +19:28:45.836 Description OK ASCOM Switch Driver for LynxAstro.DewController +19:28:45.883 DriverInfo OK ASCOM Switch Driver for LynxAstro.DewController .net driver. Version: 6.5.1.0 +19:28:45.928 DriverVersion OK 6.5.1.0 +19:28:45.975 Name OK LynxAstro.DewController +19:28:46.022 CommandString INFO Conform cannot test the CommandString method +19:28:46.026 CommandBlind INFO Conform cannot test the CommandBlind method +19:28:46.031 CommandBool INFO Conform cannot test the CommandBool method +19:28:46.036 Action INFO Conform cannot test the Action method +19:28:46.041 SupportedActions OK Driver returned an empty action list + +Properties +19:28:46.198 MaxSwitch OK 4 + +Methods +19:28:46.314 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: CanWrite +19:28:46.320 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: CanWrite +19:28:46.326 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: GetSwitch +19:28:46.331 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: GetSwitch +19:28:46.337 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: GetSwitchDescription +19:28:46.343 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: GetSwitchDescription +19:28:46.350 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: GetSwitchName +19:28:46.356 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: GetSwitchName +19:28:46.362 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: GetSwitchValue +19:28:46.368 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: GetSwitchValue +19:28:46.374 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: MaxSwitchValue +19:28:46.380 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: MaxSwitchValue +19:28:46.387 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: MinSwitchValue +19:28:46.393 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: MinSwitchValue +19:28:46.400 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: SetSwitch +19:28:46.406 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: SetSwitch +19:28:46.414 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: SetSwitchValue +19:28:46.420 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: SetSwitchValue +19:28:46.427 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID below 0 was used in method: SwitchStep +19:28:46.434 SwitchNumber OK Switch device threw an InvalidOperationException when a switch ID above MaxSwitch was used in method: SwitchStep +19:28:49.553 GetSwitchName OK Found switch 0 +19:28:49.559 GetSwitchName OK Name: +19:28:49.566 GetSwitchDescription OK Description: Control Knob +19:28:49.572 MinSwitchValue OK Minimum: 0 +19:28:49.579 MaxSwitchValue OK Maximum: 1023 +19:28:49.586 SwitchStep OK Step size: 1 +19:28:49.593 SwitchStep OK Step size is greater than zero +19:28:49.600 SwitchStep OK Step size is less than the range of possible values +19:28:49.607 SwitchStep OK The switch range is an integer multiple of the step size. +19:28:49.615 CanWrite OK CanWrite: True +19:28:49.623 GetSwitch OK False +19:28:49.633 GetSwitchValue OK 0 +19:28:49.690 SetSwitch OK GetSwitch returned False after SetSwitch(False) +19:28:49.699 SetSwitch OK GetSwitchValue returned MINIMUM_VALUE after SetSwitch(False) +19:28:49.799 SetSwitch OK GetSwitch read True after SetSwitch(True) +19:28:49.809 SetSwitch OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitch(True) +19:28:49.953 SetSwitchValue OK GetSwitch returned False after SetSwitchValue(MINIMUM_VALUE) +19:28:49.961 SetSwitchValue OK GetSwitchValue returned MINIMUM_VALUE after SetSwitchValue(MINIMUM_VALUE) +19:28:50.014 SetSwitchValue OK Switch threw an InvalidOperationException when a value below SwitchMinimum was set: -1 +19:28:50.155 SetSwitchValue OK GetSwitch returned True after SetSwitchValue(MAXIMUM_VALUE) +19:28:50.165 SetSwitchValue OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitchValue(MAXIMUM_VALUE) +19:28:50.215 SetSwitchValue OK Switch threw an InvalidOperationException when a value above SwitchMaximum was set: 1024 +19:28:50.309 SetSwitchValue INFO Testing with steps that are 0% offset from integer SwitchStep values +19:28:50.402 SetSwitchValue Offset: 0% OK Set and read match: 0 +19:28:50.572 SetSwitchValue Offset: 0% OK Set and read match: 102 +19:28:50.743 SetSwitchValue Offset: 0% OK Set and read match: 205 +19:28:50.913 SetSwitchValue Offset: 0% OK Set and read match: 307 +19:28:51.083 SetSwitchValue Offset: 0% OK Set and read match: 409 +19:28:51.253 SetSwitchValue Offset: 0% OK Set and read match: 512 +19:28:51.423 SetSwitchValue Offset: 0% OK Set and read match: 614 +19:28:51.594 SetSwitchValue Offset: 0% OK Set and read match: 716 +19:28:51.764 SetSwitchValue Offset: 0% OK Set and read match: 818 +19:28:51.934 SetSwitchValue Offset: 0% OK Set and read match: 921 +19:28:52.105 SetSwitchValue Offset: 0% OK Set and read match: 1023 +19:28:52.226 SetSwitchValue INFO Testing with steps that are 25% offset from integer SwitchStep values +19:28:52.320 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.25, Read: 0 +19:28:52.491 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.25, Read: 102 +19:28:52.661 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.25, Read: 205 +19:28:52.831 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.25, Read: 307 +19:28:53.002 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.25, Read: 409 +19:28:53.172 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.25, Read: 512 +19:28:53.342 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.25, Read: 614 +19:28:53.512 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.25, Read: 716 +19:28:53.682 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.25, Read: 818 +19:28:53.853 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.25, Read: 921 +19:28:54.005 SetSwitchValue INFO Testing with steps that are 50% offset from integer SwitchStep values +19:28:54.100 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 0.5, Read: 1 +19:28:54.270 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 102.5, Read: 103 +19:28:54.440 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 205.5, Read: 206 +19:28:54.611 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 307.5, Read: 308 +19:28:54.781 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 409.5, Read: 410 +19:28:54.951 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 512.5, Read: 513 +19:28:55.121 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 614.5, Read: 615 +19:28:55.291 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 716.5, Read: 717 +19:28:55.462 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 818.5, Read: 819 +19:28:55.632 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 921.5, Read: 922 +19:28:55.785 SetSwitchValue INFO Testing with steps that are 75% offset from integer SwitchStep values +19:28:55.879 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.75, Read: 1 +19:28:56.050 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.75, Read: 103 +19:28:56.220 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.75, Read: 206 +19:28:56.390 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.75, Read: 308 +19:28:56.561 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.75, Read: 410 +19:28:56.731 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.75, Read: 513 +19:28:56.901 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.75, Read: 615 +19:28:57.071 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.75, Read: 717 +19:28:57.241 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.75, Read: 819 +19:28:57.396 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.75, Read: 922 +19:28:57.551 SetSwitchValue OK Switch has been reset to its original state + +19:28:57.692 GetSwitchName OK Found switch 1 +19:28:57.700 GetSwitchName OK Name: +19:28:57.710 GetSwitchDescription OK Description: Control Knob +19:28:57.719 MinSwitchValue OK Minimum: 0 +19:28:57.729 MaxSwitchValue OK Maximum: 1023 +19:28:57.738 SwitchStep OK Step size: 1 +19:28:57.747 SwitchStep OK Step size is greater than zero +19:28:57.756 SwitchStep OK Step size is less than the range of possible values +19:28:57.766 SwitchStep OK The switch range is an integer multiple of the step size. +19:28:57.775 CanWrite OK CanWrite: True +19:28:57.786 GetSwitch OK True +19:28:57.798 GetSwitchValue OK 470 +19:28:57.860 SetSwitch OK GetSwitch returned False after SetSwitch(False) +19:28:57.871 SetSwitch OK GetSwitchValue returned MINIMUM_VALUE after SetSwitch(False) +19:28:58.015 SetSwitch OK GetSwitch read True after SetSwitch(True) +19:28:58.030 SetSwitch OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitch(True) +19:28:58.216 SetSwitchValue OK GetSwitch returned False after SetSwitchValue(MINIMUM_VALUE) +19:28:58.227 SetSwitchValue OK GetSwitchValue returned MINIMUM_VALUE after SetSwitchValue(MINIMUM_VALUE) +19:28:58.276 SetSwitchValue OK Switch threw an InvalidOperationException when a value below SwitchMinimum was set: -1 +19:28:58.417 SetSwitchValue OK GetSwitch returned True after SetSwitchValue(MAXIMUM_VALUE) +19:28:58.429 SetSwitchValue OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitchValue(MAXIMUM_VALUE) +19:28:58.524 SetSwitchValue OK Switch threw an InvalidOperationException when a value above SwitchMaximum was set: 1024 +19:28:58.616 SetSwitchValue INFO Testing with steps that are 0% offset from integer SwitchStep values +19:28:58.711 SetSwitchValue Offset: 0% OK Set and read match: 0 +19:28:58.882 SetSwitchValue Offset: 0% OK Set and read match: 102 +19:28:59.052 SetSwitchValue Offset: 0% OK Set and read match: 205 +19:28:59.222 SetSwitchValue Offset: 0% OK Set and read match: 307 +19:28:59.392 SetSwitchValue Offset: 0% OK Set and read match: 409 +19:28:59.562 SetSwitchValue Offset: 0% OK Set and read match: 512 +19:28:59.732 SetSwitchValue Offset: 0% OK Set and read match: 614 +19:28:59.903 SetSwitchValue Offset: 0% OK Set and read match: 716 +19:29:00.057 SetSwitchValue Offset: 0% OK Set and read match: 818 +19:29:00.228 SetSwitchValue Offset: 0% OK Set and read match: 921 +19:29:00.398 SetSwitchValue Offset: 0% OK Set and read match: 1023 +19:29:00.520 SetSwitchValue INFO Testing with steps that are 25% offset from integer SwitchStep values +19:29:00.583 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.25, Read: 0 +19:29:00.754 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.25, Read: 102 +19:29:00.924 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.25, Read: 205 +19:29:01.094 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.25, Read: 307 +19:29:01.264 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.25, Read: 409 +19:29:01.435 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.25, Read: 512 +19:29:01.605 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.25, Read: 614 +19:29:01.775 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.25, Read: 716 +19:29:01.945 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.25, Read: 818 +19:29:02.116 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.25, Read: 921 +19:29:02.268 SetSwitchValue INFO Testing with steps that are 50% offset from integer SwitchStep values +19:29:02.363 SetSwitchValue Offset: 50% INFO Set/Read differ by >100% of SwitchStep. Set: 0.5, Read: 468 +19:29:02.533 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 102.5, Read: 103 +19:29:02.703 SetSwitchValue Offset: 50% INFO Set/Read differ by >100% of SwitchStep. Set: 205.5, Read: 475 +19:29:02.874 SetSwitchValue Offset: 50% INFO Set/Read differ by >100% of SwitchStep. Set: 307.5, Read: 469 +19:29:03.044 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 409.5, Read: 410 +19:29:03.214 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 512.5, Read: 513 +19:29:03.384 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 614.5, Read: 615 +19:29:03.554 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 716.5, Read: 717 +19:29:03.724 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 818.5, Read: 819 +19:29:03.895 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 921.5, Read: 922 +19:29:04.047 SetSwitchValue INFO Testing with steps that are 75% offset from integer SwitchStep values +19:29:04.142 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.75, Read: 1 +19:29:04.312 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.75, Read: 103 +19:29:04.467 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.75, Read: 206 +19:29:04.637 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.75, Read: 308 +19:29:04.808 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.75, Read: 410 +19:29:04.978 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.75, Read: 513 +19:29:05.148 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.75, Read: 615 +19:29:05.318 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.75, Read: 717 +19:29:05.488 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.75, Read: 819 +19:29:05.658 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.75, Read: 922 +19:29:05.814 SetSwitchValue OK Switch has been reset to its original state + +19:29:05.954 GetSwitchName OK Found switch 2 +19:29:05.963 GetSwitchName OK Name: +19:29:05.972 GetSwitchDescription OK Description: Control Knob +19:29:05.981 MinSwitchValue OK Minimum: 0 +19:29:05.991 MaxSwitchValue OK Maximum: 1023 +19:29:06.001 SwitchStep OK Step size: 1 +19:29:06.010 SwitchStep OK Step size is greater than zero +19:29:06.020 SwitchStep OK Step size is less than the range of possible values +19:29:06.029 SwitchStep OK The switch range is an integer multiple of the step size. +19:29:06.039 CanWrite OK CanWrite: True +19:29:06.051 GetSwitch OK False +19:29:06.061 GetSwitchValue OK 0 +19:29:06.122 SetSwitch OK GetSwitch returned False after SetSwitch(False) +19:29:06.133 SetSwitch OK GetSwitchValue returned MINIMUM_VALUE after SetSwitch(False) +19:29:06.231 SetSwitch OK GetSwitch read True after SetSwitch(True) +19:29:06.244 SetSwitch OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitch(True) +19:29:06.431 SetSwitchValue OK GetSwitch returned False after SetSwitchValue(MINIMUM_VALUE) +19:29:06.442 SetSwitchValue OK GetSwitchValue returned MINIMUM_VALUE after SetSwitchValue(MINIMUM_VALUE) +19:29:06.492 SetSwitchValue OK Switch threw an InvalidOperationException when a value below SwitchMinimum was set: -1 +19:29:06.634 SetSwitchValue OK GetSwitch returned True after SetSwitchValue(MAXIMUM_VALUE) +19:29:06.646 SetSwitchValue OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitchValue(MAXIMUM_VALUE) +19:29:06.740 SetSwitchValue OK Switch threw an InvalidOperationException when a value above SwitchMaximum was set: 1024 +19:29:06.832 SetSwitchValue INFO Testing with steps that are 0% offset from integer SwitchStep values +19:29:06.896 SetSwitchValue Offset: 0% OK Set and read match: 0 +19:29:07.066 SetSwitchValue Offset: 0% OK Set and read match: 102 +19:29:07.237 SetSwitchValue Offset: 0% OK Set and read match: 205 +19:29:07.407 SetSwitchValue Offset: 0% OK Set and read match: 307 +19:29:07.577 SetSwitchValue Offset: 0% OK Set and read match: 409 +19:29:07.747 SetSwitchValue Offset: 0% OK Set and read match: 512 +19:29:07.917 SetSwitchValue Offset: 0% OK Set and read match: 614 +19:29:08.088 SetSwitchValue Offset: 0% OK Set and read match: 716 +19:29:08.258 SetSwitchValue Offset: 0% OK Set and read match: 818 +19:29:08.428 SetSwitchValue Offset: 0% OK Set and read match: 921 +19:29:08.599 SetSwitchValue Offset: 0% OK Set and read match: 1023 +19:29:08.720 SetSwitchValue INFO Testing with steps that are 25% offset from integer SwitchStep values +19:29:08.814 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.25, Read: 0 +19:29:08.985 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.25, Read: 102 +19:29:09.155 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.25, Read: 205 +19:29:09.325 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.25, Read: 307 +19:29:09.481 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.25, Read: 409 +19:29:09.650 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.25, Read: 512 +19:29:09.821 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.25, Read: 614 +19:29:09.991 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.25, Read: 716 +19:29:10.161 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.25, Read: 818 +19:29:10.331 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.25, Read: 921 +19:29:10.484 SetSwitchValue INFO Testing with steps that are 50% offset from integer SwitchStep values +19:29:10.578 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 0.5, Read: 1 +19:29:10.749 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 102.5, Read: 103 +19:29:10.921 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 205.5, Read: 206 +19:29:11.059 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 307.5, Read: 308 +19:29:11.229 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 409.5, Read: 410 +19:29:11.400 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 512.5, Read: 513 +19:29:11.569 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 614.5, Read: 615 +19:29:11.739 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 716.5, Read: 717 +19:29:11.910 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 818.5, Read: 819 +19:29:12.080 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 921.5, Read: 922 +19:29:12.233 SetSwitchValue INFO Testing with steps that are 75% offset from integer SwitchStep values +19:29:12.327 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.75, Read: 1 +19:29:12.497 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.75, Read: 103 +19:29:12.668 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.75, Read: 206 +19:29:12.838 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.75, Read: 308 +19:29:13.008 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.75, Read: 410 +19:29:13.178 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.75, Read: 513 +19:29:13.349 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.75, Read: 615 +19:29:13.519 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.75, Read: 717 +19:29:13.689 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.75, Read: 819 +19:29:13.859 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.75, Read: 922 +19:29:14.014 SetSwitchValue OK Switch has been reset to its original state + +19:29:14.155 GetSwitchName OK Found switch 3 +19:29:14.165 GetSwitchName OK Name: +19:29:14.174 GetSwitchDescription OK Description: Control Knob +19:29:14.184 MinSwitchValue OK Minimum: 0 +19:29:14.194 MaxSwitchValue OK Maximum: 1023 +19:29:14.205 SwitchStep OK Step size: 1 +19:29:14.214 SwitchStep OK Step size is greater than zero +19:29:14.224 SwitchStep OK Step size is less than the range of possible values +19:29:14.233 SwitchStep OK The switch range is an integer multiple of the step size. +19:29:14.243 CanWrite OK CanWrite: True +19:29:14.254 GetSwitch OK False +19:29:14.265 GetSwitchValue OK 0 +19:29:14.385 SetSwitch OK GetSwitch returned False after SetSwitch(False) +19:29:14.396 SetSwitch OK GetSwitchValue returned MINIMUM_VALUE after SetSwitch(False) +19:29:14.494 SetSwitch OK GetSwitch read True after SetSwitch(True) +19:29:14.506 SetSwitch OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitch(True) +19:29:14.694 SetSwitchValue OK GetSwitch returned False after SetSwitchValue(MINIMUM_VALUE) +19:29:14.707 SetSwitchValue OK GetSwitchValue returned MINIMUM_VALUE after SetSwitchValue(MINIMUM_VALUE) +19:29:14.802 SetSwitchValue OK Switch threw an InvalidOperationException when a value below SwitchMinimum was set: -1 +19:29:14.943 SetSwitchValue OK GetSwitch returned True after SetSwitchValue(MAXIMUM_VALUE) +19:29:14.954 SetSwitchValue OK GetSwitchValue returned MAXIMUM_VALUE after SetSwitchValue(MAXIMUM_VALUE) +19:29:15.049 SetSwitchValue OK Switch threw an InvalidOperationException when a value above SwitchMaximum was set: 1024 +19:29:15.142 SetSwitchValue INFO Testing with steps that are 0% offset from integer SwitchStep values +19:29:15.236 SetSwitchValue Offset: 0% OK Set and read match: 0 +19:29:15.407 SetSwitchValue Offset: 0% OK Set and read match: 102 +19:29:15.577 SetSwitchValue Offset: 0% OK Set and read match: 205 +19:29:15.747 SetSwitchValue Offset: 0% OK Set and read match: 307 +19:29:15.917 SetSwitchValue Offset: 0% OK Set and read match: 409 +19:29:16.087 SetSwitchValue Offset: 0% OK Set and read match: 512 +19:29:16.258 SetSwitchValue Offset: 0% OK Set and read match: 614 +19:29:16.428 SetSwitchValue Offset: 0% OK Set and read match: 716 +19:29:16.583 SetSwitchValue Offset: 0% OK Set and read match: 818 +19:29:16.753 SetSwitchValue Offset: 0% OK Set and read match: 921 +19:29:16.924 SetSwitchValue Offset: 0% OK Set and read match: 1023 +19:29:17.045 SetSwitchValue INFO Testing with steps that are 25% offset from integer SwitchStep values +19:29:17.139 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.25, Read: 0 +19:29:17.310 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.25, Read: 102 +19:29:17.480 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.25, Read: 205 +19:29:17.650 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.25, Read: 307 +19:29:17.821 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.25, Read: 409 +19:29:17.991 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.25, Read: 512 +19:29:18.161 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.25, Read: 614 +19:29:18.331 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.25, Read: 716 +19:29:18.501 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.25, Read: 818 +19:29:18.671 SetSwitchValue Offset: 25% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.25, Read: 921 +19:29:18.824 SetSwitchValue INFO Testing with steps that are 50% offset from integer SwitchStep values +19:29:18.918 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 0.5, Read: 1 +19:29:19.089 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 102.5, Read: 103 +19:29:19.260 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 205.5, Read: 206 +19:29:19.415 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 307.5, Read: 308 +19:29:19.584 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 409.5, Read: 410 +19:29:19.754 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 512.5, Read: 513 +19:29:19.925 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 614.5, Read: 615 +19:29:20.095 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 716.5, Read: 717 +19:29:20.265 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 818.5, Read: 819 +19:29:20.435 SetSwitchValue Offset: 50% INFO Set/Read differ by 40-50% of SwitchStep. Set: 921.5, Read: 922 +19:29:20.588 SetSwitchValue INFO Testing with steps that are 75% offset from integer SwitchStep values +19:29:20.682 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 0.75, Read: 1 +19:29:20.853 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 102.75, Read: 103 +19:29:21.023 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 205.75, Read: 206 +19:29:21.194 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 307.75, Read: 308 +19:29:21.364 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 409.75, Read: 410 +19:29:21.534 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 512.75, Read: 513 +19:29:21.704 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 614.75, Read: 615 +19:29:21.874 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 716.75, Read: 717 +19:29:22.045 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 818.75, Read: 819 +19:29:22.199 SetSwitchValue Offset: 75% INFO Set/Read differ by 20-30% of SwitchStep. Set: 921.75, Read: 922 +19:29:22.354 SetSwitchValue OK Switch has been reset to its original state + + +Conformance test complete + +No errors, warnings or issues found: your driver passes ASCOM validation!! + +Driver Hash Value: 2324CFC7AD89514CA3132538F5949BD837ABDF01A2CB27A7E8B4F11F6E34A60F1D0A054FDD9B71AAAEBDE250B552785A4FD3EE2521E13C2AFFDD32F423269B1E diff --git a/LynxAstro.DewController.Setup/AscomLocalServer.wxs b/LynxAstro.DewController.Setup/AscomLocalServer.wxs new file mode 100644 index 0000000..a6f9ca1 --- /dev/null +++ b/LynxAstro.DewController.Setup/AscomLocalServer.wxs @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Setup/AscomSwitchDriver.wxs b/LynxAstro.DewController.Setup/AscomSwitchDriver.wxs new file mode 100644 index 0000000..ef902a6 --- /dev/null +++ b/LynxAstro.DewController.Setup/AscomSwitchDriver.wxs @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Setup/Config.wxi b/LynxAstro.DewController.Setup/Config.wxi new file mode 100644 index 0000000..506f9c2 --- /dev/null +++ b/LynxAstro.DewController.Setup/Config.wxi @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Setup/Directories.wxs b/LynxAstro.DewController.Setup/Directories.wxs new file mode 100644 index 0000000..435128a --- /dev/null +++ b/LynxAstro.DewController.Setup/Directories.wxs @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Setup/FeatureTree.wxs b/LynxAstro.DewController.Setup/FeatureTree.wxs new file mode 100644 index 0000000..74a0983 --- /dev/null +++ b/LynxAstro.DewController.Setup/FeatureTree.wxs @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Setup/InstallationUI.wxs b/LynxAstro.DewController.Setup/InstallationUI.wxs new file mode 100644 index 0000000..a52ab86 --- /dev/null +++ b/LynxAstro.DewController.Setup/InstallationUI.wxs @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + "1"]]> + + 1 + + NOT Installed + Installed AND PATCH + + 1 + LicenseAccepted = "1" + + 1 + 1 + NOT WIXUI_DONTVALIDATEPATH + "1"]]> + WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" + 1 + 1 + + NOT Installed + Installed AND NOT PATCH + Installed AND PATCH + + 1 + + 1 + 1 + 1 + + + + + + + diff --git a/LynxAstro.DewController.Setup/License.rtf b/LynxAstro.DewController.Setup/License.rtf new file mode 100644 index 0000000000000000000000000000000000000000..db6e68d206c11358588c456f573a0f4636bae962 GIT binary patch literal 1457 zcma)6&2Hm15bilZ-eHhayAWd8-R+{AlVZ~`v9e@H@}>rOp(V;WS_3n3=n!}5g zyUxQT9*3op8fN_Hj1CYNpxc|3uzTs8G#aFXPFS&hft}^rizb2=6@zIZ7~F{nyaBFX z;2V}-ag1tfhYl{Zn zV`uj7onVXF-fE=8reIBDuw?_|pQ8355|p<_DRT_gYNHz&{JJmyo?V4Wd~3do!PRBh z+ISQ_G=3PxAm=jQ>z3;{y4Z?q>reqUI)q-0q0fHnJRSjwQ#jxe4$S{GeE&|#f2*@9+f|MoX1<>Ux zr37O%o2Kg-&7R;9>$1FplrCw7pj8eP5?-Mw!MZstNikg@a`H%1T5Tej(<%#?a|BIb zl@t}7uG6G|)w)>aB|-Wb!e%s^7sx`EB&+U`6=OiY;R4DTmbOmhrgVU2Hylc&L@2Rq)^R?ePFG^`{(>=v00G#E9R=4pPKW{JH@Q&d#`qSSkDBt`5 literal 0 HcmV?d00001 diff --git a/LynxAstro.DewController.Setup/LynxAstro.DewController.Setup.wixproj b/LynxAstro.DewController.Setup/LynxAstro.DewController.Setup.wixproj new file mode 100644 index 0000000..0cd3350 --- /dev/null +++ b/LynxAstro.DewController.Setup/LynxAstro.DewController.Setup.wixproj @@ -0,0 +1,74 @@ + + + + Debug + x86 + 3.10 + {1073a462-d9a4-4c72-9c3f-a345b307153a} + 2.0 + LynxAstro.DewController.Setup + Package + + + bin\$(Configuration)\ + obj\$(Configuration)\ + Debug + + + bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + + + + + + + + LynxAstro.DewController.Switch + {64308775-bd4a-469c-bcab-3ed830b811af} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + LynxAstro.DewController + {c708e487-e3a9-4073-a545-294b88674225} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + $(WixExtDir)\WixUIExtension.dll + WixUIExtension + + + $(WixExtDir)\WixNetFxExtension.dll + WixNetFxExtension + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Setup/Product.wxs b/LynxAstro.DewController.Setup/Product.wxs new file mode 100644 index 0000000..e2e47d8 --- /dev/null +++ b/LynxAstro.DewController.Setup/Product.wxs @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + = "6.4.1"]]> + + + + + + + + + + + + + + + + + diff --git a/LynxAstro.DewController.Switch.UnitTests/LynxAstro.DewController.Switch.UnitTests.csproj b/LynxAstro.DewController.Switch.UnitTests/LynxAstro.DewController.Switch.UnitTests.csproj new file mode 100644 index 0000000..399bd6d --- /dev/null +++ b/LynxAstro.DewController.Switch.UnitTests/LynxAstro.DewController.Switch.UnitTests.csproj @@ -0,0 +1,120 @@ + + + + + + Debug + AnyCPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F} + Library + Properties + LynxAstro.DewController.Switch.UnitTests + LynxAstro.DewController.Switch.UnitTests + v4.7.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AnyCPU + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Astrometry.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Attributes.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Cache.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Controls.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.DeviceInterfaces.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.DriverAccess.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Exceptions.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Internal.Extensions.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.SettingsProvider.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Utilities.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Utilities.Video.dll + + + ..\packages\Castle.Core.4.4.0\lib\net45\Castle.Core.dll + + + ..\packages\Moq.4.16.0\lib\net45\Moq.dll + + + ..\packages\NUnit.3.13.1\lib\net45\nunit.framework.dll + + + + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + + + + + {64308775-bd4a-469c-bcab-3ed830b811af} + LynxAstro.DewController.Switch + + + {c708e487-e3a9-4073-a545-294b88674225} + LynxAstro.DewController + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Switch.UnitTests/Properties/AssemblyInfo.cs b/LynxAstro.DewController.Switch.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1573b92 --- /dev/null +++ b/LynxAstro.DewController.Switch.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LynxAstro.DewController.Switch.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LynxAstro.DewController.Switch.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("afa5a4a4-188c-4180-b910-7c5ba7d5c11f")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs b/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs new file mode 100644 index 0000000..d3aa19a --- /dev/null +++ b/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs @@ -0,0 +1,27 @@ +using ASCOM.LynxAstro.DewController; +using Moq; +using NUnit.Framework; + +namespace LynxAstro.DewController.Switch.UnitTests +{ + [TestFixture] + public class SwitchUnitTests + { + private ASCOM.LynxAstro.DewController.Switch _switch; + private Mock _sharedResourcesWrapperMock; + + [SetUp] + public void Setup() + { + _sharedResourcesWrapperMock = new Mock(); + + _switch = new ASCOM.LynxAstro.DewController.Switch(); + } + + [Test] + public void CheckThatClassCreatedProperly() + { + Assert.That(_switch, Is.Not.Null); + } + } +} diff --git a/LynxAstro.DewController.Switch.UnitTests/packages.config b/LynxAstro.DewController.Switch.UnitTests/packages.config new file mode 100644 index 0000000..f485460 --- /dev/null +++ b/LynxAstro.DewController.Switch.UnitTests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Switch/ASCOM.ico b/LynxAstro.DewController.Switch/ASCOM.ico new file mode 100644 index 0000000000000000000000000000000000000000..9bf8f4110f20bfdde720ab5e04def9b01b414268 GIT binary patch literal 125783 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 literal 0 HcmV?d00001 diff --git a/LynxAstro.DewController.Switch/ASCOMDriverTemplate.snk b/LynxAstro.DewController.Switch/ASCOMDriverTemplate.snk new file mode 100644 index 0000000000000000000000000000000000000000..5ab294522d027053efbc99370de86ab44f7d999f GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096o@;lSCg(u0Cs*BuyKR9r^P7mA>i6=N| z*}>a~-mZ)Ls%FAV}Sq^{wX`Ye#nGu3i15cdI?<@%KFXx_Qu1Q zN>O%rp3WAnZ)@;RL`$uw-03doE|FK(K%8USc|TsTgk^<-c1|7cI%~?31LIyTy58J< z(taLe4Y_8Nh}Or>g|R?a+P~+Jr3m-}1VHV}8MG@t3cStINbjmS^i<3>k4h+hz~GNt z!8WZr{qM3aNd*4ISiND;`zskEKm-${7citI0lHIW8_tw=H7N}rUVoSh@Z0OAZLl6E zg4N4tVSk`gTDSF;w7bcn&{MOEFqqy=Ox~K)dzZP}jp0K6s4Gh^sqMDcqM}~JxMG*P z5rI=Fs#&gf4uxH-ATi-cxXakgqSr>inHh9jdLju(8XosCtg!90t5C7nYMaYhglwJV z1mep!noQ3{J9amy(`{qn2B&nJ#uy{GMKAhs)#Zvf}wC>k)weHP-6 z7D;L&Jk#(ossUu=V0_*yfJFz4$gnsHpzT~Y)$N?L)gSilWU%u5(kvc4y5ee=$CX^9 zq+$TozA{2;O}JYE11w~)ND9yKepmrn_1VTcdv35sO)b%cS3!i<;&LB(eJ&j~^ktgh i2?jBBQqyjQmCuueJbfE~HZ)z<*Si*L2C5Qx(CwX1izKQ5 literal 0 HcmV?d00001 diff --git a/LynxAstro.DewController.Switch/LynxAstro.DewController.Switch.csproj b/LynxAstro.DewController.Switch/LynxAstro.DewController.Switch.csproj new file mode 100644 index 0000000..b888fe3 --- /dev/null +++ b/LynxAstro.DewController.Switch/LynxAstro.DewController.Switch.csproj @@ -0,0 +1,151 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {64308775-BD4A-469C-BCAB-3ED830B811AF} + Library + Properties + ASCOM.LynxAstro.DewController + ASCOM.LynxAstro.DewController.Switch + + + + + 3.5 + v4.7.2 + ASCOM.ico + true + ASCOMDriverTemplate.snk + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + AnyCPU + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AnyCPU + false + false + + + + + + + + + + + + + + + 3.5 + + + + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + Designer + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + {c708e487-e3a9-4073-a545-294b88674225} + LynxAstro.DewController + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Switch/Properties/AssemblyInfo.cs b/LynxAstro.DewController.Switch/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0d50e61 --- /dev/null +++ b/LynxAstro.DewController.Switch/Properties/AssemblyInfo.cs @@ -0,0 +1,38 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +// +// TODO - Add your authorship information here +[assembly: AssemblyTitle("ASCOM.LynxAstro.DewController.Switch")] +[assembly: AssemblyDescription("ASCOM Switch driver for LynxAstro.DewController")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("The ASCOM Initiative")] +[assembly: AssemblyProduct("ASCOM Switch driver for LynxAstro.DewController")] +[assembly: AssemblyCopyright("Copyright © 2021 The ASCOM Initiative")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(true)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a567b01c-a066-45ce-af3d-0192bad973ea")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +// +// TODO - Set your driver's version here +[assembly: AssemblyVersion("6.5.1.0")] +[assembly: AssemblyFileVersion("6.5.1.0")] diff --git a/LynxAstro.DewController.Switch/Properties/Resources.Designer.cs b/LynxAstro.DewController.Switch/Properties/Resources.Designer.cs new file mode 100644 index 0000000..4fb3684 --- /dev/null +++ b/LynxAstro.DewController.Switch/Properties/Resources.Designer.cs @@ -0,0 +1,96 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ASCOM.LynxAstro.DewController.Properties +{ + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if (object.ReferenceEquals(resourceMan, null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ASCOM.LynxAstro.DewController.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ASCOM + { + get + { + object obj = ResourceManager.GetObject("ASCOM", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon DefaultIcon + { + get + { + object obj = ResourceManager.GetObject("DefaultIcon", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + } +} diff --git a/LynxAstro.DewController.Switch/Properties/Resources.resx b/LynxAstro.DewController.Switch/Properties/Resources.resx new file mode 100644 index 0000000..e522d9e --- /dev/null +++ b/LynxAstro.DewController.Switch/Properties/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\ASCOM.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\ASCOM.ico;System.Drawing.Icon, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/LynxAstro.DewController.Switch/Properties/Settings.Designer.cs b/LynxAstro.DewController.Switch/Properties/Settings.Designer.cs new file mode 100644 index 0000000..54a2225 --- /dev/null +++ b/LynxAstro.DewController.Switch/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ASCOM.LynxAstro.DewController.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/LynxAstro.DewController.Switch/Properties/Settings.settings b/LynxAstro.DewController.Switch/Properties/Settings.settings new file mode 100644 index 0000000..8e615f2 --- /dev/null +++ b/LynxAstro.DewController.Switch/Properties/Settings.settings @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.Switch/ReadMe.htm b/LynxAstro.DewController.Switch/ReadMe.htm new file mode 100644 index 0000000..cbb8f37 --- /dev/null +++ b/LynxAstro.DewController.Switch/ReadMe.htm @@ -0,0 +1,147 @@ + + + + + Untitled Document + + + + + + + + + + + +
+

ASCOM Switch Driver (C#)

+
+



+

+

You have just created the skeleton of an ASCOM +Switch driver in C#. It produces an in-process +(assembly) based driver.

+
+

Prior to developing your first driver, please +familiarize yourself with the developer +information we've provided at the ASCOM Initiative web site +(internet required). +

+

You must do the following in order to complete +your implementation:

+
    +
  1. Switch to the Debug configuration + and build the template now. It should build without errors. +

    +
  2. Add a test project to the + solution. There are templates that can be used to add either a + console or a Windows Forms application:

    +
+
    +
  • Select the ASCOM + Test Forms App (CS) or ASCOM + Test Console App (CS) template.

    +
  • Set a name for the test + application and click on OK.

    +
  • In the Wizard: set the same device + type and model name as for the driver and select Create to build the + test project.

    +
  • Set the Test Application to Run at + Startup.

    +
  • Click on Debug and the test + application should run. You should be able to select your + application in the chooser. Selecting Properties should show the + default setup dialog for your driver.

    +
  • Trying to continue will generate + errors because the additional properties have not been implemented.

    +
+
    +
  1. Go through the Driver.cs file and + replace the System.NotImplemented exceptions with code to implement + your driver's functionality. See the ASCOM ISwitchV2 + spec. If a property or method is not implemented in your driver the + System.NotImplemented exception must be replaced by an + ASCOM.PropertyNotImplemented or ASCOM.MethodNotImplemented + exception.

    +
  2. Customize the Setup Dialog (SetupDialogForm) to provide the + settings and other controls for your driver. You can bind settings + directly to controls on your dialog form, there's no need to manage + settings manually. A custom Settings class takes care of managing + your settings behind the scenes. +

    +
+

Notes:

+
    +
  • Successfully building the driver, + as well as using regasm + on the assembly, registers it for both COM and ASCOM (the Chooser). + See the code in the ASCOM Registration region of Driver.vb. +

    +
  • Doing a Clean for the project, as + well doing a regasm + -u on the assembly, unregisters it for both COM and ASCOM + (the Chooser). +

    +
  • Place a breakpoint in your driver class constructor, then + start debugging (go, F5). Your breakpoint will be hit when the test + application creates an instance of your driver (after selecting it + in the Chooser). You can now single step, examine variables, etc. + Please review the test application and make changes and additions to + activate various parts of your driver during debugging.

    +
  • The project's Debug configuration is already configured (The + test application creates an instance of your driver (after selecting + it in the Chooser). You can now single step, examine variables, etc. + Please review the test application and feel free to make changes and + additions to activate various parts of your driver during debugging. +

    +
+
+ + + + + + + +
+ + + + + +
+

ASCOM Initiative

+
+
+



+

+
+

The ASCOM Initiative consists of a group of astronomy software + developers and instrument vendors whose goals are to promote the + driver/client model and scripting automation. +

+

See the ASCOM + web site for more information. Please participate in the + ASCOM-Talk + Groups.IO forum. +

+
+
+



+

+

+

+ + \ No newline at end of file diff --git a/LynxAstro.DewController.Switch/Resources/ASCOM.bmp b/LynxAstro.DewController.Switch/Resources/ASCOM.bmp new file mode 100644 index 0000000000000000000000000000000000000000..55516c7aaa3591604865d4cd18379f8f650c3104 GIT binary patch literal 3382 zcmeIwA#&YN425B3CLjUbgJ57$uqM1+gO+lZkut%6E<(#F6wORPc)-i=r?4!c_Vwp; zZ3Fx2>gm_x54#?Zw`LE_etx;JyM1xL@%<_@PLnpcGPUluKQDjn-|L&12NJySLSxLr z;9wF7q0kt!G&lm>q)=$g*LR%^B!og^%$Qm^0tune7=F^=2qc6;W6U^h9D#&TXpDvN zo8~x1LMSw5m*!3e5<;OdyXkPr%u*^;D_frLmI06Zw(3st}a59h(3XRzqWFR3F8nf*xCj$wg(3oxIax#z*3XRz|Zzlr@q3r$RV$Z*g8j2NO iuBTlt#|(_)(Q$tsX9dm*oE11La8}@~z*&L+tOB3FDm>c& literal 0 HcmV?d00001 diff --git a/LynxAstro.DewController.Switch/Switch.cs b/LynxAstro.DewController.Switch/Switch.cs new file mode 100644 index 0000000..624101f --- /dev/null +++ b/LynxAstro.DewController.Switch/Switch.cs @@ -0,0 +1,695 @@ +#define Switch + +using System; +using System.Runtime.InteropServices; +using ASCOM.Utilities; +using ASCOM.DeviceInterface; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; + +namespace ASCOM.LynxAstro.DewController +{ + // + // Your driver's DeviceID is ASCOM.LynxAstro.DewController.Switch + // + // The Guid attribute sets the CLSID for ASCOM.LynxAstro.DewController.Switch + // The ClassInterface/None attribute prevents an empty interface called + // _LynxAstro.DewController from being created and used as the [default] interface + // + // TODO Replace the not implemented exceptions with code to implement the function or + // throw the appropriate ASCOM exception. + // + + /// + /// ASCOM Switch Driver for LynxAstro.DewController. + /// + [Guid("3a29744a-f33f-4843-a7e0-6938d9bd50ba")] + [ProgId("ASCOM.LynxAstro.DewController.Switch")] + [ServedClassName("LynxAstro.DewController")] + [ClassInterface(ClassInterfaceType.None)] + public class Switch : ISwitchV2 + { + /// + /// ASCOM DeviceID (COM ProgID) for this driver. + /// The DeviceID is used by ASCOM applications to load the driver at runtime. + /// + internal static string DriverId = Marshal.GenerateProgIdForType(MethodBase.GetCurrentMethod().DeclaringType ?? throw new System.InvalidOperationException()); + + /// + /// Driver description that displays in the ASCOM Chooser. + /// + private static string DriverDescription = "ASCOM Switch Driver for LynxAstro.DewController"; + + protected static string ComPort; // Variables to hold the currrent device configuration + protected static TraceLogger Tl; + + protected readonly ISharedResourcesWrapper SharedResourcesWrapper; + + /// + /// Initializes a new instance of the class. + /// Must be public for COM registration. + /// + /// + public Switch() + { + SharedResourcesWrapper = new SharedResourcesWrapper(); + + Initialise(nameof(Switch)); + } + + public Switch(ISharedResourcesWrapper sharedResourcesWrapper) + { + SharedResourcesWrapper = sharedResourcesWrapper; + + Initialise(nameof(Switch)); + } + + + protected void Initialise(string className) + { + Tl = new TraceLogger("", $"LynxAstro.DewController.{className}"); + + ReadProfile(); // Read device configuration from the ASCOM Profile store + + IsConnected = false; // Initialise connected to false + + LogMessage(className, "Completed initialisation"); + LogMessage(className, $"Driver version: {DriverVersion}"); + } + + // + // PUBLIC COM INTERFACE ISwitchV2 IMPLEMENTATION + // + + #region Common properties and methods. + + /// + /// Displays the Setup Dialog form. + /// If the user clicks the OK button to dismiss the form, then + /// the new settings are saved, otherwise the old values are reloaded. + /// THIS IS THE ONLY PLACE WHERE SHOWING USER INTERFACE IS ALLOWED! + /// + public void SetupDialog() + { + LogMessage("SetupDialog", "Opening setup dialog"); + SharedResourcesWrapper.SetupDialog(); + ReadProfile(); + LogMessage("SetupDialog", "complete"); + } + + public ArrayList SupportedActions + { + get + { + Tl.LogMessage("SupportedActions Get", "Returning empty arraylist"); + return new ArrayList(); + } + } + + public string Action(string actionName, string actionParameters) + { + LogMessage("", "Action {0}, parameters {1} not implemented", actionName, actionParameters); + throw new ASCOM.ActionNotImplementedException("Action " + actionName + " is not implemented by this driver"); + } + + public void CommandBlind(string command, bool raw) + { + CheckConnected("CommandBlind"); + SharedResourcesWrapper.SendBlind(command); + } + + public bool CommandBool(string command, bool raw) + { + CheckConnected("CommandBool"); + throw new ASCOM.MethodNotImplementedException("CommandBool"); + } + + public string CommandString(string command, bool raw) + { + CheckConnected("CommandString"); + return SharedResourcesWrapper.SendString(command); + } + + public void Dispose() + { + // Clean up the trace logger and util objects + Tl.Enabled = false; + Tl.Dispose(); + Tl = null; + } + + private string DecodeResult(string pattern, string encodedString) + { + var decodedString = encodedString.Substring(pattern.Length).TrimEnd('#'); + return decodedString; + } + + public bool Connected + { + get + { + LogMessage("Connected", "Get {0}", IsConnected); + return IsConnected; + } + set + { + LogMessage("Connected", "Set {0}", value); + if (value == IsConnected) + return; + + if (value) + { + try + { + ReadProfile(); + + LogMessage("Connected Set", "Connecting to port {0}", ComPort); + var connectionInfo = SharedResourcesWrapper.Connect("Serial", DriverId, Tl); + try + { + LogMessage("Connected Set", $"Connected to port {ComPort}. Version:{SharedResourcesWrapper.FirmwareVersion}"); + + IsConnected = true; + + LogMessage("Connected Set", $"Connected OK."); + + var maxSwitch = MaxSwitch; + } + catch (Exception) + { + SharedResourcesWrapper.Disconnect("Serial", DriverId); + throw; + } + } + catch (Exception ex) + { + LogMessage("Connected Set", "Error connecting to port {0} - {1}", ComPort, ex.Message); + } + } + else + { + LogMessage("Connected Set", "Disconnecting from port {0}", ComPort); + SharedResourcesWrapper.Disconnect("Serial", DriverId); + IsConnected = false; + } + } + } + + public string Description + { + get + { + Tl.LogMessage("Description Get", DriverDescription); + return DriverDescription; + } + } + + public string DriverInfo + { + get + { + string driverInfo = $"{Description} .net driver. Version: {DriverVersion}"; + LogMessage("DriverInfo Get", driverInfo); + return driverInfo; + } + } + + public string DriverVersion + { + get + { + Version version = Assembly.GetExecutingAssembly().GetName().Version; + string driverVersion = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + LogMessage("DriverVersion Get", driverVersion); + return driverVersion; + } + } + + public short InterfaceVersion + { + // set by the driver wizard + get + { + LogMessage("InterfaceVersion Get", "2"); + return Convert.ToInt16("2"); + } + } + + public string Name + { + get + { + string name = "LynxAstro.DewController"; + Tl.LogMessage("Name Get", name); + return name; + } + } + + #endregion + + #region ISwitchV2 Implementation + + private short numSwitch = 0; + private List switchNames = new List(); + + /// + /// The number of switches managed by this driver + /// + public short MaxSwitch + { + get + { + CheckConnected("MaxSwitch Get"); + + var result = SharedResourcesWrapper.SendString(":GD#"); + var decoded = DecodeResult(":GD", result); + + this.numSwitch = short.Parse(decoded); + + //Command: :GD# + //Purpose: Get the device type, i.e.the number of channels the dew controller has. + //Response: :GDX# where X is either 1 or 4 depending on the number of channels this device has + + Tl.LogMessage("MaxSwitch Get", numSwitch.ToString()); + return this.numSwitch; + } + } + + /// + /// Return the name of switch n + /// + /// The switch number to return + /// + /// The name of the switch + /// + public string GetSwitchName(short id) + { + Validate("GetSwitchName", id); + ReadProfile(); + + return switchNames.Count > id ? switchNames[id] : string.Empty; + } + + /// + /// Sets a switch name to a specified value + /// + /// The number of the switch whose name is to be set + /// The name of the switch + public void SetSwitchName(short id, string name) + { + Validate("SetSwitchName", id); + ReadProfile(); + + while (id > switchNames.Count) + switchNames.Add(string.Empty); + + switchNames[id] = name; + + WriteSwitchNames(); + } + + /// + /// Gets the switch description. + /// + /// The id. + /// + public string GetSwitchDescription(short id) + { + Validate("GetSwitchDescription", id); + return "Control Knob"; + } + + /// + /// Reports if the specified switch can be written to. + /// This is false if the switch cannot be written to, for example a limit switch or a sensor. + /// The default is true. + /// + /// The number of the switch whose write state is to be returned + /// true if the switch can be written to, otherwise false. + /// + /// If the method is not implemented + /// If id is outside the range 0 to MaxSwitch - 1 + public bool CanWrite(short id) + { + Validate("CanWrite", id); + // default behavour is to report true + Tl.LogMessage("CanWrite", string.Format("CanWrite({0}) - default true", id)); + return true; + // implementation should report the correct behaviour + //tl.LogMessage("CanWrite", string.Format("CanWrite({0}) - not implemented", id)); + //throw new MethodNotImplementedException("CanWrite"); + } + + #region boolean switch members + + /// + /// Return the state of switch n + /// a multi-value switch must throw a not implemented exception + /// + /// The switch number to return + /// + /// True or false + /// + public bool GetSwitch(short id) + { + var value = GetSwitchValue(id); + return value > 0; + } + + /// + /// Sets a switch to the specified state + /// If the switch cannot be set then throws a MethodNotImplementedException. + /// A multi-value switch must throw a not implemented exception + /// setting it to false will set it to its minimum value. + /// + /// + /// + public void SetSwitch(short id, bool state) + { + Validate("SetSwitch", id); + if (!CanWrite(id)) + { + var str = string.Format("SetSwitch({0}) - Cannot Write", id); + Tl.LogMessage("SetSwitch", str); + throw new MethodNotImplementedException(str); + } + SetSwitchValue(id, state ? 1023 : 0); + } + + #endregion + + #region analogue members + + /// + /// returns the maximum value for this switch + /// boolean switches must return 1.0 + /// + /// + /// + public double MaxSwitchValue(short id) + { + Validate("MaxSwitchValue", id); + // boolean switch implementation: + return 1023; + // or + //tl.LogMessage("MaxSwitchValue", string.Format("MaxSwitchValue({0}) - not implemented", id)); + //throw new MethodNotImplementedException("MaxSwitchValue"); + } + + /// + /// returns the minimum value for this switch + /// boolean switches must return 0.0 + /// + /// + /// + public double MinSwitchValue(short id) + { + Validate("MinSwitchValue", id); + // boolean switch implementation: + return 0; + // or + //tl.LogMessage("MinSwitchValue", string.Format("MinSwitchValue({0}) - not implemented", id)); + //throw new MethodNotImplementedException("MinSwitchValue"); + } + + /// + /// returns the step size that this switch supports. This gives the difference between + /// successive values of the switch. + /// The number of values is ((MaxSwitchValue - MinSwitchValue) / SwitchStep) + 1 + /// boolean switches must return 1.0, giving two states. + /// + /// + /// + public double SwitchStep(short id) + { + Validate("SwitchStep", id); + // boolean switch implementation: + return 1; + // or + //tl.LogMessage("SwitchStep", string.Format("SwitchStep({0}) - not implemented", id)); + //throw new MethodNotImplementedException("SwitchStep"); + } + + /// + /// returns the analogue switch value for switch id + /// boolean switches must throw a not implemented exception + /// + /// + /// + public double GetSwitchValue(short id) + { + Validate("GetSwitchValue", id); + Tl.LogMessage("GetSwitchValue", string.Format("GetSwitchValue({0}) - not implemented", id)); + + var channel = ChannelToLetter(id); + + var encoded = SharedResourcesWrapper.SendString($":GC{channel}#"); + var decoded = DecodeResult(":GC", encoded); + + return double.Parse(decoded.TrimStart(channel)); + //Command: :GCX# where X is the channel A, B, C or D to retrieve. + //Purpose: Get the current power setting for a specific channel. + //Response: :GCXVVVV# where X is the channel A, B, C or D returned and VVVV is the power level between 0 - 1023. + //Possible Errors + //:ER5# = Channel out of range, e.g. channel B on a 1 channel device. + } + + private char ChannelToLetter(short id) + { + UInt16 ord = (UInt16) 'A'; + ord += (ushort)id; + return (char)ord; + } + + /// + /// set the analogue value for this switch. + /// If the switch cannot be set then throws a MethodNotImplementedException. + /// If the value is not between the maximum and minimum then throws an InvalidValueException + /// boolean switches must throw a not implemented exception. + /// + /// + /// + public void SetSwitchValue(short id, double value) + { + Validate("SetSwitchValue", id, value); + if (!CanWrite(id)) + { + Tl.LogMessage("SetSwitchValue", string.Format("SetSwitchValue({0}) - Cannot write", id)); + throw new ASCOM.MethodNotImplementedException(string.Format("SetSwitchValue({0}) - Cannot write", id)); + } + //Tl.LogMessage("SetSwitchValue", string.Format("SetSwitchValue({0}) = {1} - not implemented", id, value)); + //throw new MethodNotImplementedException("SetSwitchValue"); + + + //Command: :SCXVVVV# where X is the channel A, B, C or D to set and VVVV is the power level + //between 0 - 1023.The power level must be 4 digits long so pad with leading zeros if necessary. + //Purpose: Set the current power setting for a specific channel. + //Response: :SC1# indicates success. Run a GA or GC command to verify. + //Possible Errors + //:ER4# = Not enough data received - make sure you zero pad the power level. + //:ER5# = Channel or power level out of range, e.g. channel B on a 1 channel device or power above 1023. + + var channel = ChannelToLetter(id); + + var encoded = SharedResourcesWrapper.SendString($":SC{channel}{value:0000}#"); + var decoded = DecodeResult(":SC", encoded); + + if (decoded != "1") + { + throw new InvalidValueException($"Unable to set switch {{id}} to value {value}"); + } + //return double.Parse(decoded.TrimStart(channel)); + } + + #endregion + #endregion + + #region private methods + + /// + /// Checks that the switch id is in range and throws an InvalidValueException if it isn't + /// + /// The message. + /// The id. + private void Validate(string message, short id) + { + if (id < 0 || id >= numSwitch) + { + Tl.LogMessage(message, string.Format("Switch {0} not available, range is 0 to {1}", id, numSwitch - 1)); + throw new InvalidValueException(message, id.ToString(), string.Format("0 to {0}", numSwitch - 1)); + } + } + + /// + /// Checks that the switch id and value are in range and throws an + /// InvalidValueException if they are not. + /// + /// The message. + /// The id. + /// The value. + private void Validate(string message, short id, double value) + { + Validate(message, id); + var min = MinSwitchValue(id); + var max = MaxSwitchValue(id); + if (value < min || value > max) + { + Tl.LogMessage(message, string.Format("Value {1} for Switch {0} is out of the allowed range {2} to {3}", id, value, min, max)); + throw new InvalidValueException(message, value.ToString(), string.Format("Switch({0}) range {1} to {2}", id, min, max)); + } + } + + /// + /// Checks that the number of states for the switch is correct and throws a methodNotImplemented exception if not. + /// Boolean switches must have 2 states and multi-value switches more than 2. + /// + /// + /// + /// + //private void Validate(string message, short id, bool expectBoolean) + //{ + // Validate(message, id); + // var ns = (int)(((MaxSwitchValue(id) - MinSwitchValue(id)) / SwitchStep(id)) + 1); + // if ((expectBoolean && ns != 2) || (!expectBoolean && ns <= 2)) + // { + // tl.LogMessage(message, string.Format("Switch {0} has the wriong number of states", id, ns)); + // throw new MethodNotImplementedException(string.Format("{0}({1})", message, id)); + // } + //} + + #endregion + + #region Private properties and methods + // here are some useful properties and methods that can be used as required + // to help with driver development + + #region ASCOM Registration + + // Register or unregister driver for ASCOM. This is harmless if already + // registered or unregistered. + // + /// + /// Register or unregister the driver with the ASCOM Platform. + /// This is harmless if the driver is already registered/unregistered. + /// + /// If true, registers the driver, otherwise unregisters it. + private static void RegUnregASCOM(bool bRegister) + { + using (var P = new ASCOM.Utilities.Profile()) + { + P.DeviceType = "Switch"; + if (bRegister) + { + P.Register(DriverId, DriverDescription); + } + else + { + P.Unregister(DriverId); + } + } + } + + /// + /// This function registers the driver with the ASCOM Chooser and + /// is called automatically whenever this class is registered for COM Interop. + /// + /// Type of the class being registered, not used. + /// + /// This method typically runs in two distinct situations: + /// + /// + /// In Visual Studio, when the project is successfully built. + /// For this to work correctly, the option Register for COM Interop + /// must be enabled in the project settings. + /// + /// During setup, when the installer registers the assembly for COM Interop. + /// + /// This technique should mean that it is never necessary to manually register a driver with ASCOM. + /// + [ComRegisterFunction] + public static void RegisterASCOM(Type t) + { + RegUnregASCOM(true); + } + + /// + /// This function unregisters the driver from the ASCOM Chooser and + /// is called automatically whenever this class is unregistered from COM Interop. + /// + /// Type of the class being registered, not used. + /// + /// This method typically runs in two distinct situations: + /// + /// + /// In Visual Studio, when the project is cleaned or prior to rebuilding. + /// For this to work correctly, the option Register for COM Interop + /// must be enabled in the project settings. + /// + /// During uninstall, when the installer unregisters the assembly from COM Interop. + /// + /// This technique should mean that it is never necessary to manually unregister a driver from ASCOM. + /// + [ComUnregisterFunction] + public static void UnregisterASCOM(Type t) + { + RegUnregASCOM(false); + } + + #endregion + + protected bool IsConnected { get; set; } + + /// + /// Use this function to throw an exception if we aren't connected to the hardware + /// + /// + private void CheckConnected(string message) + { + if (!IsConnected) + { + throw new ASCOM.NotConnectedException(message); + } + } + + /// + /// Read the device configuration from the ASCOM Profile store + /// + protected void ReadProfile() + { + var profileProperties = SharedResourcesWrapper.ReadProfile(); + Tl.Enabled = profileProperties.TraceLogger; + ComPort = profileProperties.ComPort; + + switchNames.Clear(); + switchNames.AddRange(profileProperties.SwitchNames); + + LogMessage("ReadProfile", $"Trace logger enabled: {Tl.Enabled}"); + LogMessage("ReadProfile", $"Com Port: {ComPort}"); + + } + + protected void WriteSwitchNames() + { + var profileProperties = SharedResourcesWrapper.ReadProfile(); + + profileProperties.SwitchNames.Clear(); + profileProperties.SwitchNames.AddRange(switchNames); + + SharedResourcesWrapper.WriteProfile(profileProperties); + } + + /// + /// Log helper function that takes formatted strings and arguments + /// + /// + /// + /// + internal void LogMessage(string identifier, string message, params object[] args) + { + var msg = string.Format(message, args); + Tl.LogMessage(identifier, msg); + } + #endregion + } +} diff --git a/LynxAstro.DewController.Switch/app.config b/LynxAstro.DewController.Switch/app.config new file mode 100644 index 0000000..c8fc52c --- /dev/null +++ b/LynxAstro.DewController.Switch/app.config @@ -0,0 +1,8 @@ + + + + +
+ + + diff --git a/LynxAstro.DewController.TestConsole/LynxAstro.DewController.TestConsole.csproj b/LynxAstro.DewController.TestConsole/LynxAstro.DewController.TestConsole.csproj new file mode 100644 index 0000000..905009c --- /dev/null +++ b/LynxAstro.DewController.TestConsole/LynxAstro.DewController.TestConsole.csproj @@ -0,0 +1,64 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {D5207217-61C7-4E94-8097-91DBACE57D2A} + Exe + Properties + ASCOM.LynxAstro.DewController + ASCOM.LynxAstro.DewController.TestConsole + v4.7.2 + + + 512 + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + x86 + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.TestConsole/Program.cs b/LynxAstro.DewController.TestConsole/Program.cs new file mode 100644 index 0000000..e7787a2 --- /dev/null +++ b/LynxAstro.DewController.TestConsole/Program.cs @@ -0,0 +1,78 @@ +// This implements a console application that can be used to test an ASCOM driver +// + +// This is used to define code in the template that is specific to one class implementation +// unused code can be deleted and this definition removed. + +#define Switch +// remove this to bypass the code that uses the chooser to select the driver +//#define UseChooser + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ASCOM +{ + class Program + { + static void Main(string[] args) + { + // Uncomment the code that's required +#if UseChooser + // choose the device + string id = ASCOM.DriverAccess.Switch.Choose(""); + if (string.IsNullOrEmpty(id)) + return; + // create this device + ASCOM.DriverAccess.Switch device = new ASCOM.DriverAccess.Switch(id); +#else + // this can be replaced by this code, it avoids the chooser and creates the driver class directly. + ASCOM.DriverAccess.Switch device = new ASCOM.DriverAccess.Switch("ASCOM.LynxAstro.DewController.Switch"); +#endif + // now run some tests, adding code to your driver so that the tests will pass. + // these first tests are common to all drivers. + Console.WriteLine("name " + device.Name); + Console.WriteLine("description " + device.Description); + Console.WriteLine("DriverInfo " + device.DriverInfo); + Console.WriteLine("driverVersion " + device.DriverVersion); + + // TODO add more code to test the driver. + device.Connected = true; + + Console.WriteLine("Number of switches: " + device.MaxSwitch); + + for (short i = 0; i < device.MaxSwitch; i++) + { + var min = device.MinSwitchValue(i); + var max = device.MaxSwitchValue(i); + var step = device.SwitchStep(i); + + var value = device.GetSwitchValue(i); + + Console.WriteLine($"switch {i}: {min} - {max} stepsize {step}: {value}"); + } + + for (short i = 0; i < device.MaxSwitch; i++) + { + device.SetSwitchValue(i, 500); + } + + for (short i = 0; i < device.MaxSwitch; i++) + { + var min = device.MinSwitchValue(i); + var max = device.MaxSwitchValue(i); + var step = device.SwitchStep(i); + + var value = device.GetSwitchValue(i); + + Console.WriteLine($"switch {i}: {min} - {max} stepsize {step}: {value}"); + } + + device.Connected = false; + Console.WriteLine("Press Enter to finish"); + Console.ReadLine(); + } + } +} diff --git a/LynxAstro.DewController.TestConsole/Properties/AssemblyInfo.cs b/LynxAstro.DewController.TestConsole/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3622a8c --- /dev/null +++ b/LynxAstro.DewController.TestConsole/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LynxAstro.DewController Test Application")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("ASCOM Initiative")] +[assembly: AssemblyProduct("LynxAstro.DewController")] +[assembly: AssemblyCopyright("Copyright © ASCOM Initiative 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c7008f94-e3b9-4481-b720-3b56557860c6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: + +[assembly: AssemblyVersion("6.4.0.0")] +[assembly: AssemblyFileVersion("6.4.0.0")] diff --git a/LynxAstro.DewController.TestConsole/app.config b/LynxAstro.DewController.TestConsole/app.config new file mode 100644 index 0000000..5c22b42 --- /dev/null +++ b/LynxAstro.DewController.TestConsole/app.config @@ -0,0 +1,3 @@ + + + diff --git a/LynxAstro.DewController.UnitTests/LynxAstro.DewController.UnitTests.csproj b/LynxAstro.DewController.UnitTests/LynxAstro.DewController.UnitTests.csproj new file mode 100644 index 0000000..ac7edec --- /dev/null +++ b/LynxAstro.DewController.UnitTests/LynxAstro.DewController.UnitTests.csproj @@ -0,0 +1,115 @@ + + + + + + Debug + AnyCPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD} + Library + Properties + LynxAstro.DewController.UnitTests + LynxAstro.DewController.UnitTests + v4.7.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Astrometry.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Attributes.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Cache.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Controls.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.DeviceInterfaces.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.DriverAccess.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Exceptions.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Internal.Extensions.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.SettingsProvider.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Utilities.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Utilities.Video.dll + + + ..\packages\Castle.Core.4.4.0\lib\net45\Castle.Core.dll + + + ..\packages\Moq.4.16.0\lib\net45\Moq.dll + + + ..\packages\NUnit.3.13.1\lib\net45\nunit.framework.dll + + + + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + + + + + + + + {c708e487-e3a9-4073-a545-294b88674225} + LynxAstro.DewController + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.UnitTests/Properties/AssemblyInfo.cs b/LynxAstro.DewController.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..27ecda4 --- /dev/null +++ b/LynxAstro.DewController.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("LynxAstro.DewController.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("LynxAstro.DewController.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bded8ebb-7f9f-4710-8b74-59298c2dfcad")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs b/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs new file mode 100644 index 0000000..f0ff61c --- /dev/null +++ b/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs @@ -0,0 +1,189 @@ +using System; +using System.Security.Cryptography.X509Certificates; +using ASCOM.LynxAstro.DewController; +using ASCOM.Utilities.Interfaces; +using Moq; +using NUnit.Framework; + +namespace LynxAstro.DewController.UnitTests +{ + [TestFixture] + public class SharedResourcesUnitTests + { + private Mock _serialMock; + private Mock _traceLoggerMock; + + [SetUp] + public void Setup() + { + _serialMock = new Mock(); + _serialMock.SetupAllProperties(); + + _traceLoggerMock = new Mock(); + + SharedResources.SharedSerial = _serialMock.Object; + } + + [Test] + public void CheckThatSerialPortIsSetToUseMock() + { + Assert.That(SharedResources.SharedSerial, Is.EqualTo(_serialMock.Object)); + } + + [Test] + public void SendBlind_WhenCalled_Then_ClearsBuffersAndSendsMessage() + { + var expectedMessage = "Test"; + + SharedResources.SendBlind(expectedMessage); + + _serialMock.Verify(x => x.ClearBuffers(), Times.Once); + _serialMock.Verify(x => x.Transmit(expectedMessage), Times.Once); + } + + [Test] + public void SendChar_WhenCalled_ThenSendsMessageAndReadsExpectedNumberOfCharacters() + { + var expectedMessage = "Test"; + var expectedResult = "A"; + + _serialMock.Setup(x => x.ReceiveCounted(1)).Returns(expectedResult); + + var result = SharedResources.SendChar(expectedMessage); + + _serialMock.Verify(x => x.ClearBuffers(), Times.Once); + _serialMock.Verify(x => x.Transmit(expectedMessage), Times.Once); + _serialMock.Verify(x => x.ReceiveCounted(1), Times.Once); + Assert.That(result, Is.EqualTo(expectedResult)); + } + + [Test] + public void SendString_WhenCalled_ThenSendsMessageAndReadsResultUntilTerminatorFound() + { + var expectedMessage = "Test"; + var expectedResult = "TestMessage#"; + + _serialMock.Setup(x => x.ReceiveTerminated("#")).Returns(expectedResult); + + var result = SharedResources.SendString(expectedMessage); + + _serialMock.Verify(x => x.ClearBuffers(), Times.Once); + _serialMock.Verify(x => x.Transmit(expectedMessage), Times.Once); + _serialMock.Verify(x => x.ReceiveTerminated("#"), Times.Once); + Assert.That(result, Is.EqualTo(expectedResult.TrimEnd('#'))); + } + + [Test] + public void ReadTerminated_WhenCalled_ThenReadsResultUntilTerminatorFound() + { + var expectedResult = "TestMessage#"; + + _serialMock.Setup(x => x.ReceiveTerminated("#")).Returns(expectedResult); + + var result = SharedResources.ReadTerminated(); + + _serialMock.Verify(x => x.ReceiveTerminated("#"), Times.Once); + Assert.That(result, Is.EqualTo(expectedResult)); + } + + [Test] + public void ReadCharacters_WhenCalled_ThenReadsSpecificNumberOfCharacters() + { + var numberOfCharacters = 5; + + SharedResources.ReadCharacters(numberOfCharacters); + + _serialMock.Verify(x => x.ReceiveCounted(numberOfCharacters), Times.Once); + } + + [Test] + public void WriteProfile_WhenCalled_WritesExpectedProfileSettings() + { + string DriverId = "ASCOM.MeadeGeneric.Telescope"; + + Mock profileWrapperMock = new Mock(); + profileWrapperMock.SetupAllProperties(); + + IProfileWrapper profeWrapper = profileWrapperMock.Object; + + Mock profileFactoryMock = new Mock(); + profileFactoryMock.Setup(x => x.Create()).Returns(profileWrapperMock.Object); + + SharedResources.ProfileFactory = profileFactoryMock.Object; + + var profileProperties = new ProfileProperties + { + TraceLogger = false, + ComPort = "TestComPort" + }; + + SharedResources.WriteProfile(profileProperties); + + Assert.That(profeWrapper.DeviceType, Is.EqualTo("Telescope")); + profileWrapperMock.Verify(x => x.WriteValue(DriverId, "Trace Level", profileProperties.TraceLogger.ToString()), Times.Once); + profileWrapperMock.Verify(x => x.WriteValue(DriverId, "COM Port", profileProperties.ComPort), Times.Once); + } + + [Test] + public void ReadProfile_WhenCalled_ReturnsExpectedDefaultValues() + { + string DriverId = "ASCOM.MeadeGeneric.Telescope"; + + string ComPortDefault = "COM1"; + string TraceStateDefault = "false"; + string GuideRateProfileNameDefault = "10.077939"; //67% of sidereal rate + string PrecisionDefault = "Unchanged"; + string GuidingStyleDefault = "Auto"; + string BacklashCompensationDefault = "3000"; + string ReverseFocuserDiectionDefault = "true"; + + Mock profileWrapperMock = new Mock(); + profileWrapperMock.SetupAllProperties(); + + profileWrapperMock.Setup(x => x.GetValue(DriverId, "Trace Level", string.Empty, TraceStateDefault)) + .Returns(() => + TraceStateDefault); + profileWrapperMock.Setup(x => x.GetValue(DriverId, "COM Port", string.Empty, ComPortDefault)) + .Returns(ComPortDefault); + profileWrapperMock + .Setup(x => x.GetValue(DriverId, "Guide Rate Arc Seconds Per Second", string.Empty, + GuideRateProfileNameDefault)).Returns(GuideRateProfileNameDefault); + profileWrapperMock.Setup(x => x.GetValue(DriverId, "Precision", string.Empty, PrecisionDefault)) + .Returns(PrecisionDefault); + profileWrapperMock.Setup(x => x.GetValue(DriverId, "Guiding Style", string.Empty, GuidingStyleDefault)) + .Returns(GuidingStyleDefault); + profileWrapperMock.Setup(x => + x.GetValue(DriverId, "Backlash Compensation", string.Empty, BacklashCompensationDefault)) + .Returns(BacklashCompensationDefault); + profileWrapperMock.Setup(x => + x.GetValue(DriverId, "Reverse Focuser Direction", string.Empty, ReverseFocuserDiectionDefault)) + .Returns(() => ReverseFocuserDiectionDefault); + + profileWrapperMock.Setup(x => + x.GetValue(DriverId, It.IsRegex("^SwitchName_[0-9{1}]$"), string.Empty, It.IsAny())) + .Returns((string driverId, string name, string subKey, string defaultValue) => defaultValue); + + IProfileWrapper profeWrapper = profileWrapperMock.Object; + + Mock profileFactoryMock = new Mock(); + profileFactoryMock.Setup(x => x.Create()).Returns(profileWrapperMock.Object); + + SharedResources.ProfileFactory = profileFactoryMock.Object; + + var profileProperties = SharedResources.ReadProfile(); + + Assert.That(profeWrapper.DeviceType, Is.EqualTo("Telescope")); + Assert.That(profileProperties.ComPort, Is.EqualTo(ComPortDefault)); + Assert.That(profileProperties.TraceLogger, Is.EqualTo(bool.Parse(TraceStateDefault))); + } + + [TestCase("TCP")] + [TestCase("Carrier Pigeon")] + public void Connect_WhenDeviceIdIsNotSerial_ThenThrowsException(string deviceId) + { + var result = Assert.Throws(() => { SharedResources.Connect(deviceId, string.Empty, _traceLoggerMock.Object); }); + + Assert.That(result.Message, Is.EqualTo($"deviceId {deviceId} not currently supported")); + } + } +} diff --git a/LynxAstro.DewController.UnitTests/packages.config b/LynxAstro.DewController.UnitTests/packages.config new file mode 100644 index 0000000..f485460 --- /dev/null +++ b/LynxAstro.DewController.UnitTests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController.sln b/LynxAstro.DewController.sln new file mode 100644 index 0000000..85c1de8 --- /dev/null +++ b/LynxAstro.DewController.sln @@ -0,0 +1,88 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29905.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynxAstro.DewController", "LynxAstro.DewController\LynxAstro.DewController.csproj", "{C708E487-E3A9-4073-A545-294B88674225}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynxAstro.DewController.Switch", "LynxAstro.DewController.Switch\LynxAstro.DewController.Switch.csproj", "{64308775-BD4A-469C-BCAB-3ED830B811AF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{7CAA050A-2EB3-4A21-8A75-131EB0AC9778}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynxAstro.DewController.UnitTests", "LynxAstro.DewController.UnitTests\LynxAstro.DewController.UnitTests.csproj", "{BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynxAstro.DewController.Switch.UnitTests", "LynxAstro.DewController.Switch.UnitTests\LynxAstro.DewController.Switch.UnitTests.csproj", "{AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}" +EndProject +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "LynxAstro.DewController.Setup", "LynxAstro.DewController.Setup\LynxAstro.DewController.Setup.wixproj", "{1073A462-D9A4-4C72-9C3F-A345B307153A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test Console", "Test Console", "{9B4388AE-0FB9-4C65-9C3F-000CE932E110}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LynxAstro.DewController.TestConsole", "LynxAstro.DewController.TestConsole\LynxAstro.DewController.TestConsole.csproj", "{D5207217-61C7-4E94-8097-91DBACE57D2A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C708E487-E3A9-4073-A545-294B88674225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Debug|x86.ActiveCfg = Debug|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Debug|x86.Build.0 = Debug|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Release|Any CPU.Build.0 = Release|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Release|x86.ActiveCfg = Release|Any CPU + {C708E487-E3A9-4073-A545-294B88674225}.Release|x86.Build.0 = Release|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Debug|x86.Build.0 = Debug|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Release|Any CPU.Build.0 = Release|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Release|x86.ActiveCfg = Release|Any CPU + {64308775-BD4A-469C-BCAB-3ED830B811AF}.Release|x86.Build.0 = Release|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Debug|x86.ActiveCfg = Debug|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Debug|x86.Build.0 = Debug|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Release|Any CPU.Build.0 = Release|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Release|x86.ActiveCfg = Release|Any CPU + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD}.Release|x86.Build.0 = Release|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Debug|x86.ActiveCfg = Debug|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Debug|x86.Build.0 = Debug|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Release|Any CPU.Build.0 = Release|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Release|x86.ActiveCfg = Release|Any CPU + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F}.Release|x86.Build.0 = Release|Any CPU + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Debug|Any CPU.Build.0 = Debug|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Debug|x86.ActiveCfg = Debug|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Debug|x86.Build.0 = Debug|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|Any CPU.ActiveCfg = Release|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|x86.ActiveCfg = Release|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|x86.Build.0 = Release|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Debug|Any CPU.ActiveCfg = Debug|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Debug|Any CPU.Build.0 = Debug|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Debug|x86.ActiveCfg = Debug|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Debug|x86.Build.0 = Debug|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Release|Any CPU.ActiveCfg = Release|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Release|x86.ActiveCfg = Release|x86 + {D5207217-61C7-4E94-8097-91DBACE57D2A}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BDED8EBB-7F9F-4710-8B74-59298C2DFCAD} = {7CAA050A-2EB3-4A21-8A75-131EB0AC9778} + {AFA5A4A4-188C-4180-B910-7C5BA7D5C11F} = {7CAA050A-2EB3-4A21-8A75-131EB0AC9778} + {D5207217-61C7-4E94-8097-91DBACE57D2A} = {9B4388AE-0FB9-4C65-9C3F-000CE932E110} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {343D05C4-AD6F-4A05-A971-ECAABF3294AF} + EndGlobalSection +EndGlobal diff --git a/LynxAstro.DewController/AssemblyInfo.cs b/LynxAstro.DewController/AssemblyInfo.cs new file mode 100644 index 0000000..61470c7 --- /dev/null +++ b/LynxAstro.DewController/AssemblyInfo.cs @@ -0,0 +1,91 @@ +using System; +using System.Reflection; + +namespace ASCOM.LynxAstro.DewController +{ + public class AssemblyInfo + { + // The assembly information values. + //public readonly string Title = string.Empty; + //public readonly string Description = string.Empty; + //public readonly string Company = string.Empty; + public readonly string Product = string.Empty; + //public readonly string Copyright = string.Empty; + //public readonly string Trademark = string.Empty; + public readonly string AssemblyVersion; + //public readonly string FileVersion = string.Empty; + //public readonly string Guid = string.Empty; + //public readonly string NeutralLanguage = string.Empty; + //public readonly bool IsComVisible; + + // Return a particular assembly attribute value. + private T GetAssemblyAttribute(Assembly assembly) + where T : Attribute + { + // Get attributes of this type. + object[] attributes = assembly.GetCustomAttributes(typeof(T), true); + + // If we didn't get anything, return null. + if (attributes.Length == 0) + return null; + + // Convert the first attribute value into + // the desired type and return it. + return (T)attributes[0]; + } + + // Constructors. + public AssemblyInfo() + : this(Assembly.GetExecutingAssembly()) + { + } + + private AssemblyInfo(Assembly assembly) + { + // Get values from the assembly. + //var titleAttr = GetAssemblyAttribute(assembly); + //if (titleAttr != null) + // Title = titleAttr.Title; + + //var assemblyAttr = GetAssemblyAttribute(assembly); + //if (assemblyAttr != null) + // Description = assemblyAttr.Description; + + //var companyAttr =GetAssemblyAttribute(assembly); + //if (companyAttr != null) + // Company = companyAttr.Company; + + var productAttr = GetAssemblyAttribute(assembly); + if (productAttr != null) + Product = productAttr.Product; + + //var copyrightAttr = GetAssemblyAttribute(assembly); + //if (copyrightAttr != null) + // Copyright = copyrightAttr.Copyright; + + //var trademarkAttr = GetAssemblyAttribute(assembly); + //if (trademarkAttr != null) + // Trademark = trademarkAttr.Trademark; + + var version = assembly.GetName().Version; + AssemblyVersion = $"{version.Major}.{version.Minor}.{version.Build}.{version.Revision}"; + + + //var fileVersionAttr = GetAssemblyAttribute(assembly); + //if (fileVersionAttr != null) FileVersion = + // fileVersionAttr.Version; + + //var guidAttr = GetAssemblyAttribute(assembly); + //if (guidAttr != null) + // Guid = guidAttr.Value; + + //var languageAttr = GetAssemblyAttribute(assembly); + //if (languageAttr != null) + // NeutralLanguage = languageAttr.CultureName; + + //var comAttr = GetAssemblyAttribute(assembly); + //if (comAttr != null) + // IsComVisible = comAttr.Value; + } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/ClassFactory.cs b/LynxAstro.DewController/ClassFactory.cs new file mode 100644 index 0000000..159e1e7 --- /dev/null +++ b/LynxAstro.DewController/ClassFactory.cs @@ -0,0 +1,244 @@ +using System; +using System.Runtime.InteropServices; +using System.Collections; + +namespace ASCOM.LynxAstro.DewController +{ + + #region C# Definition of IClassFactory + // + // Provide a definition of theCOM IClassFactory interface. + // + [ + ComImport, // This interface originated from COM. + ComVisible(false), // Must not be exposed to COM!!! + InterfaceType(ComInterfaceType.InterfaceIsIUnknown), // Indicate that this interface is not IDispatch-based. + Guid("00000001-0000-0000-C000-000000000046") // This GUID is the actual GUID of IClassFactory. + ] + public interface IClassFactory + { + void CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject); + void LockServer(bool fLock); + } + #endregion + + // + // Universal ClassFactory. Given a type as a parameter of the + // constructor, it implements IClassFactory for any interface + // that the class implements. Magic!!! + // + public class ClassFactory : IClassFactory + { + + #region Access to ole32.dll functions for class factories + + // Define two common GUID objects for public usage. + public static Guid IID_IUnknown = new Guid("{00000000-0000-0000-C000-000000000046}"); + public static Guid IID_IDispatch = new Guid("{00020400-0000-0000-C000-000000000046}"); + + [Flags] + enum CLSCTX : uint + { + CLSCTX_INPROC_SERVER = 0x1, + CLSCTX_INPROC_HANDLER = 0x2, + CLSCTX_LOCAL_SERVER = 0x4, + CLSCTX_INPROC_SERVER16 = 0x8, + CLSCTX_REMOTE_SERVER = 0x10, + CLSCTX_INPROC_HANDLER16 = 0x20, + CLSCTX_RESERVED1 = 0x40, + CLSCTX_RESERVED2 = 0x80, + CLSCTX_RESERVED3 = 0x100, + CLSCTX_RESERVED4 = 0x200, + CLSCTX_NO_CODE_DOWNLOAD = 0x400, + CLSCTX_RESERVED5 = 0x800, + CLSCTX_NO_CUSTOM_MARSHAL = 0x1000, + CLSCTX_ENABLE_CODE_DOWNLOAD = 0x2000, + CLSCTX_NO_FAILURE_LOG = 0x4000, + CLSCTX_DISABLE_AAA = 0x8000, + CLSCTX_ENABLE_AAA = 0x10000, + CLSCTX_FROM_DEFAULT_CONTEXT = 0x20000, + CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, + CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, + CLSCTX_ALL = CLSCTX_SERVER | CLSCTX_INPROC_HANDLER + } + + [Flags] + enum REGCLS : uint + { + REGCLS_SINGLEUSE = 0, + REGCLS_MULTIPLEUSE = 1, + REGCLS_MULTI_SEPARATE = 2, + REGCLS_SUSPENDED = 4, + REGCLS_SURROGATE = 8 + } + // + // CoRegisterClassObject() is used to register a Class Factory + // into COM's internal table of Class Factories. + // + [DllImport("ole32.dll")] + static extern int CoRegisterClassObject( + [In] ref Guid rclsid, + [MarshalAs(UnmanagedType.IUnknown)] object pUnk, + uint dwClsContext, + uint flags, + out uint lpdwRegister); + // + // Called by a COM EXE Server that can register multiple class objects + // to inform COM about all registered classes, and permits activation + // requests for those class objects. + // This function causes OLE to inform the SCM about all the registered + // classes, and begins letting activation requests into the server process. + // + [DllImport("ole32.dll")] + static extern int CoResumeClassObjects(); + // + // Prevents any new activation requests from the SCM on all class objects + // registered within the process. Even though a process may call this API, + // the process still must call CoRevokeClassObject for each CLSID it has + // registered, in the apartment it registered in. + // + [DllImport("ole32.dll")] + static extern int CoSuspendClassObjects(); + // + // CoRevokeClassObject() is used to unregister a Class Factory + // from COM's internal table of Class Factories. + // + [DllImport("ole32.dll")] + static extern int CoRevokeClassObject(uint dwRegister); + #endregion + + #region Constructor and Private ClassFactory Data + + protected Type m_ClassType; + protected Guid m_ClassId; + protected ArrayList m_InterfaceTypes; + protected uint m_ClassContext; + protected uint m_Flags; + protected UInt32 m_locked = 0; + protected uint m_Cookie; + protected string m_progid; + + public ClassFactory(Type type) + { + if (type == null) + throw new ArgumentNullException("type"); + m_ClassType = type; + + //PWGS Get the ProgID from the MetaData + m_progid = Marshal.GenerateProgIdForType(type); + m_ClassId = Marshal.GenerateGuidForType(type); // Should be nailed down by [Guid(...)] + m_ClassContext = (uint)CLSCTX.CLSCTX_LOCAL_SERVER; // Default + m_Flags = (uint)REGCLS.REGCLS_MULTIPLEUSE | // Default + (uint)REGCLS.REGCLS_SUSPENDED; + m_InterfaceTypes = new ArrayList(); + foreach (Type T in type.GetInterfaces()) // Save all of the implemented interfaces + m_InterfaceTypes.Add(T); + } + + #endregion + + #region Common ClassFactory Methods + public uint ClassContext + { + get { return m_ClassContext; } + set { m_ClassContext = value; } + } + + public Guid ClassId + { + get { return m_ClassId; } + set { m_ClassId = value; } + } + + public uint Flags + { + get { return m_Flags; } + set { m_Flags = value; } + } + + public bool RegisterClassObject() + { + // Register the class factory + int i = CoRegisterClassObject + ( + ref m_ClassId, + this, + m_ClassContext, + m_Flags, + out m_Cookie + ); + return (i == 0); + } + + public bool RevokeClassObject() + { + int i = CoRevokeClassObject(m_Cookie); + return (i == 0); + } + + public static bool ResumeClassObjects() + { + int i = CoResumeClassObjects(); + return (i == 0); + } + + public static bool SuspendClassObjects() + { + int i = CoSuspendClassObjects(); + return (i == 0); + } + #endregion + + #region IClassFactory Implementations + // + // Implement creation of the type and interface. + // + void IClassFactory.CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject) + { + IntPtr nullPtr = new IntPtr(0); + ppvObject = nullPtr; + + // + // Handle specific requests for implemented interfaces + // + foreach (Type iType in m_InterfaceTypes) + { + if (riid == Marshal.GenerateGuidForType(iType)) + { + ppvObject = Marshal.GetComInterfaceForObject(Activator.CreateInstance(m_ClassType), iType); + return; + } + } + // + // Handle requests for IDispatch or IUnknown on the class + // + if (riid == IID_IDispatch) + { + ppvObject = Marshal.GetIDispatchForObject(Activator.CreateInstance(m_ClassType)); + return; + } + else if (riid == IID_IUnknown) + { + ppvObject = Marshal.GetIUnknownForObject(Activator.CreateInstance(m_ClassType)); + } + else + { + // + // Oops, some interface that the class doesn't implement + // + throw new COMException("No interface", unchecked((int)0x80004002)); + } + } + + void IClassFactory.LockServer(bool bLock) + { + if (bLock) + Server.CountLock(); + else + Server.UncountLock(); + // Always attempt to see if we need to shutdown this server application. + Server.ExitIf(); + } + #endregion + } +} diff --git a/LynxAstro.DewController/ConnectionInfo.cs b/LynxAstro.DewController/ConnectionInfo.cs new file mode 100644 index 0000000..5ac8175 --- /dev/null +++ b/LynxAstro.DewController/ConnectionInfo.cs @@ -0,0 +1,8 @@ +namespace ASCOM.LynxAstro.DewController +{ + public class ConnectionInfo + { + //public int Connections { get; set; } + public int SameDevice { get; set; } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/GarbageCollection.cs b/LynxAstro.DewController/GarbageCollection.cs new file mode 100644 index 0000000..dfc92f8 --- /dev/null +++ b/LynxAstro.DewController/GarbageCollection.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; + +namespace ASCOM.LynxAstro.DewController +{ + /// + /// Summary description for GarbageCollection. + /// + class GarbageCollection + { + protected bool m_bContinueThread; + protected bool m_GCWatchStopped; + protected int m_iInterval; + protected ManualResetEvent m_EventThreadEnded; + + public GarbageCollection(int iInterval) + { + m_bContinueThread = true; + m_GCWatchStopped = false; + m_iInterval = iInterval; + m_EventThreadEnded = new ManualResetEvent(false); + } + + public void GCWatch() + { + // Pause for a moment to provide a delay to make threads more apparent. + while (ContinueThread()) + { + GC.Collect(); + Thread.Sleep(m_iInterval); + } + m_EventThreadEnded.Set(); + } + + protected bool ContinueThread() + { + lock (this) + { + return m_bContinueThread; + } + } + + public void StopThread() + { + lock (this) + { + m_bContinueThread = false; + } + } + + public void WaitForThreadToStop() + { + m_EventThreadEnded.WaitOne(); + m_EventThreadEnded.Reset(); + } + } +} diff --git a/LynxAstro.DewController/IProfileFactory.cs b/LynxAstro.DewController/IProfileFactory.cs new file mode 100644 index 0000000..94f0919 --- /dev/null +++ b/LynxAstro.DewController/IProfileFactory.cs @@ -0,0 +1,7 @@ +namespace ASCOM.LynxAstro.DewController +{ + public interface IProfileFactory + { + IProfileWrapper Create(); + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/IProfileWrapper.cs b/LynxAstro.DewController/IProfileWrapper.cs new file mode 100644 index 0000000..6b320d0 --- /dev/null +++ b/LynxAstro.DewController/IProfileWrapper.cs @@ -0,0 +1,10 @@ +using System; +using ASCOM.Utilities.Interfaces; + +namespace ASCOM.LynxAstro.DewController +{ + public interface IProfileWrapper : IProfile, IProfileExtra, IDisposable + { + + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/ISharedResourcesWrapper.cs b/LynxAstro.DewController/ISharedResourcesWrapper.cs new file mode 100644 index 0000000..bbc575c --- /dev/null +++ b/LynxAstro.DewController/ISharedResourcesWrapper.cs @@ -0,0 +1,28 @@ +using System; +using ASCOM.Utilities.Interfaces; + +namespace ASCOM.LynxAstro.DewController +{ + public interface ISharedResourcesWrapper + { + ConnectionInfo Connect(string deviceId, string driverId, ITraceLogger traceLogger); + void Disconnect(string deviceId, string driverId); + + string FirmwareVersion { get; } + + void Lock(Action action); + T Lock(Func func); + + string SendString(string message); + void SendBlind(string message); + string SendChar(string message); + + string ReadTerminated(); + + ProfileProperties ReadProfile(); + + void SetupDialog(); + void WriteProfile(ProfileProperties profileProperties); + void ReadCharacters(int throwAwayCharacters); + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/LocalServer.cs b/LynxAstro.DewController/LocalServer.cs new file mode 100644 index 0000000..f06b4cf --- /dev/null +++ b/LynxAstro.DewController/LocalServer.cs @@ -0,0 +1,640 @@ +// +// ASCOM.LynxAstro.DewController Local COM Server +// +// This is the core of a managed COM Local Server, capable of serving +// multiple instances of multiple interfaces, within a single +// executable. This implementes the equivalent functionality of VB6 +// which has been extensively used in ASCOM for drivers that provide +// multiple interfaces to multiple clients (e.g. Meade Telescope +// and Focuser) as well as hubs (e.g., POTH). +// +// Written by: Robert B. Denny (Version 1.0.1, 29-May-2007) +// Modified by Chris Rowland and Peter Simpson to allow use with multiple devices of the same type March 2011 +// +// +using System; +using System.IO; +using System.Windows.Forms; +using System.Collections; +using System.Runtime.InteropServices; +using System.Reflection; +using ASCOM.Utilities; +using Microsoft.Win32; +using System.Threading; +using System.Security.Principal; +using System.Diagnostics; + +namespace ASCOM.LynxAstro.DewController +{ + public static class Server + { + + #region Access to kernel32.dll, user32.dll, and ole32.dll functions + [Flags] + enum CLSCTX : uint + { + CLSCTX_INPROC_SERVER = 0x1, + CLSCTX_INPROC_HANDLER = 0x2, + CLSCTX_LOCAL_SERVER = 0x4, + CLSCTX_INPROC_SERVER16 = 0x8, + CLSCTX_REMOTE_SERVER = 0x10, + CLSCTX_INPROC_HANDLER16 = 0x20, + CLSCTX_RESERVED1 = 0x40, + CLSCTX_RESERVED2 = 0x80, + CLSCTX_RESERVED3 = 0x100, + CLSCTX_RESERVED4 = 0x200, + CLSCTX_NO_CODE_DOWNLOAD = 0x400, + CLSCTX_RESERVED5 = 0x800, + CLSCTX_NO_CUSTOM_MARSHAL = 0x1000, + CLSCTX_ENABLE_CODE_DOWNLOAD = 0x2000, + CLSCTX_NO_FAILURE_LOG = 0x4000, + CLSCTX_DISABLE_AAA = 0x8000, + CLSCTX_ENABLE_AAA = 0x10000, + CLSCTX_FROM_DEFAULT_CONTEXT = 0x20000, + CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, + CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER, + CLSCTX_ALL = CLSCTX_SERVER | CLSCTX_INPROC_HANDLER + } + + [Flags] + enum COINIT : uint + { + /// Initializes the thread for multi-threaded object concurrency. + COINIT_MULTITHREADED = 0x0, + /// Initializes the thread for apartment-threaded object concurrency. + COINIT_APARTMENTTHREADED = 0x2, + /// Disables DDE for Ole1 support. + COINIT_DISABLE_OLE1DDE = 0x4, + /// Trades memory for speed. + COINIT_SPEED_OVER_MEMORY = 0x8 + } + + [Flags] + enum REGCLS : uint + { + REGCLS_SINGLEUSE = 0, + REGCLS_MULTIPLEUSE = 1, + REGCLS_MULTI_SEPARATE = 2, + REGCLS_SUSPENDED = 4, + REGCLS_SURROGATE = 8 + } + + + // CoInitializeEx() can be used to set the apartment model + // of individual threads. + [DllImport("ole32.dll")] + static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit); + + // CoUninitialize() is used to uninitialize a COM thread. + [DllImport("ole32.dll")] + static extern void CoUninitialize(); + + // PostThreadMessage() allows us to post a Windows Message to + // a specific thread (identified by its thread id). + // We will need this API to post a WM_QUIT message to the main + // thread in order to terminate this application. + [DllImport("user32.dll")] + static extern bool PostThreadMessage(uint idThread, uint Msg, UIntPtr wParam, + IntPtr lParam); + + // GetCurrentThreadId() allows us to obtain the thread id of the + // calling thread. This allows us to post the WM_QUIT message to + // the main thread. + [DllImport("kernel32.dll")] + static extern uint GetCurrentThreadId(); + #endregion + + #region Private Data + private static int objsInUse; // Keeps a count on the total number of objects alive. + private static int serverLocks; // Keeps a lock count on this application. + private static frmMain s_MainForm = null; // Reference to our main form + private static ArrayList s_ComObjectAssys; // Dynamically loaded assemblies containing served COM objects + private static ArrayList s_ComObjectTypes; // Served COM object types + private static ArrayList s_ClassFactories; // Served COM object class factories + private static string s_appId = "{34e5f3f3-29e4-4d87-b861-c079485a5c97}"; // Our AppId + private static readonly Object lockObject = new object(); + #endregion + + // This property returns the main thread's id. + public static uint MainThreadId { get; private set; } // Stores the main thread's thread id. + + // Used to tell if started by COM or manually + public static bool StartedByCOM { get; private set; } // True if server started by COM (-embedding) + + + #region Server Lock, Object Counting, and AutoQuit on COM startup + // Returns the total number of objects alive currently. + public static int ObjectsCount + { + get + { + lock (lockObject) + { + return objsInUse; + } + } + } + + // This method performs a thread-safe incrementation of the objects count. + public static int CountObject() + { + // Increment the global count of objects. + return Interlocked.Increment(ref objsInUse); + } + + // This method performs a thread-safe decrementation the objects count. + public static int UncountObject() + { + // Decrement the global count of objects. + return Interlocked.Decrement(ref objsInUse); + } + + // Returns the current server lock count. + public static int ServerLockCount + { + get + { + lock (lockObject) + { + return serverLocks; + } + } + } + + // This method performs a thread-safe incrementation the + // server lock count. + public static int CountLock() + { + // Increment the global lock count of this server. + return Interlocked.Increment(ref serverLocks); + } + + // This method performs a thread-safe decrementation the + // server lock count. + public static int UncountLock() + { + // Decrement the global lock count of this server. + return Interlocked.Decrement(ref serverLocks); + } + + // AttemptToTerminateServer() will check to see if the objects count and the server + // lock count have both dropped to zero. + // + // If so, and if we were started by COM, we post a WM_QUIT message to the main thread's + // message loop. This will cause the message loop to exit and hence the termination + // of this application. If hand-started, then just trace that it WOULD exit now. + // + public static void ExitIf() + { + lock (lockObject) + { + if ((ObjectsCount <= 0) && (ServerLockCount <= 0)) + { + if (StartedByCOM) + { + UIntPtr wParam = new UIntPtr(0); + IntPtr lParam = new IntPtr(0); + PostThreadMessage(MainThreadId, 0x0012, wParam, lParam); + } + } + } + } + #endregion + + // ----------------- + // PRIVATE FUNCTIONS + // ----------------- + + #region Dynamic Driver Assembly Loader + // + // Load the assemblies that contain the classes that we will serve + // via COM. These will be located in the same folder as + // our executable. + // + private static bool LoadComObjectAssemblies() + { + s_ComObjectAssys = new ArrayList(); + s_ComObjectTypes = new ArrayList(); + + // put everything into one folder, the same as the server. + string assyPath = Assembly.GetEntryAssembly().Location; + assyPath = Path.GetDirectoryName(assyPath); + + DirectoryInfo d = new DirectoryInfo(assyPath); + foreach (FileInfo fi in d.GetFiles("*.dll")) + { + string aPath = fi.FullName; + // + // First try to load the assembly and get the types for + // the class and the class factory. If this doesn't work ???? + // + try + { + Assembly so = Assembly.LoadFrom(aPath); + //PWGS Get the types in the assembly + Type[] types = so.GetTypes(); + foreach (Type type in types) + { + // PWGS Now checks the type rather than the assembly + // Check to see if the type has the ServedClassName attribute, only use it if it does. + MemberInfo info = type; + + object[] attrbutes = info.GetCustomAttributes(typeof(ServedClassNameAttribute), false); + if (attrbutes.Length > 0) + { + //MessageBox.Show("Adding Type: " + type.Name + " " + type.FullName); + s_ComObjectTypes.Add(type); //PWGS - much simpler + s_ComObjectAssys.Add(so); + } + } + } + catch (BadImageFormatException) + { + // Probably an attempt to load a Win32 DLL (i.e. not a .net assembly) + // Just swallow the exception and continue to the next item. + continue; + } + catch (Exception e) + { + MessageBox.Show("Failed to load served COM class assembly " + fi.Name + " - " + e.Message, + "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Stop); + return false; + } + + } + return true; + } + #endregion + + #region COM Registration and Unregistration + // + // Test if running elevated + // + private static bool IsAdministrator + { + get + { + WindowsIdentity i = WindowsIdentity.GetCurrent(); + WindowsPrincipal p = new WindowsPrincipal(i); + return p.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + // + // Elevate by re-running ourselves with elevation dialog + // + private static void ElevateSelf(string arg) + { + ProcessStartInfo si = new ProcessStartInfo(); + si.Arguments = arg; + si.WorkingDirectory = Environment.CurrentDirectory; + si.FileName = Application.ExecutablePath; + si.Verb = "runas"; + try { Process.Start(si); } + catch (System.ComponentModel.Win32Exception) + { + MessageBox.Show("The LynxAstro.DewController was not " + (arg == "/register" ? "registered" : "unregistered") + + " because you did not allow it.", "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Warning); + } + catch (Exception ex) + { + MessageBox.Show(ex.ToString(), "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Stop); + } + return; + } + + // + // Do everything to register this for COM. Never use REGASM on + // this exe assembly! It would create InProcServer32 entries + // which would prevent proper activation! + // + // Using the list of COM object types generated during dynamic + // assembly loading, it registers each one for COM as served by our + // exe/local server, as well as registering it for ASCOM. It also + // adds DCOM info for the local server itself, so it can be activated + // via an outboiud connection from TheSky. + // + private static void RegisterObjects() + { + if (!IsAdministrator) + { + ElevateSelf("/register"); + return; + } + // + // If reached here, we're running elevated + // + + Assembly assy = Assembly.GetExecutingAssembly(); + Attribute attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyTitleAttribute)); + string assyTitle = ((AssemblyTitleAttribute)attr).Title; + attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyDescriptionAttribute)); + string assyDescription = ((AssemblyDescriptionAttribute)attr).Description; + + // + // Local server's DCOM/AppID information + // + try + { + // + // HKCR\APPID\appid + // + using (RegistryKey key = Registry.ClassesRoot.CreateSubKey("APPID\\" + s_appId)) + { + key.SetValue(null, assyDescription); + key.SetValue("AppID", s_appId); + key.SetValue("AuthenticationLevel", 1, RegistryValueKind.DWord); + key.SetValue("RunAs", "Interactive User", RegistryValueKind.String); // Added to ensure that only one copy of the local server runs if the user uses both elevated and non-elevated clients concurrently + } + // + // HKCR\APPID\exename.ext + // + using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(string.Format("APPID\\{0}", + Application.ExecutablePath.Substring(Application.ExecutablePath.LastIndexOf('\\') + 1)))) + { + key.SetValue("AppID", s_appId); + } + } + catch (Exception ex) + { + MessageBox.Show("Error while registering the server:\n" + ex.ToString(), + "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Stop); + return; + } + finally + { + } + + // + // For each of the driver assemblies + // + foreach (Type type in s_ComObjectTypes) + { + bool bFail = false; + try + { + // + // HKCR\CLSID\clsid + // + string clsid = Marshal.GenerateGuidForType(type).ToString("B"); + string progid = Marshal.GenerateProgIdForType(type); + //PWGS Generate device type from the Class name + string deviceType = type.Name; + + using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(string.Format("CLSID\\{0}", clsid))) + { + key.SetValue(null, progid); // Could be assyTitle/Desc??, but .NET components show ProgId here + key.SetValue("AppId", s_appId); + using (RegistryKey key2 = key.CreateSubKey("Implemented Categories")) + { + key2.CreateSubKey("{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}"); + } + using (RegistryKey key2 = key.CreateSubKey("ProgId")) + { + key2.SetValue(null, progid); + } + key.CreateSubKey("Programmable"); + using (RegistryKey key2 = key.CreateSubKey("LocalServer32")) + { + key2.SetValue(null, Application.ExecutablePath); + } + } + // + // HKCR\CLSID\progid + // + using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(progid)) + { + key.SetValue(null, assyTitle); + using (RegistryKey key2 = key.CreateSubKey("CLSID")) + { + key2.SetValue(null, clsid); + } + } + // + // ASCOM + // + assy = type.Assembly; + + // Pull the display name from the ServedClassName attribute. + attr = Attribute.GetCustomAttribute(type, typeof(ServedClassNameAttribute)); //PWGS Changed to search type for attribute rather than assembly + string chooserName = ((ServedClassNameAttribute)attr).DisplayName ?? "MultiServer"; + using (var P = new Profile()) + { + P.DeviceType = deviceType; + P.Register(progid, chooserName); + } + } + catch (Exception ex) + { + MessageBox.Show("Error while registering the server:\n" + ex.ToString(), + "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Stop); + bFail = true; + } + finally + { + } + if (bFail) break; + } + } + + // + // Remove all traces of this from the registry. + // + // **TODO** If the above does AppID/DCOM stuff, this would have + // to remove that stuff too. + // + private static void UnregisterObjects() + { + if (!IsAdministrator) + { + ElevateSelf("/unregister"); + return; + } + + // + // Local server's DCOM/AppID information + // + Registry.ClassesRoot.DeleteSubKey(string.Format("APPID\\{0}", s_appId), false); + Registry.ClassesRoot.DeleteSubKey(string.Format("APPID\\{0}", + Application.ExecutablePath.Substring(Application.ExecutablePath.LastIndexOf('\\') + 1)), false); + + // + // For each of the driver assemblies + // + foreach (Type type in s_ComObjectTypes) + { + string clsid = Marshal.GenerateGuidForType(type).ToString("B"); + string progid = Marshal.GenerateProgIdForType(type); + string deviceType = type.Name; + // + // Best efforts + // + // + // HKCR\progid + // + Registry.ClassesRoot.DeleteSubKey(String.Format("{0}\\CLSID", progid), false); + Registry.ClassesRoot.DeleteSubKey(progid, false); + // + // HKCR\CLSID\clsid + // + Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Implemented Categories\\{{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}}", clsid), false); + Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Implemented Categories", clsid), false); + Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\ProgId", clsid), false); + Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\LocalServer32", clsid), false); + Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Programmable", clsid), false); + Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}", clsid), false); + try + { + // + // ASCOM + // + using (var P = new Profile()) + { + P.DeviceType = deviceType; + P.Unregister(progid); + } + } + catch (Exception) { } + } + } + #endregion + + #region Class Factory Support + // + // On startup, we register the class factories of the COM objects + // that we serve. This requires the class facgtory name to be + // equal to the served class name + "ClassFactory". + // + private static bool RegisterClassFactories() + { + s_ClassFactories = new ArrayList(); + foreach (Type type in s_ComObjectTypes) + { + ClassFactory factory = new ClassFactory(type); // Use default context & flags + s_ClassFactories.Add(factory); + if (!factory.RegisterClassObject()) + { + MessageBox.Show("Failed to register class factory for " + type.Name, + "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Stop); + return false; + } + } + ClassFactory.ResumeClassObjects(); // Served objects now go live + return true; + } + + private static void RevokeClassFactories() + { + ClassFactory.SuspendClassObjects(); // Prevent race conditions + foreach (ClassFactory factory in s_ClassFactories) + factory.RevokeClassObject(); + } + #endregion + + #region Command Line Arguments + // + // ProcessArguments() will process the command-line arguments + // If the return value is true, we carry on and start this application. + // If the return value is false, we terminate this application immediately. + // + private static bool ProcessArguments(string[] args) + { + bool bRet = true; + + // + //**TODO** -Embedding is "ActiveX start". Prohibit non_AX starting? + // + if (args.Length > 0) + { + + switch (args[0].ToLower()) + { + case "-embedding": + StartedByCOM = true; // Indicate COM started us + break; + + case "-register": + case @"/register": + case "-regserver": // Emulate VB6 + case @"/regserver": + RegisterObjects(); // Register each served object + bRet = false; + break; + + case "-unregister": + case @"/unregister": + case "-unregserver": // Emulate VB6 + case @"/unregserver": + UnregisterObjects(); //Unregister each served object + bRet = false; + break; + + default: + MessageBox.Show("Unknown argument: " + args[0] + "\nValid are : -register, -unregister and -embedding", + "LynxAstro.DewController", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + break; + } + } + else + StartedByCOM = false; + + return bRet; + } + #endregion + + #region SERVER ENTRY POINT (main) + // + // ================== + // SERVER ENTRY POINT + // ================== + // + [STAThread] + static void Main(string[] args) + { + if (!LoadComObjectAssemblies()) return; // Load served COM class assemblies, get types + + if (!ProcessArguments(args)) return; // Register/Unregister + + // Initialize critical member variables. + objsInUse = 0; + serverLocks = 0; + MainThreadId = GetCurrentThreadId(); + Thread.CurrentThread.Name = "Main Thread"; + + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + s_MainForm = new frmMain(); + if (StartedByCOM) s_MainForm.WindowState = FormWindowState.Minimized; + + // Register the class factories of the served objects + RegisterClassFactories(); + + // Start up the garbage collection thread. + GarbageCollection GarbageCollector = new GarbageCollection(1000); + Thread GCThread = new Thread(new ThreadStart(GarbageCollector.GCWatch)); + GCThread.Name = "Garbage Collection Thread"; + GCThread.Start(); + + // + // Start the message loop. This serializes incoming calls to our + // served COM objects, making this act like the VB6 equivalent! + // + try + { + Application.Run(s_MainForm); + } + finally + { + // Revoke the class factories immediately. + // Don't wait until the thread has stopped before + // we perform revocation!!! + RevokeClassFactories(); + + // Now stop the Garbage Collector thread. + GarbageCollector.StopThread(); + GarbageCollector.WaitForThreadToStop(); + } + } + #endregion + } +} diff --git a/LynxAstro.DewController/LocalServer.snk b/LynxAstro.DewController/LocalServer.snk new file mode 100644 index 0000000000000000000000000000000000000000..97e44064e7bc6d608913a5a8542b39243e53f825 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097L4}G28oM$F0^<%6-cFiYYwSe?a7L$A- zPjs7(CKo#X44$udl=V0HdhSa|Y66!|Ee(v)u)x;tmy;IRJq0+T?30Vq3#N+7qYfU@ z9O=aZq`Jd&LLfG^7Q=v6%;?I7R*Ob~BNWW>wU0h|lZxR5RjYCU5hT%23xHk!l7y{w*Esa?cjU<{J?8%BZ&RTmS$T{Q8GL(z0_4No`a@f>bwzY>oTIZ?KKmt1 z%!cfuHpE9Qu{Tgt^*7ThFAo9E_{`1u)ALOuR?3|Kk%^P@5fo{=p-yZy|a-1WwMiN94P<1|k z=QFJxPSyMkCSL)@b@w(o(6~ALWpYG_snMlk_o;5nI$DnQ*Tg`u@w*iH%R;7PGk#FC z<^Xcp`x#c7`sRstdut^$U+rB}H21X-*C+yMr+P&V#vP~GM$sJiz;Q!T#JQl9p%jDV zE9>eGU#kLv8q?h#9EtW9Y@0W@DvTwt;ax~FLk2K^;9_V;6Y(tQxcBNk$F{w_mnD!8 zyu%keSLp3=yW&LBho=(UyK%1d3#C|3h%I1j_nvwvd6bwG@H`nK?OC-O5be;t(wl3c i#j%9ZS$2MI$Iv1T + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {C708E487-E3A9-4073-A545-294B88674225} + WinExe + Properties + ASCOM.LynxAstro.DewController + ASCOM.LynxAstro.DewController.Server + v4.7.2 + + + 2.0 + + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + AnyCPU + MinimumRecommendedRules.ruleset + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + x86 + false + + + true + + + LocalServer.snk + + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Astrometry.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Attributes.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Cache.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Controls.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.DeviceInterfaces.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.DriverAccess.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Exceptions.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Internal.Extensions.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.SettingsProvider.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Utilities.dll + + + ..\packages\ASCOM.Platform.6.5.2\lib\net40\ASCOM.Utilities.Video.dll + + + ..\packages\JetBrains.Annotations.2020.3.0\lib\net20\JetBrains.Annotations.dll + + + + + + + + + + + Form + + + frmMain.cs + + + + + + + + + + + + + Designer + frmMain.cs + + + True + True + Resources.resx + + + + Form + + + SetupDialogForm.cs + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + SetupDialogForm.cs + Designer + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + + + + + + + + \ No newline at end of file diff --git a/LynxAstro.DewController/ProfileFactory.cs b/LynxAstro.DewController/ProfileFactory.cs new file mode 100644 index 0000000..1316bb9 --- /dev/null +++ b/LynxAstro.DewController/ProfileFactory.cs @@ -0,0 +1,10 @@ +namespace ASCOM.LynxAstro.DewController +{ + public class ProfileFactory : IProfileFactory + { + public IProfileWrapper Create() + { + return new ProfileWrapper(); + } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/ProfileProperties.cs b/LynxAstro.DewController/ProfileProperties.cs new file mode 100644 index 0000000..075c1a6 --- /dev/null +++ b/LynxAstro.DewController/ProfileProperties.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace ASCOM.LynxAstro.DewController +{ + public class ProfileProperties + { + // properies that are part of the profile + public string ComPort { get; set; } + public bool TraceLogger { get; set; } + public List SwitchNames { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/ProfileWrapper.cs b/LynxAstro.DewController/ProfileWrapper.cs new file mode 100644 index 0000000..4b714c3 --- /dev/null +++ b/LynxAstro.DewController/ProfileWrapper.cs @@ -0,0 +1,132 @@ +using System.Collections; +using ASCOM.Utilities; + +namespace ASCOM.LynxAstro.DewController +{ + public class ProfileWrapper : IProfileWrapper + { + private readonly Profile _profile = new Profile(); + + public ArrayList RegisteredDevices(string deviceType) + { + return _profile.RegisteredDevices(deviceType); + } + + public bool IsRegistered(string driverId) + { + return _profile.IsRegistered(driverId); + } + + public void Register(string driverId, string descriptiveName) + { + _profile.Register(driverId, descriptiveName); + } + + public void Unregister(string driverId) + { + _profile.Unregister(driverId); + } + + public string GetValue(string driverId, string name, string subKey, string defaultValue) + { + return _profile.GetValue(driverId, name, subKey, defaultValue); + } + + public void WriteValue(string driverId, string name, string value, string subKey) + { + _profile.WriteValue(driverId, name, value); + } + + public ArrayList Values(string driverId, string subKey) + { + return _profile.Values(driverId, subKey); + } + + public void DeleteValue(string driverId, string name, string subKey) + { + _profile.DeleteValue(driverId, name, subKey); + } + + public void CreateSubKey(string driverId, string subKey) + { + _profile.CreateSubKey(driverId, subKey); + } + + public ArrayList SubKeys(string driverId, string subKey) + { + return _profile.SubKeys(driverId, subKey); + } + + public void DeleteSubKey(string driverId, string subKey) + { + _profile.DeleteSubKey(driverId, subKey); + } + + public string GetProfileXML(string deviceId) + { + return _profile.GetProfileXML(deviceId); + } + + public void SetProfileXML(string deviceId, string xml) + { + _profile.SetProfileXML(deviceId, xml); + } + + public string DeviceType + { + get => _profile.DeviceType; + set => _profile.DeviceType = value; + } + public ArrayList RegisteredDeviceTypes => _profile.RegisteredDeviceTypes; + + public void MigrateProfile(string currentPlatformVersion) + { + _profile.MigrateProfile(currentPlatformVersion); + } + + public void DeleteValue(string driverId, string name) + { + _profile.DeleteValue(driverId, name); + } + + public string GetValue(string driverId, string name) + { + return _profile.GetValue(driverId, name); + } + + public string GetValue(string driverId, string name, string subKey) + { + return _profile.GetValue(driverId, name, subKey); + } + + public ArrayList SubKeys(string driverId) + { + return _profile.SubKeys(driverId); + } + + public ArrayList Values(string driverId) + { + return _profile.Values(driverId); + } + + public void WriteValue(string driverId, string name, string value) + { + _profile.WriteValue(driverId, name, value); + } + + public ASCOMProfile GetProfile(string driverId) + { + return _profile.GetProfile(driverId); + } + + public void SetProfile(string driverId, ASCOMProfile xmlProfileKey) + { + _profile.SetProfile(driverId, xmlProfileKey); + } + + public void Dispose() + { + _profile.Dispose(); + } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/Properties/AssemblyInfo.cs b/LynxAstro.DewController/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..73d16a3 --- /dev/null +++ b/LynxAstro.DewController/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("ASCOM LynxAstro.DewController server")] +[assembly: AssemblyDescription("ASCOM multi-interface server for LynxAstro.DewController")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("ASCOM Initiative")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Copyright © 2021, colin")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("6.4.0.0")] +[assembly: AssemblyFileVersion("6.4.0.0")] + +[assembly: ComVisibleAttribute(false)] diff --git a/LynxAstro.DewController/Properties/Resources.Designer.cs b/LynxAstro.DewController/Properties/Resources.Designer.cs new file mode 100644 index 0000000..01bc24f --- /dev/null +++ b/LynxAstro.DewController/Properties/Resources.Designer.cs @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ASCOM.LynxAstro.DewController.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ASCOM.LynxAstro.DewController.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ASCOM { + get { + object obj = ResourceManager.GetObject("ASCOM", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized string similar to {0} Settings ({1}). + /// + internal static string SetupDialogForm_SetupDialogForm__0__Settings___1__ { + get { + return ResourceManager.GetString("SetupDialogForm_SetupDialogForm__0__Settings___1__", resourceCulture); + } + } + } +} diff --git a/LynxAstro.DewController/Properties/Resources.resx b/LynxAstro.DewController/Properties/Resources.resx new file mode 100644 index 0000000..d1d4df2 --- /dev/null +++ b/LynxAstro.DewController/Properties/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\ASCOM.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + {0} Settings ({1}) + + \ No newline at end of file diff --git a/LynxAstro.DewController/ReadMe.htm b/LynxAstro.DewController/ReadMe.htm new file mode 100644 index 0000000..73dcae4 --- /dev/null +++ b/LynxAstro.DewController/ReadMe.htm @@ -0,0 +1,666 @@ + + + + + Untitled Document + + + + + + + + + + + + +
+

+ ASCOM LocalServer (singleton) Host +

+
+

+
+
+ +

+

+ You have just created a local server (singleton) host for one or + more ASCOM driver classes. +

+
+

+ This project implements an ASCOM host server for one or more + driver classes in a single-instance executable. It can be used to + serve multiple instances of a single driver class (hub), provide + driver services for multiple devices (e.g., Telescope and Focuser) to + multiple applications and allow multiple devices of the same type to + be connected. In the latter scenario, the multiple driver classes + will often share one or more resources such as the serial connection + and a microcontroller in the combined device. From the client's + perspective, using the drivers served by the local server is exactly + the same as if the drivers are loaded into the client's process space + (in-proc servers). +

+

+ + NOTE: + + + Unless you are prepared to handle all of the timing issues that arise + when multiple clients are accessing the properties and methods of + your driver(s), stop now. Just because the local server serializes + the calls to your driver(s)' properties and methods does not mean + that there will be no timing or concurrency issues.
+  
For + example, suppose the hub serves instances of a Telescope driver. One + client sets the TargetRightAscension property, then another sets + TargetRightAscension to a different value, then the first client sets + TargetDeclination, then the first client calls SlewToTarget() + followed by the second client calling SlewToTarget(). Besides the + first client's slew command sending the scope to the wrong (and + possibly dangerous) coordinates, there is the problem of the second + client trying to slew a slewing scope. Local server drivers are + tricky to get right. There is no such thing as "ignorance is + bliss" here. + +
+

+

+ + This implementation has changed + from what was defined for Platform 5.5 as follows: + +

+

+ + The drivers are now installed in + the same folder as the local server executable. This makes deployment + cleaner because the whole driver can exist in a single folder + independently of other drivers. + +

+

+ + The ProgId and friendly name as + displayed by the Chooser are defined using attributes. This allows + driver dlls to be identified clearly and so avoids confusion with + other dlls that may be required such as interop dlls. + +

+

+ + Some changes have been made that + will facilitate generating multiple drivers of the same type. + +

+

+ + I've put some additional advice and + comments in the notes below in italics. + +

+

+ You're probably anxious to get going, but you really should read + through the Theory of Operation and + Detailed + Use and Deployment + below. +

+

You must do the following in order to complete your local server:

+
    +
  1. +

    + In the local server's project + properties, Application tab, change BOTH the AssemblyName and the + default assembly name to ASCOM.xxx (e.g., ASCOM.SuperScope). + This + may be done by default now. + +

    +
  2. +
  3. +

    + Add one or more driver skeleton + projects using the in-proc templates. You may use either the C# or + VB templates. Project name is not important (not used in ProgID) + choose something like TelescopeDriver. You will be changing the + substituted project name in these projects below. If you ensure that + the LocalServer and all the driver projects have the same NameSpace + e.g. ASCOM.SuperScope the renaming in section 6a will not be + required. +

    +
  4. +
  5. +

    + Develop and debug these driver + projects as normal in-process assemblies. This will be much simpler + because the driver and test code can be debugged in the same + process. +

    +
  6. +
  7. +

    + Build the LocalServer. +

    +
  8. +
  9. +

    + Set a reference to the local + server project in each of the driver skeleton + projects. +

    +
  10. +
  11. +

    In each skeleton driver project:

    +
      +
    1. +

      + Do a Find In Files for the + project name of the skeleton driver (e.g., TelescopeDriver) and + change it to match the project name of your local server (e.g. + SuperScope). You don't have to do this in the ReadMe.html file. + Everywhere else, however, is IMPORTANT. This sets the correct + namespace, progID, etc. If you're a bit more brave, you can use + Replace in Files. + This may not be needed if the correct + namespace and naming conventions have been followed when the + drivers and local server were generated. + +

      +
    2. +
    3. +

      + In project properties, + Application tab, change the assembly name to + ASCOM.localserverprojectname.drivertype, + (e.g., ASCOM.SuperScope.Telescope). +

      +
    4. +
    5. +

      + In project properties, + Application tab, click Assembly Information... +

      +
        +
      • +

        + Assure that Make assembly COM + visible is on (it should already be on). +

        +
      • +
      • +

        + Edit the Product Name to be the + "friendly name" of your driver as will be shown in the + Chooser. + Not used now, use the ServedClassName attribute + instead. + +

        +
      • +
      +
    6. +
    7. +

      + In project properties, Build tab, + turn off Register for COM Interop. +

      +
    8. +
    9. +

      + Modify the driver class declaration to inherit from + ReferenceCountedObjectBase. Examples:
      C#: +

      +
      +              public class Telescope :
      +              ReferenceCountedObjectBase,
      +              ITelescope
      +            
      +

      + VB: +

      +
      +              Public Class Telescope
      +              '==================================
      +              Inherits ReferenceCountedObjectBase
      +              Implements ITelescope
      +              '==================================
      +            
      +
    10. +
    11. +

      + In driver.cs/driver.vb, remove + the entire ASCOM Registration region +

      +
    12. +
    13. +

      + In driver.cs/driver.vb, remove + the private strings for driver ID and driver description. + They + may be needed internally, and if so should be set from the + associated attributes, ServedClassName for the description and + ProgId for the driver Id. + +

      +
    14. +
    15. +

      + Modify the class attributes by + adding the ServedClassName and ProgID attributes. The + ServedClassName attribute must be the friendly name shown as the + device name in the Chooser and the ProgId the progid of the driver + e.g. ASCOM.SuperScope.Telescope. The class header should look like + this: +

      +

      C#:

      +
      +              
      +                [Guid("0AE8B38D-10A1-4A8D-A5B7-1B050F74B48B")]  // set by the template
      +                [ProgId("ASCOM.SuperScope.Telescope")]
      +                [ServedClassName ("Super Scope Telescope")]
      +                [ClassInterface(ClassInterfaceType.None)]
      +                public class Telescope : ReferenceCountedObjectBase , ITelescope
      +              
      +            
      +
    16. +
    +
  12. +
+

+
+

+
    +
      +

      VB:

      +
      +          
      +            <Guid(“0AE8B38D-10A1-4A8D-A5B7-1B050F74B48B”)>
      +            <ProgId(“ASCOM.SuperScope.Telescope”)>
      +            <ServedClassName(“Super Scope Telescope”)>
      +            Public Class Telescope
      +            '==================================
      +            Inherits ReferenceCountedObjectBase
      +            Implements ITelescope
      +            '==================================
      +          
      +        
      +
    +
+

+ Add the following line to the driver + constructor, this sets the driver ID using the ProgId Attribute: +

+
    +
      +

      C#

      +
      +          
      +            s_csDriverID = Marshal.GenerateProgIdForType(this.GetType());
      +          
      +        
      +

      VB:

      +
      +          
      +            s_csDriverID = Marshal.GenerateProgIdForType(Me.GetType())
      +          
      +        
      +
    +
  1. +

    + Unless you're writing a + single-driver hub, you will have two or more driver types (e.g. + Telescope and Focuser) and thus two or more driver assembly projects + added. Presumably, these drivers need to share some resources (e.g. + a single COM port via Helper.Serial). + Put shared resources into + the SharedResources class provided + . There are some examples that + should give a clue, modify and delete these as required. +

    +
  2. +
  3. +

    + A shared serial port is already + provided (see SharedResources.cs) as SharedResources.SharedSerial + and it is an ASCOM Helper Serial object. You may wish to define + additional shared resources in static member variables with public + static accessor properties as is already done for SharedSerial. + Unfortunately, if you are a Visual Basic programmer, you will have + to make these additions in C#. +

    +
  4. +
  5. +

    + If you are writing a hub and don't + need the serial port, in SharedResources.cs you can remove the + public static SharedSerial property, the m_SharedSerial member in + the private data region, and the line in main that initializes it. + If you don't need any other shared resources for your hub, then you + can remove the SharedResources.cs file completely. +

    +
  6. +
  7. +

    + If you modified the LocalServer, + build it again now. This will refresh the stuff that's visible to + the drivers. +

    +
  8. +
  9. +

    + Build the driver skeletons to + verify that you got all of the namespace and other variable changes. +

    +
  10. +
  11. +

    + The local server dynamically loads the driver assemblies from + the same folder as the local server executable.
    +
    During + development, you'll need to add a post-build task to each of your + driver assembly projects which puts a copy of the driver assembly + into the local server executable folder. Here is an example: +

    +
        copy "$(TargetPath)" "$(SolutionDir)\SuperScope\$(OutDir)\$(TargetFileName)"
    +

    + This assumes that the server project is called “SuperScope”, + and handles using the debug or release build.
    + Note the quotes for + possible path elements with spaces in them. + An alternative is to + set the build path to the required destination instead of the + default path. + +

    +
  12. +
  13. +

    + + Make sure the drivers are + registered through the local server by running it with the /register + parameter, see below for details. + +

    +
  14. +
  15. +

    + IMPORTANT: + With a local server based driver (or hub) it is possible for + multiple clients to control the device(s). It is up to you to + safeguard against abuse. + The sort of thing that's needed is to + have a counter of the number of connections to a device, the + connection is only fully broken when the number of connections is + zero. You may also need code to prevent several drivers from talking + to the hardware at the same time, the lock pattern is useful for + that. + +

    +
  16. +
  17. +

    + You may want to add controls and/or status information to the + main form frmMain of the local server. Please resist the temptation + to turn the local server's main form into a graphical device control + panel. Instead, make a separate application that uses the served + driver(s). A driver is not a program! +

    +
  18. +
+

Notes

+
    +
  • +

    + The local server handles all of + the registration and unregistration for each of its served driver + classes, including the ASCOM Chooser info and the DCOM/AppID info + needed for activation from TheSky. By running the server from a + command line and giving /register or /unregister as the command line + option, it will register or unregister all served classes + (respectively). + Never use REGASM + on the local server executable! + + This can be done in the + Visual Studio IDE by setting the server project to run as startup + and setting the command line argument to /register in Debug – + Start Options. + +

    +
  • +
  • +

    + When you make the installer for + your local server based driver/hub, do not let it register the + executable for COM. Instead, have it activate the installed local + server with the /register option. +

    +
  • +
  • +

    + The ASCOM registration uses the ServedClassName attribute as + the friendly name that will show in the chooser and the ProgId + attribute as the driver Id. +

    +
  • +
  • +

    + The best deployment way is to install all the files in a + folder that's a sub folder of the main driver, so the SuperScope + driver files will be in the folder ...\ASCOM\Telescope\SuperScope. + This can be done in the Inno script by changing the DefaultDirName + like this:
    + DefaultDirName="{cf}\ASCOM\Telescope\SuperScope"
    then + the files can all be installed with DestDir: {app}; +

    +
  • +
+

+ Theory of Operation +

+

+ The local server is an executable which can provide multiple + instances of multiple drivers to multiple clients. This capability is + needed for two applications: +

+
    +
  • +

    + A hub, which allows multiple + clients to share a single device +

    +
  • +
  • +

    + A device which provides multiple services, such as a + telescope which has a focuser built-in where both the telescope and + focuser are controlled by the same serial connection and different + client programs need to control to the focuser and telescope. +

    +
  • +
+

+ By simply dropping suitably developed driver assemblies into the + same folder as the local server executable, the local server will + find them and register them for COM and ASCOM and serve any number of + instances of the drivers' interfaces to any number of client + programs. It does this by locating and loading the driver assemblies, + analysing them to detect their classes and interfaces, and + implementing a class factory that can create instances of them for + clients. +

+

+ A driver is an assembly which contains a class that implements + one of the ASCOM standard driver interfaces and inherits the + ReferenceCountedObjectBase class of the local server. Apart from + that, driver assemblies are identical to those that are used + in-process (DLL-type). The instructions above detail the steps needed + to convert an in-process driver into one that can be served by the + local server. +

+

+ The name of the local server is important, so we provide it as a + template from which you can create a local server for your + produce. To make this clear, let's assume that your company AlphaTech + produces a telescope system which contains a microcontroller that is + able to control not only the telescope mount, but also a focuser and + a camera rotator. The mount, focuser, and rotator are all controlled + via commands sent through a common serial line connecting the + computer to the microcontroller, so you need a local server. In + ASCOM, then, you probably want your system to appear as + AlphaTech.Telescope, AlphaTech.Focuser, and AlphaTech.Rotator. Then + you would name the local server AlphaTech. Be sure to give this due + consideration before creating the template, the project name is the + name of your local server. + Is this still correct? I get the + impression that ASCOM.AlphaTech.Server would be OK. + +

+

+ The fact that driver classes inherit from the local server's + ReferenceCountedObjectBase class allows the local server to maintain + a reference count on the driver class. If a client creates an + instance of a served driver, the local server automatically starts up + and provides an instance of the class to the client. Once started the + local server can provide additional instances of any of its served + driver classes. If the reference count of all served classes drops to + zero as a result of clients releasing their instances, the local + server will automatically exit. +

+

+ Registration services provided include not only the basic COM + class registration, but also DCOM/AppID info needed to use the served + classes from outbound connections from Software Bisque's TheSky. It + also registers the served classes for the ASCOM Chooser. The + "friendly" name of each served driver that appears in the + chooser comes from the driver's ServedClassName attribute. This also + used to identify a driver so that non driver dlls, such as Interop + dlls can be ignored. The COM ProgID for each served driver is + specified in the ProgId attribute - ASCOM.localservername.drivertype, + for example, ASCOM.AlphaTech.Telescope, where AlphaTech is the local + server name and Telescope is the type of the driver. Unregistering + removes all of this information from the system. Specifying the + ProgId as an attribute allows multiple driver assemblies to be + generated using the same source and namespace. This is used to + provide multiple instances of the same driver, each with a different + ProgId and so able to be registered separately. +

+

+ Driver DLLs are identified for registering/unregistering because + they contain a type with the ServedClassName attribute. Only these + will be registered for Com and ASCOM. This has changed; in Platform + 5 there was no attribute and the local server attempted to register + all dlls. The new behaviour allows support dlls such as interop dlls + to be included without them being registered incorrectly. There was + also an interim version where the ServedClassName attribute was on + the assembly, not the class. + All these previous versions, and the + new drivers will operate together with Platform 6, the changes are + local to the individual drivers. + +

+

+ Detailed Use and Deployment +

+

+ Once you have built your local server and the served driver class + assemblies, here's how to use it. To register the served classes, + activate the local server from a shell command line with the option + /register (or /regserver, for VB6 compatibility): +

+
+      C:\xxx> localserver.exe /register
+    
+

+ To unregister the local server and its drivers, activate the local + server from a shell command line with the option /unregister (or + /unregserver for VB6 compatibility): +

+
+      C:\xxx> localserver.exe /unregister
+    
+

+ When the operating system starts the local server in response to a + client creating one of it's served driver classes, the command option + /embedding is included. The local server's code detects this and sets + a variable that you can use. +

+

+ When deploying a hub or set of drivers + with the local server, you'll have to arrange for the local server + and the driver assemblies to be placed together in a folder in the + ASCOM driver folder. Any support files, such as Interop DLLs can be + put in the same fiolder. That's all you need to do, the local server + will find them in the same folder as it is located in. +

+
+ + + + + + + +
+ + + + + +
+

ASCOM Initiative

+
+ +
+

+
+
+ +

+
+

+ The ASCOM Initiative consists of a group of astronomy software + developers and instrument vendors whose goals are to promote the + driver/client model and scripting automation. +

+

+ See the + ASCOM + web site + for more information. Please participate in the + + ASCOM-Talk + Group + . +

+
+
+

+
+
+ +

+

+
+
+ +

+ + \ No newline at end of file diff --git a/LynxAstro.DewController/ReferenceCountedObject.cs b/LynxAstro.DewController/ReferenceCountedObject.cs new file mode 100644 index 0000000..fe86948 --- /dev/null +++ b/LynxAstro.DewController/ReferenceCountedObject.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace ASCOM.LynxAstro.DewController +{ + [ComVisible(false)] + public class ReferenceCountedObjectBase + { + public ReferenceCountedObjectBase() + { + // We increment the global count of objects. + Server.CountObject(); + } + + ~ReferenceCountedObjectBase() + { + // We decrement the global count of objects. + Server.UncountObject(); + // We then immediately test to see if we the conditions + // are right to attempt to terminate this server application. + Server.ExitIf(); + } + } +} diff --git a/LynxAstro.DewController/Resources/ASCOM.png b/LynxAstro.DewController/Resources/ASCOM.png new file mode 100644 index 0000000000000000000000000000000000000000..a83b77ba9c970d59c584bfa93751f6e4fc6962d2 GIT binary patch literal 1922 zcmV-|2YvX7P)Fw|HpR2&X$JNc#-rwWu-s0&2B3J<;R{|qg10-1kBUS_? zRs|(m1}0kvCtL_9TnZ{)4J=>|Enp8WVGu835invBF=G=lWEC@I7BpoUH)kC>Y8^Ug zA3SRyJ!~UCZYV@?DMfNDM|3SnbudeJF-&(eO?fp>dN)yfIa7T-R)0WQfkj<}M_-0W zV1`R#h)`#YRBDb{ZjxSem0)$2Vs@BneVuH7o^61iaD$g=%-Gc0WsI^Z5Dt_V@T1Hc%NgP8&B)o~pFJ$ITr% zO&>Z;Vs(FGb$um2N+v)`Dnm&vMo2D4NBR2tFGxo*Nk%eCMzy}YHB3e1=jJ|COgK+P z?(pw9P((XXLp)MLJyS$KRYE~lL|AH0OI|)rU_Ew)Xi#H3z{j{zV>?r2J5**oS!y|2 zYdG@r@Lg>;Tx>XFb2hEDmVSy=YIrkgcQfVZ-TVCa^7HO;elS~QDSCx2c7iX5kxYGu zEy~WRgpDd^Z6nOksOIRfktu9(AZu_Tl9@8+>CBv=BbuKhmz^V&nk8~|9`W+q ztgaoWsvdWG8nU!2$jMsS+M&e46u!L|xw#m_!5Xx+8nd(-#ls%2up8IcciGv3`}^+y z|MvU)_Wk|&|Ns5_`}zO>{^Z;V;o1w|*bCd#4%N^O(#;Rc#}dcH6W7uh*wsPy@{Q~1 zlKT19|MCF-@c{ep0sZg-`tAbx?E?1e1N7+x^XLTd<^}KN1?}So>*5FL;0Wj52=wU* z{qYX`@*My4EdBE``SC&U>vH+^mHPL&|NP7O_~8Hk_4@kyR27Zh00007bV*G`2iFA? z4GaTx$9&TO00b&YL_t(|oMSk|L>$-%0s9!X7qAco(!d~l6$4WW13KUr5fc*@zz_xj zK4u0c7Bn$l5m_~DZ8cdDUMw0|v1kxh(6@4OveH)&CQbvdq^_M`Y^SH$8CEE*lbX;%=mk8IZw2u5^*;0%Nj2Fz5j8g zv!N_;8W==Xj-0Rfwa8RO6jMD`4g6hSrx_o4pe)W$qy~mvw<2XHf0=|7sJy)Vyu3Iy zRKHon%Nu@cH$u69xU`&%xG*mc4eQ>OfynoHFaZ&H4MS5SEk!Y2tQt~p>;+MEFBd`u z1mq2DT)o_EjTD8kYMB451x%kh26d2_x{YsaT#&!Ljs!0@4fjq#=vmL&AR43%U1JkV z*ttAR<@vE_nD}%G)M?kZfN6d?lc;6?t`)QSSpX9kriO!egrKagpR&L<$V_|m=R$LQ zu(z2477g_;m%^BhFXn)#?la%cwR5or$2jUqVe!M3>$!0DiQ^0m9S5GBoNQ$8>F4ce zsw9dTN>y*yAUIQAFFgC?SRaF!qK>JBnVu3Tt)OdI{k9yzX?XYK_H>XhL?q-D6{JK# zX$wt5^36SPcJ78(FF%$+*u4A#0+6JLreW^0Rwyfc$%mJ#dtWVJKm%wRj-7zed^2x7 z*(-*@C8 zN%IPvISk8ds2V^?c*Va9e~#A3BWHaP`NcnKp?M8O1274D#a+5~xf7g(;Q;8+!iVdj zc@5PD86&r#qW{n&40jO7p*tUkLh>4_260VWe|QpxYXCX4=ifX?UPBEfVMU}Q3$H|z$Aq}P(1GE_!5*QsA7}!AsTmwe}qbdV~HKP%O zJEN* zwr1cA0ci*UYH$hRU^GY4pv@S<$(azq;0{zN&FJC)@{bE6(1qF{IR~HycSc5OBn=!1 z3?L7v0u3>8XEc%y;dbF*bZ3wbNpN>h2$2SAFlP*5K+@pi0CEGTDua0dCx;P`>CVY% z4H9zTb%7 literal 0 HcmV?d00001 diff --git a/LynxAstro.DewController/SetupDialogForm.cs b/LynxAstro.DewController/SetupDialogForm.cs new file mode 100644 index 0000000..4e8d169 --- /dev/null +++ b/LynxAstro.DewController/SetupDialogForm.cs @@ -0,0 +1,89 @@ +using System; +using System.IO.Ports; +using System.Linq; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using ASCOM.LynxAstro.DewController.Properties; + +namespace ASCOM.LynxAstro.DewController +{ + + [ComVisible(false)] // Form not registered for COM! + public partial class SetupDialogForm : Form + { + public SetupDialogForm() + { + InitializeComponent(); + + var assemblyInfo = new AssemblyInfo(); + + Text = string.Format(Resources.SetupDialogForm_SetupDialogForm__0__Settings___1__, assemblyInfo.Product, assemblyInfo.AssemblyVersion); + } + + private void cmdOK_Click(object sender, EventArgs e) // OK button event handler + { + + } + + private void cmdCancel_Click(object sender, EventArgs e) // Cancel button event handler + { + Close(); + } + + private void BrowseToAscom(object sender, EventArgs e) // Click on ASCOM logo event handler + { + try + { + System.Diagnostics.Process.Start("https://ascom-standards.org/"); + } + catch (System.ComponentModel.Win32Exception noBrowser) + { + if (noBrowser.ErrorCode == -2147467259) + MessageBox.Show(noBrowser.Message); + } + catch (System.Exception other) + { + MessageBox.Show(other.Message); + } + } + + public void SetProfile(ProfileProperties profileProperties) + { + chkTrace.Checked = profileProperties.TraceLogger; + // set the list of com ports to those that are currently available + comboBoxComPort.Items.Clear(); + comboBoxComPort.Items.AddRange(SerialPort.GetPortNames().ToArray()); // use System.IO because it's static + // select the current port if possible + if (comboBoxComPort.Items.Contains(profileProperties.ComPort)) + { + comboBoxComPort.SelectedItem = profileProperties.ComPort; + } + } + + public ProfileProperties GetProfile() + { + var profileProperties = new ProfileProperties + { + TraceLogger = chkTrace.Checked, + ComPort = comboBoxComPort.SelectedItem.ToString(), + }; + + return profileProperties; + } + + public void SetReadOnlyMode() + { + foreach (Control control in Controls) + { + control.Enabled = false; + } + + cmdCancel.Enabled = true; + //cmdOK.Enabled = false; + //comboBoxComPort.Enabled = false; + //chkTrace.Enabled = false; + //txtGuideRate.Enabled = false; + //cboPrecision.Enabled = false; + } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/SetupDialogForm.designer.cs b/LynxAstro.DewController/SetupDialogForm.designer.cs new file mode 100644 index 0000000..3d4fa24 --- /dev/null +++ b/LynxAstro.DewController/SetupDialogForm.designer.cs @@ -0,0 +1,149 @@ +namespace ASCOM.LynxAstro.DewController +{ + partial class SetupDialogForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.cmdOK = new System.Windows.Forms.Button(); + this.cmdCancel = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.picASCOM = new System.Windows.Forms.PictureBox(); + this.label2 = new System.Windows.Forms.Label(); + this.chkTrace = new System.Windows.Forms.CheckBox(); + this.comboBoxComPort = new System.Windows.Forms.ComboBox(); + ((System.ComponentModel.ISupportInitialize)(this.picASCOM)).BeginInit(); + this.SuspendLayout(); + // + // cmdOK + // + this.cmdOK.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.cmdOK.DialogResult = System.Windows.Forms.DialogResult.OK; + this.cmdOK.Location = new System.Drawing.Point(281, 112); + this.cmdOK.Name = "cmdOK"; + this.cmdOK.Size = new System.Drawing.Size(59, 24); + this.cmdOK.TabIndex = 0; + this.cmdOK.Text = "OK"; + this.cmdOK.UseVisualStyleBackColor = true; + this.cmdOK.Click += new System.EventHandler(this.cmdOK_Click); + // + // cmdCancel + // + this.cmdCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.cmdCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.cmdCancel.Location = new System.Drawing.Point(281, 142); + this.cmdCancel.Name = "cmdCancel"; + this.cmdCancel.Size = new System.Drawing.Size(59, 25); + this.cmdCancel.TabIndex = 1; + this.cmdCancel.Text = "Cancel"; + this.cmdCancel.UseVisualStyleBackColor = true; + this.cmdCancel.Click += new System.EventHandler(this.cmdCancel_Click); + // + // label1 + // + this.label1.Location = new System.Drawing.Point(12, 9); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(123, 31); + this.label1.TabIndex = 2; + this.label1.Text = "Construct your driver\'s setup dialog here."; + // + // picASCOM + // + this.picASCOM.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.picASCOM.Cursor = System.Windows.Forms.Cursors.Hand; + this.picASCOM.Image = global::ASCOM.LynxAstro.DewController.Properties.Resources.ASCOM; + this.picASCOM.Location = new System.Drawing.Point(292, 9); + this.picASCOM.Name = "picASCOM"; + this.picASCOM.Size = new System.Drawing.Size(48, 56); + this.picASCOM.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize; + this.picASCOM.TabIndex = 3; + this.picASCOM.TabStop = false; + this.picASCOM.Click += new System.EventHandler(this.BrowseToAscom); + this.picASCOM.DoubleClick += new System.EventHandler(this.BrowseToAscom); + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(13, 90); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(58, 13); + this.label2.TabIndex = 5; + this.label2.Text = "Comm Port"; + // + // chkTrace + // + this.chkTrace.AutoSize = true; + this.chkTrace.Location = new System.Drawing.Point(77, 118); + this.chkTrace.Name = "chkTrace"; + this.chkTrace.Size = new System.Drawing.Size(69, 17); + this.chkTrace.TabIndex = 6; + this.chkTrace.Text = "Trace on"; + this.chkTrace.UseVisualStyleBackColor = true; + // + // comboBoxComPort + // + this.comboBoxComPort.FormattingEnabled = true; + this.comboBoxComPort.Location = new System.Drawing.Point(77, 87); + this.comboBoxComPort.Name = "comboBoxComPort"; + this.comboBoxComPort.Size = new System.Drawing.Size(90, 21); + this.comboBoxComPort.TabIndex = 7; + // + // SetupDialogForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(350, 175); + this.Controls.Add(this.comboBoxComPort); + this.Controls.Add(this.chkTrace); + this.Controls.Add(this.label2); + this.Controls.Add(this.picASCOM); + this.Controls.Add(this.label1); + this.Controls.Add(this.cmdCancel); + this.Controls.Add(this.cmdOK); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "SetupDialogForm"; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "LynxAstro.DewController Setup"; + ((System.ComponentModel.ISupportInitialize)(this.picASCOM)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button cmdOK; + private System.Windows.Forms.Button cmdCancel; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.PictureBox picASCOM; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.CheckBox chkTrace; + private System.Windows.Forms.ComboBox comboBoxComPort; + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/SetupDialogForm.resx b/LynxAstro.DewController/SetupDialogForm.resx new file mode 100644 index 0000000..d58980a --- /dev/null +++ b/LynxAstro.DewController/SetupDialogForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/LynxAstro.DewController/SharedResources.cs b/LynxAstro.DewController/SharedResources.cs new file mode 100644 index 0000000..428898f --- /dev/null +++ b/LynxAstro.DewController/SharedResources.cs @@ -0,0 +1,379 @@ +// +// ================ +// Shared Resources +// ================ +// +// This class is a container for all shared resources that may be needed +// by the drivers served by the Local Server. +// +// NOTES: +// +// * ALL DECLARATIONS MUST BE STATIC HERE!! INSTANCES OF THIS CLASS MUST NEVER BE CREATED! +// +// Written by: Bob Denny 29-May-2007 +// Modified by Chris Rowland and Peter Simpson to hamdle multiple hardware devices March 2011 +// + +using System; +using System.Collections.Generic; +using System.Windows.Forms; +using ASCOM.Utilities; +using ASCOM.Utilities.Interfaces; +using JetBrains.Annotations; + +namespace ASCOM.LynxAstro.DewController +{ + /// + /// The resources shared by all drivers and devices, in this example it's a serial port with a shared SendMessage method + /// an idea for locking the message and handling connecting is given. + /// In reality extensive changes will probably be needed. + /// Multiple drivers means that several applications connect to the same hardware device, aka a hub. + /// Multiple devices means that there are more than one instance of the hardware, such as two focusers. + /// In this case there needs to be multiple instances of the hardware connector, each with it's own connection count. + /// + public static class SharedResources + { + // object used for locking to prevent multiple drivers accessing common code at the same time + private static readonly object LockObject = new object(); + + // Shared serial port. This will allow multiple drivers to use one single serial port. + private static ISerial _sSharedSerial; // Shared serial port + + // + // Public access to shared resources + // + + #region single serial port connector + + // + // this region shows a way that a single serial port could be connected to by multiple + // drivers. + // + // Connected is used to handle the connections to the port. + // + // SendMessage is a way that messages could be sent to the hardware without + // conflicts between different drivers. + // + // All this is for a single connection, multiple connections would need multiple ports + // and a way to handle connecting and disconnection from them - see the + // multi driver handling section for ideas. + // + + /// + /// Shared serial port. Do not directly access this method. + /// + public static ISerial SharedSerial + { + get => _sSharedSerial ?? (_sSharedSerial = new Serial()); + set => _sSharedSerial = value; + } + + public static IProfileFactory ProfileFactory + { + get => _profileFactory ?? (_profileFactory = new ProfileFactory()); + set => _profileFactory = value; + } + + //todo add code to ensure that there is a minimum gap between commands. 5ms as default. + public static void SendBlind(string message) + { + lock (LockObject) + { + SharedSerial.ClearBuffers(); + SharedSerial.Transmit(message); + } + } + + /// + /// Example of a shared SendMessage method, the lock + /// prevents different drivers tripping over one another. + /// It needs error handling and assumes that the message will be sent unchanged + /// and that the reply will always be terminated by a "#" character. + /// + /// + /// + public static string SendString(string message) + { + lock (LockObject) + { + SharedSerial.ClearBuffers(); + SharedSerial.Transmit(message); + return SharedSerial.ReceiveTerminated("#").TrimEnd('#'); + } + } + + public static string SendChar(string message) + { + lock (LockObject) + { + SharedSerial.ClearBuffers(); + SharedSerial.Transmit(message); + return SharedSerial.ReceiveCounted(1); + } + } + + public static string ReadTerminated() + { + lock (LockObject) + { + return SharedSerial.ReceiveTerminated("#"); + } + } + + public static void ReadCharacters(int throwAwayCharacters) + { + lock (LockObject) + { + SharedSerial.ReceiveCounted(throwAwayCharacters); + } + } + + #endregion + + #region Profile + + private const string DriverId = "ASCOM.MeadeGeneric.Telescope"; + + // Constants used for Profile persistence + private const string ComPortProfileName = "COM Port"; + private const string TraceStateProfileName = "Trace Level"; + private const string SwitchNameProfileName = "SwitchName_{0}"; + + public static void WriteProfile(ProfileProperties profileProperties) + { + lock (LockObject) + { + using (IProfileWrapper driverProfile = ProfileFactory.Create()) + { + driverProfile.DeviceType = "Telescope"; + driverProfile.WriteValue(DriverId, TraceStateProfileName, profileProperties.TraceLogger.ToString()); + driverProfile.WriteValue(DriverId, ComPortProfileName, profileProperties.ComPort); + } + } + } + + private const string ComPortDefault = "COM1"; + private const string TraceStateDefault = "false"; + + private const string noSwitchValue = "no_switch_value"; + + public static ProfileProperties ReadProfile() + { + lock (LockObject) + { + ProfileProperties profileProperties = new ProfileProperties(); + using (IProfileWrapper driverProfile = ProfileFactory.Create()) + { + driverProfile.DeviceType = "Telescope"; + profileProperties.ComPort = driverProfile.GetValue(DriverId, ComPortProfileName, string.Empty, ComPortDefault); + profileProperties.TraceLogger = Convert.ToBoolean(driverProfile.GetValue(DriverId, TraceStateProfileName, string.Empty, TraceStateDefault)); + + var switchNo = 0; + var finished = false; + profileProperties.SwitchNames.Clear(); + do + { + var switchName = string.Format(SwitchNameProfileName, switchNo); + var switchValue = driverProfile.GetValue(DriverId, switchName, string.Empty, noSwitchValue); + + finished = switchValue == noSwitchValue; + + if (!finished) + profileProperties.SwitchNames.Add(switchValue); + + } while (!finished); + + } + + return profileProperties; + } + } + + #endregion + + #region SetupDialog + + public static void SetupDialog() + { + var profileProperties = ReadProfile(); + + using (SetupDialogForm f = new SetupDialogForm()) + { + f.SetProfile(profileProperties); + + if (IsConnected()) + { + f.SetReadOnlyMode(); + } + + var result = f.ShowDialog(); + if (result == DialogResult.OK) + { + profileProperties = f.GetProfile(); + + WriteProfile(profileProperties); // Persist device configuration values to the ASCOM Profile store + } + } + } + + #endregion + + #region Multi Driver handling + + public static string FirmwareVersion { get; private set; } = string.Empty; + + // this section illustrates how multiple drivers could be handled, + // it's for drivers where multiple connections to the hardware can be made and ensures that the + // hardware is only disconnected from when all the connected devices have disconnected. + + // It is NOT a complete solution! This is to give ideas of what can - or should be done. + // + // An alternative would be to move the hardware control here, handle connecting and disconnecting, + // and provide the device with a suitable connection to the hardware. + // + /// + /// dictionary carrying device connections. + /// The Key is the connection number that identifies the device, it could be the COM port name, + /// USB ID or IP Address, the Value is the DeviceHardware class + /// + private static readonly Dictionary ConnectedDevices = new Dictionary(); + + private static readonly Dictionary ConnectedDeviceIds = new Dictionary(); + private static IProfileFactory _profileFactory; + + + /// + /// This is called in the driver Connect(true) property, + /// it add the device id to the list of devices if it's not there and increments the device count. + /// + /// + /// + /// + public static ConnectionInfo Connect(string deviceId, string driverId, ITraceLogger traceLogger) + { + lock (LockObject) + { + if (!ConnectedDevices.ContainsKey(deviceId)) + ConnectedDevices.Add(deviceId, new DeviceHardware()); + + if (!ConnectedDeviceIds.ContainsKey(driverId)) + ConnectedDeviceIds.Add(driverId, new DeviceHardware()); + + if (deviceId == "Serial") + { + if (ConnectedDevices[deviceId].Count == 0) + { + var profileProperties = ReadProfile(); + SharedSerial.PortName = profileProperties.ComPort; + //SharedSerial.DTREnable = profileProperties.RtsDtrEnabled; + //SharedSerial.RTSEnable = profileProperties.RtsDtrEnabled; + SharedSerial.DataBits = 8; + SharedSerial.StopBits = SerialStopBits.One; + SharedSerial.Parity = SerialParity.None; + SharedSerial.Speed = SerialSpeed.ps9600; + SharedSerial.Handshake = SerialHandshake.None; + SharedSerial.Connected = true; + } + + try + { + var encodedString = SendString(":GV#"); + FirmwareVersion = DecodeResult(":GV", encodedString ); + //Command: :GV# + //Purpose: Get the devices firmware version. + //Response: :GVXXXXXXXX# where X is a version string, e.g. 1.0. + } + catch (Exception ex) + { + traceLogger.LogIssue("Connect", $"Error getting telescope information \"{ex.Message}\" setting to LX200 Classic mode."); + FirmwareVersion = "Unknown"; + } + } + else + throw new ArgumentException($"deviceId {deviceId} not currently supported"); + + ConnectedDevices[deviceId].Count++; // increment the value + ConnectedDeviceIds[driverId].Count++; // increment the value + + return new ConnectionInfo + { + //Connections = ConnectedDevices[deviceId].Count, + SameDevice = ConnectedDeviceIds[driverId].Count + }; + } + } + + private static string DecodeResult(string pattern, string encodedString) + { + var decodedString = encodedString.Substring( pattern.Length ).TrimEnd('#'); + return decodedString; + } + + public static void Disconnect(string deviceId, string driverId) + { + lock (LockObject) + { + if (ConnectedDevices.ContainsKey(deviceId)) + { + ConnectedDevices[deviceId].Count--; + if (ConnectedDevices[deviceId].Count <= 0) + { + ConnectedDevices.Remove(deviceId); + if (deviceId == "Serial") + { + SharedSerial.Connected = false; + } + } + } + + if (ConnectedDeviceIds.ContainsKey(driverId)) + { + ConnectedDeviceIds[driverId].Count--; + } + } + } + + private static bool IsConnected() + { + foreach (var device in ConnectedDevices) + { + if (device.Value.Count > 0) + return true; + } + + return false; + } + + #endregion + + public static void Lock(Action action) + { + lock (LockObject) + { + action(); + } + } + + public static T Lock(Func func) + { + lock (LockObject) + { + return func(); + } + } + + /// + /// Skeleton of a hardware class, all this does is hold a count of the connections, + /// in reality extra code will be needed to handle the hardware in some way + /// + private class DeviceHardware + { + internal int Count { set; get; } + + internal DeviceHardware() + { + Count = 0; + } + } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/SharedResourcesWrapper.cs b/LynxAstro.DewController/SharedResourcesWrapper.cs new file mode 100644 index 0000000..b4da760 --- /dev/null +++ b/LynxAstro.DewController/SharedResourcesWrapper.cs @@ -0,0 +1,70 @@ +using System; +using ASCOM.Utilities.Interfaces; + +namespace ASCOM.LynxAstro.DewController +{ + public class SharedResourcesWrapper : ISharedResourcesWrapper + { + public ConnectionInfo Connect(string deviceId, string driverId, ITraceLogger traceLogger) + { + return SharedResources.Connect(deviceId, driverId, traceLogger); + } + + public void Disconnect(string deviceId, string driverId) + { + SharedResources.Disconnect(deviceId, driverId); + } + + public string FirmwareVersion => SharedResources.FirmwareVersion; + + public void Lock(Action action) + { + SharedResources.Lock(action); + } + + public T Lock(Func func) + { + return SharedResources.Lock(func); + } + + public string SendString(string message) + { + return SharedResources.SendString(message); + } + + public void SendBlind(string message) + { + SharedResources.SendBlind(message); + } + + public string SendChar(string message) + { + return SharedResources.SendChar(message); + } + + public string ReadTerminated() + { + return SharedResources.ReadTerminated(); + } + + public void ReadCharacters(int throwAwayCharacters) + { + SharedResources.ReadCharacters(throwAwayCharacters); + } + + public ProfileProperties ReadProfile() + { + return SharedResources.ReadProfile(); + } + + public void SetupDialog() + { + SharedResources.SetupDialog(); + } + + public void WriteProfile(ProfileProperties profileProperties) + { + SharedResources.WriteProfile(profileProperties); + } + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/frmMain.Designer.cs b/LynxAstro.DewController/frmMain.Designer.cs new file mode 100644 index 0000000..b9d0061 --- /dev/null +++ b/LynxAstro.DewController/frmMain.Designer.cs @@ -0,0 +1,63 @@ +using System; + +namespace ASCOM.LynxAstro.DewController +{ + partial class frmMain + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.label1 = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // label1 + // + this.label1.Location = new System.Drawing.Point(12, 10); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(199, 33); + this.label1.TabIndex = 0; + this.label1.Text = "This is an ASCOM driver, not a program for you to use."; + // + // frmMain + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(233, 52); + this.Controls.Add(this.label1); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Name = "frmMain"; + this.Text = "LynxAstro.DewController Driver Server"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label label1; + + } +} + diff --git a/LynxAstro.DewController/frmMain.cs b/LynxAstro.DewController/frmMain.cs new file mode 100644 index 0000000..aee7282 --- /dev/null +++ b/LynxAstro.DewController/frmMain.cs @@ -0,0 +1,15 @@ +using System.Windows.Forms; + +namespace ASCOM.LynxAstro.DewController +{ + public partial class frmMain : Form + { + delegate void SetTextCallback(string text); + + public frmMain() + { + InitializeComponent(); + } + + } +} \ No newline at end of file diff --git a/LynxAstro.DewController/frmMain.resx b/LynxAstro.DewController/frmMain.resx new file mode 100644 index 0000000..19dc0dd --- /dev/null +++ b/LynxAstro.DewController/frmMain.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/LynxAstro.DewController/packages.config b/LynxAstro.DewController/packages.config new file mode 100644 index 0000000..a95f5c2 --- /dev/null +++ b/LynxAstro.DewController/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file From 57b99ce84e8cc5de36455bd808328000e1286706 Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 17 Feb 2021 20:45:17 +0000 Subject: [PATCH 2/4] Added missing build.build file and fixed a couple of broken unit tests. --- .../SwitchUnitTests.cs | 17 +++++-- .../SharedResourcesUnitTests.cs | 26 +++------- LynxAstro.DewController/SharedResources.cs | 7 +-- build.build | 48 +++++++++++++++++++ 4 files changed, 73 insertions(+), 25 deletions(-) create mode 100644 build.build diff --git a/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs b/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs index d3aa19a..12f4d80 100644 --- a/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs +++ b/LynxAstro.DewController.Switch.UnitTests/SwitchUnitTests.cs @@ -1,4 +1,5 @@ -using ASCOM.LynxAstro.DewController; +using System.Collections.Generic; +using ASCOM.LynxAstro.DewController; using Moq; using NUnit.Framework; @@ -10,12 +11,22 @@ namespace LynxAstro.DewController.Switch.UnitTests private ASCOM.LynxAstro.DewController.Switch _switch; private Mock _sharedResourcesWrapperMock; + private ProfileProperties _profileProperties; + [SetUp] public void Setup() { - _sharedResourcesWrapperMock = new Mock(); + _profileProperties = new ProfileProperties() + { + ComPort = "TestCom1", + SwitchNames = new List(), + TraceLogger = false + }; - _switch = new ASCOM.LynxAstro.DewController.Switch(); + _sharedResourcesWrapperMock = new Mock(); + _sharedResourcesWrapperMock.Setup(x => x.ReadProfile()).Returns(() => _profileProperties); + + _switch = new ASCOM.LynxAstro.DewController.Switch(_sharedResourcesWrapperMock.Object); } [Test] diff --git a/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs b/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs index f0ff61c..db1d8ea 100644 --- a/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs +++ b/LynxAstro.DewController.UnitTests/SharedResourcesUnitTests.cs @@ -1,5 +1,4 @@ using System; -using System.Security.Cryptography.X509Certificates; using ASCOM.LynxAstro.DewController; using ASCOM.Utilities.Interfaces; using Moq; @@ -99,7 +98,7 @@ namespace LynxAstro.DewController.UnitTests [Test] public void WriteProfile_WhenCalled_WritesExpectedProfileSettings() { - string DriverId = "ASCOM.MeadeGeneric.Telescope"; + string DriverId = "ASCOM.LynxAstro.DewController.Switch"; Mock profileWrapperMock = new Mock(); profileWrapperMock.SetupAllProperties(); @@ -119,7 +118,7 @@ namespace LynxAstro.DewController.UnitTests SharedResources.WriteProfile(profileProperties); - Assert.That(profeWrapper.DeviceType, Is.EqualTo("Telescope")); + Assert.That(profeWrapper.DeviceType, Is.EqualTo("Switch")); profileWrapperMock.Verify(x => x.WriteValue(DriverId, "Trace Level", profileProperties.TraceLogger.ToString()), Times.Once); profileWrapperMock.Verify(x => x.WriteValue(DriverId, "COM Port", profileProperties.ComPort), Times.Once); } @@ -127,7 +126,7 @@ namespace LynxAstro.DewController.UnitTests [Test] public void ReadProfile_WhenCalled_ReturnsExpectedDefaultValues() { - string DriverId = "ASCOM.MeadeGeneric.Telescope"; + string DriverId = "ASCOM.LynxAstro.DewController.Switch"; string ComPortDefault = "COM1"; string TraceStateDefault = "false"; @@ -140,25 +139,14 @@ namespace LynxAstro.DewController.UnitTests Mock profileWrapperMock = new Mock(); profileWrapperMock.SetupAllProperties(); + profileWrapperMock.Setup(x => x.DeviceType).Returns("Switch"); + profileWrapperMock.Setup(x => x.GetValue(DriverId, "Trace Level", string.Empty, TraceStateDefault)) .Returns(() => TraceStateDefault); profileWrapperMock.Setup(x => x.GetValue(DriverId, "COM Port", string.Empty, ComPortDefault)) .Returns(ComPortDefault); - profileWrapperMock - .Setup(x => x.GetValue(DriverId, "Guide Rate Arc Seconds Per Second", string.Empty, - GuideRateProfileNameDefault)).Returns(GuideRateProfileNameDefault); - profileWrapperMock.Setup(x => x.GetValue(DriverId, "Precision", string.Empty, PrecisionDefault)) - .Returns(PrecisionDefault); - profileWrapperMock.Setup(x => x.GetValue(DriverId, "Guiding Style", string.Empty, GuidingStyleDefault)) - .Returns(GuidingStyleDefault); - profileWrapperMock.Setup(x => - x.GetValue(DriverId, "Backlash Compensation", string.Empty, BacklashCompensationDefault)) - .Returns(BacklashCompensationDefault); - profileWrapperMock.Setup(x => - x.GetValue(DriverId, "Reverse Focuser Direction", string.Empty, ReverseFocuserDiectionDefault)) - .Returns(() => ReverseFocuserDiectionDefault); - + profileWrapperMock.Setup(x => x.GetValue(DriverId, It.IsRegex("^SwitchName_[0-9{1}]$"), string.Empty, It.IsAny())) .Returns((string driverId, string name, string subKey, string defaultValue) => defaultValue); @@ -172,7 +160,7 @@ namespace LynxAstro.DewController.UnitTests var profileProperties = SharedResources.ReadProfile(); - Assert.That(profeWrapper.DeviceType, Is.EqualTo("Telescope")); + Assert.That(profeWrapper.DeviceType, Is.EqualTo("Switch")); Assert.That(profileProperties.ComPort, Is.EqualTo(ComPortDefault)); Assert.That(profileProperties.TraceLogger, Is.EqualTo(bool.Parse(TraceStateDefault))); } diff --git a/LynxAstro.DewController/SharedResources.cs b/LynxAstro.DewController/SharedResources.cs index 428898f..a94085f 100644 --- a/LynxAstro.DewController/SharedResources.cs +++ b/LynxAstro.DewController/SharedResources.cs @@ -132,7 +132,7 @@ namespace ASCOM.LynxAstro.DewController #region Profile - private const string DriverId = "ASCOM.MeadeGeneric.Telescope"; + private const string DriverId = "ASCOM.LynxAstro.DewController.Switch"; // Constants used for Profile persistence private const string ComPortProfileName = "COM Port"; @@ -145,7 +145,7 @@ namespace ASCOM.LynxAstro.DewController { using (IProfileWrapper driverProfile = ProfileFactory.Create()) { - driverProfile.DeviceType = "Telescope"; + driverProfile.DeviceType = "Switch"; driverProfile.WriteValue(DriverId, TraceStateProfileName, profileProperties.TraceLogger.ToString()); driverProfile.WriteValue(DriverId, ComPortProfileName, profileProperties.ComPort); } @@ -164,7 +164,7 @@ namespace ASCOM.LynxAstro.DewController ProfileProperties profileProperties = new ProfileProperties(); using (IProfileWrapper driverProfile = ProfileFactory.Create()) { - driverProfile.DeviceType = "Telescope"; + driverProfile.DeviceType = "Switch"; profileProperties.ComPort = driverProfile.GetValue(DriverId, ComPortProfileName, string.Empty, ComPortDefault); profileProperties.TraceLogger = Convert.ToBoolean(driverProfile.GetValue(DriverId, TraceStateProfileName, string.Empty, TraceStateDefault)); @@ -181,6 +181,7 @@ namespace ASCOM.LynxAstro.DewController if (!finished) profileProperties.SwitchNames.Add(switchValue); + switchNo++; } while (!finished); } diff --git a/build.build b/build.build new file mode 100644 index 0000000..05f3354 --- /dev/null +++ b/build.build @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 3f5c8d44cbc0cf28ced807a4f8267c99c225174a Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 17 Feb 2021 20:54:13 +0000 Subject: [PATCH 3/4] Added the setup to the build.build file. --- build.build | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.build b/build.build index 05f3354..c70e15a 100644 --- a/build.build +++ b/build.build @@ -33,6 +33,12 @@ + + + + + + From 9652428e895318036ef57788fc08397ae6fc90e3 Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 17 Feb 2021 21:10:54 +0000 Subject: [PATCH 4/4] Modified the solution to build the wix setup in release mode. --- LynxAstro.DewController.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/LynxAstro.DewController.sln b/LynxAstro.DewController.sln index 85c1de8..320cb71 100644 --- a/LynxAstro.DewController.sln +++ b/LynxAstro.DewController.sln @@ -64,6 +64,7 @@ Global {1073A462-D9A4-4C72-9C3F-A345B307153A}.Debug|x86.ActiveCfg = Debug|x86 {1073A462-D9A4-4C72-9C3F-A345B307153A}.Debug|x86.Build.0 = Debug|x86 {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|Any CPU.ActiveCfg = Release|x86 + {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|Any CPU.Build.0 = Release|x86 {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|x86.ActiveCfg = Release|x86 {1073A462-D9A4-4C72-9C3F-A345B307153A}.Release|x86.Build.0 = Release|x86 {D5207217-61C7-4E94-8097-91DBACE57D2A}.Debug|Any CPU.ActiveCfg = Debug|x86