diff --git a/ASCOM.DigitalDomeworks.Dome Registration.reg b/ASCOM.DigitalDomeworks.Dome Registration.reg new file mode 100644 index 0000000..a599a29 Binary files /dev/null and b/ASCOM.DigitalDomeworks.Dome Registration.reg differ diff --git a/Instructions for Beta Testers.md b/Instructions for Beta Testers.md new file mode 100644 index 0000000..2106385 --- /dev/null +++ b/Instructions for Beta Testers.md @@ -0,0 +1,58 @@ +ASCOM Drivers for Digital Domeworks, 2018 Reboot +================================================ + +Instructions for Beta Testers +----------------------------- + +This build is beta quality, which means it most likely contains +bugs, although it shoul dbe mostly feature-complete. You test +this software entirely at your own risk. + +About the 2018 Reboot +--------------------- + +This version (7.0) of the Digital Domeworks ASCOM driver is a +ground-up rewrite, using all the new skills and knowledge we +have acquired since the original version was started almost +12 years ago. + +The 2018 driver is an ASCOM LocalServer, meaning it can accept +multiple concurrent connections from client applications. Two +ASCOM drivers are included: + +1. ASCOM Dome driver, implementing the `IDomeV2` interface. This + driver controls dome operations such as rotation, opening and + closing the shutter. +2. ASCOM Switch driver, implementing the `ISwitchV2` interface. + This driver allows control of the _user output pins_ which + works with the _Technical Innovations Remote Power Module_. + +Drivers appear in the ASCOM Chooser as _Digital Domeworks 2018_. + +This driver uses different identifiers to the original Digital +Domeworks driver so can safely be installed side-by-side with it. +There is no need to uninstall the old driver. + +Installation - Important, Read Carefully +------------ + +**Currently there is no installer, so before attempting to use +the driver you must perform a one-time registration**. + +Simply unzip the package into a folder, which can be in any convenient location such as on the Desktop, and proceed as follows: + +- Open a command prompt such as `Windows PowerShell` or `cmd.exe`. +- Change directory to the folder where you have saved the program files. +- Issue the following command: `TA.DigitalDomeworks.Server.exe /register` +- You should see the security prompt as the program requests + elevated permissions. You must allow this operation. +- You're done. The driver should now appear in the ASCOM Chooser. + +The release version of the driver will have an installer that automates this process, but it is not completed yet. + +Reporting Bugs and Feedback +--------------------------- + +Please report all bugs, issues, feedback and feature requests at the [official public issue tracker][issues]. + +[issues]: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks/issues?status=new&status=open "Public Issue Tracker" \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..1851235 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,120 @@ +# ASCOM drivers for Digital DomeWorks - 2018 Reboot # + +![ASCOM Server Status Display][ddw-status] + +## TL;DR ## + + | + | + | + + +## Overview ## + +In 2006, Tigra Astronomy was commissioned buy Technical Innovations to produce an ASCOM driver for their observatory automation product known as Digital DomeWorks. The project served well for 12 years but here we are in 2018 and it was looking a bit tired. In the intervening years, the ASCOM LocalServer (hub) pattern was created; we created our Reactive Communications for ASCOM library; and we learned a lot about the art of software development based on experience of producing more than 10 commercial ASCOM drivers for telescopes, domes, focusers, rotators, switches and weather safety systems. + +But we started to experience problems with the old driver and we were not happy with the robustness in operation nor the code quality. We decided it was time for a new start. So we have produced the 2018 Reboot version, sporting the following features: + +- ASCOM LocalServer/Hub that accepts multiple client connections. +- ASCOM Dome driver, implementing the IDomeV2 interface. +- ASCOM Switch driver, implementing the ISwitchV2 interface. + Allows applications to control + the user output pins and therefore the T.I. Remote Power Module. +- Low level code is based on a state machine that tracks the + state of the hardware. Invalid operations are simply ignored. + This improves the robustness of the driver. +- Reliable shutter state upon connection. If the shutter position is unknown when + the driver connects, then it can optionally be forced to close to establish a + known starting state. +- Shutter Position Inference - more reliable shutter state reporting. +- Hardware simulator built in. To test the driver without hardware, simply check + the "use simulator" option in the setup dialog. +- New graphical user interface shows the current state of the hardware. +- Open and Close buttons in the status display allow convenient opening + and closing. +- Easy access to the Setup Dialog from the status display. +- Display of the number of active client connections. +- Fully asynchronous operation except where the ASCOM Standards indicate + otherwise. +- Event driven code means that the client application never has to wait + for an answer, the latest data is alaways available immediately without + having to send a command and wait for the response. This means we are + never blocking the user interface and we don't slow down applications. +- Use of the [Reactive Communications for ASCOM][RxAscom] library means + that the driver is thread-safe for use in multi-threaded applications + such as SGP. + +## Hardware Simulator ## + +The driver now incorporates the hardware simulator that we developed to assist with our unit testing. The simulator can work in `real time` or `quick time`. In `real time` mode the simulator tries to be as realistic as possible, simulating the timing characteristics of the Digital DomeWorks hardware. In `quick time` mode, everything is done as fast as possible. + +The simulator can come in very handy if you need to test software without being close to the actual hardware. Previous versions required a serial port but the new version uses an in-memory communications channel built on the technology in our [Reactive Communications][RxAscom] library. To use the simulator, all that is needed is to check the checkbox in the setup dialog. + +![The setup dialog, showing the simulator checkbox][ddw-setup] + +## Power Automation ## + +Previous versions had some clunky command line utilities and _Custom Actions_ for controlling the user output pins. These pins are typically used with the _Remote Power Module_. Now, we provide an ASCOM Switch driver making this much easier and more discoverable and usable by applications. And, because the new driver is a LocalServer Hub, both drivers can be in use by multiple clients simultaneously and everything will work. + +## Shutter Position Inference ## + +We experienced issues with Digital Domeworks reporting the shutter position as 'indeterminate' after closing, even though the shutter was fully closed and everything +worked perfectly in terms of the mechanics. We have not been able to determine the source +of this issue, which was causing ACP Expert Scheduler to abandon the imaging session and +go into "Operator Intervention Required" state. Game over; wasted clear skies. + +Digital Domeworks actually has no shutter position sensor. The limit switches activated by the shutter are wired into the Shutter Relay Box, which controls power to the shutter motor, but these signals are not available to the main microprocessor in the control box. This is why the shutter position is always 'Indeterminate' when the unit is powered up even if the shutter is fully open or closed. This lack of direct positional feedback means that Digital +Domeworks must _infer_ the shutter position based on the last shutter command and the length of time the shutter moved for. If you tell the shutter to open and it moves for 5 seconds, then Digital Domeworks assumes it is open (and vice versa for closed). Movement +is detected by measuring the current drawn by the shutter motor. When the current draw exceeds a threshold, then the motor is assumed to be energised and the shutter is assumed +to be moving. When the current draw drops below the threshold for several seconds, it is +assumed that the shutter has reached the limit of travel and stopped moving. + +It occurred to us that we could use a very similar heuristic within the ASCOM driver +and ignore the problematic detection built into the firmware. We have all of the +information available to us to do this because Digital Domeworks tells us when a shutter +movement has started, which direction it is in and then reports current measurements once +per second while the movement proceeds. + +So that's exactly what we have done. We introduced a driver option, titled +_Ignore shutter sensor and infer shutter position_. When this option is disabled +(the default) then we use the shutter position reported by Digital Domeworks. When the +option is enabled, we use our own custom heuristic to infer the shutter position. +Our initial testing has indicated that our heuristic works at least as well as the one in +the firmware and eliminates some of the phantom shutter errors that we were observing. + +This option is experimental and potentially unsafe, because we are overriding the status +reported by the firmware. We do not recommend that you use this option unless you are +seeing the 'phantom errors' and want to try our workaround and you have carefully +considered the implications. If you do enable this option, you will be warned that you +are enabling a potentially unsafe configuration. Note that this option will not fix +mechanical or electrical issues. If in doubt, leave the option at the default setting +of Disabled. + +## Obtaining and Installing the Driver ## + +The driver may be downloaded from: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks/downloads/ + +The source code is available at: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks + +The download is a zip file containing 4 installers. There are `Debug` and `Release` builds for each of `x86` (32-bit) and `x64` (64-bit) architectures. End users should normally use the `Release` build and install the Release configuration that matches their machine architecture: `x86` for 32-bit systems and `x64` for 64-bit systems. The installer checks this and will not allow the wrong architecture to be installed. + +The `Debug` configurations contain debugging sybols and produce *copious amounts* of diagnostic output and therefore run relatively slowly. ***Debug builds are not recommended for normal use*** and should only be installed when troubleshooting with a debugger is required. + +## Open Source ## + +In August 2015, Tigra Astronomy took the decision to open-source the project and we have continued with that policy in the 2018 reboot. The source code is available from a BitBucket Git repository. The code is being made available under the MIT license which is about as permissive as it gets. Basically, anyone can do anything at all with the software with no strings attached, and we are not liable for the consequences, whatever they are. + +End users should normally only install the latest release build. Beta builds are likely to contain bugs and are not supported for production use. Integration builds contain up-to-the-minute code changes but might not be production quality so are not supported under any circumstances. + +## Bug Reports and Feature Requests ## + +Hopefully there will be few problems, but if you do find any, or would like to request a new feature or provide other feedback, then please use our [official public/anonymous bug tracker][bugs]. Unless it's in our bug tracker, we haven't received it and you cannot expect any response. + + +[bugs]: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks/issues?status=new&status=open "BitBucket Public Issue Tracker" +[RxAscom]: http://tigra-astronomy.com/reactive-communications-for-ascom "Reactive ASCOM project home page" +[download]: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks/downloads/ "Download the drivers" +[git]: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks "Get the source code" +[project-home]: http://tigra-astronomy.com/ascom-drivers-for-digital-domeworks "ASCOM Drivers for Digital Domeworks, 2018 reboot" +[ddw-status]: http://tigra-astronomy.com/Media/TigraAstronomy/site-images/Digital-Domeworks-2018/DDW-status-display.png +[ddw-setup]: http://tigra-astronomy.com/Media/TigraAstronomy/site-images/Digital-Domeworks-2018/DDW-setup-dialog.png \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomDome/Dome.cs b/TA.DigitalDomeworks.AscomDome/Dome.cs new file mode 100644 index 0000000..853a7c5 --- /dev/null +++ b/TA.DigitalDomeworks.AscomDome/Dome.cs @@ -0,0 +1,238 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: Dome.cs Last modified: 2018-04-21@21:39 by Tim Long + +using System; +using System.Collections; +using System.Runtime.InteropServices; +using ASCOM; +using ASCOM.DeviceInterface; +using JetBrains.Annotations; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.Server; +using TA.DigitalDomeworks.SharedTypes; +using TA.PostSharp.Aspects; +using TI.DigitalDomeWorks; +using NotImplementedException = ASCOM.NotImplementedException; + +namespace TA.DigitalDomeworks.AscomDome + { + [ProgId(SharedResources.DomeDriverId)] + [Guid("CCF89F7D-2889-4A9D-891D-E28760A0FFCA")] + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [DeviceId(SharedResources.DomeDriverId, DeviceName = SharedResources.DomeDriverName)] + [ServedClassName(SharedResources.DomeDriverName)] + public class Dome : ReferenceCountedObject, IDomeV2, IAscomDriver + { + [NotNull] private readonly Guid clientId; + [CanBeNull] private DeviceController controller; + + public Dome() + { + clientId = SharedResources.ConnectionManager.RegisterClient("ASCOM Dome"); + } + + public void SetupDialog() + { + SharedResources.DoSetupDialog(clientId); + } + + public string Action(string ActionName, string ActionParameters) + { + switch (ActionName) + { + case Constants.ActionNameControllerStatus: + return CustomActionControllerStatus(); + case Constants.ActionNameDsrSwingoutState: + return CustomActionDsrSwingoutSensorState(); + default: + throw new ActionNotImplementedException(ActionName); + } + } + + public void CommandBlind(string Command, bool Raw = false) + { + throw new NotImplementedException(); + } + + public bool CommandBool(string Command, bool Raw = false) + { + throw new NotImplementedException(); + } + + public string CommandString(string Command, bool Raw = false) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + SharedResources.ConnectionManager.GoOffline(clientId); + SharedResources.ConnectionManager.UnregisterClient(clientId); + controller = null; //[Sentinel] + } + + [MustBeConnected] + public void AbortSlew() + { + controller.RequestEmergencyStop(); + } + + [MustBeConnected] + public void CloseShutter() + { + controller.CloseShutter(); + } + + [MustBeConnected] + public void FindHome() => controller.RotateToHomePosition(); + + [MustBeConnected] + public void OpenShutter() + { + controller.OpenShutter(); + } + + [MustBeConnected] + public void Park() + { + controller.Park(); + AtPark = controller.AtHome && controller.ShutterPosition == SensorState.Closed; + } + + public void SetPark() + { + throw new NotImplementedException(); + } + + public void SlewToAltitude(double Altitude) + { + throw new NotImplementedException(); + } + + [MustBeConnected] + public void SlewToAzimuth(double Azimuth) + { + try + { + controller.SlewToAzimuth(Azimuth); + } + catch (ArgumentOutOfRangeException ex) + { + throw new InvalidValueException(nameof(Azimuth), Azimuth.ToString(), "0.0 <= azimuth < 360.0", ex); + } + } + + public void SyncToAzimuth(double Azimuth) + { + throw new NotImplementedException(); + } + + public bool Connected + { + get => controller?.IsConnected ?? false; + set + { + if (value) + { + controller = SharedResources.ConnectionManager.GoOnline(clientId); + } + else + { + SharedResources.ConnectionManager.GoOffline(clientId); + controller = null; //[Sentinel] + } + } + } + + public string Description => "ASCOM Dome driver for Digital Domeworks"; + + public string DriverInfo => @"ASCOM Dome driver for Digital Domeworks, 2018 reboot +An open source ASCOM driver by Tigra Astronomy +Home page: http://tigra-astronomy.com +Source code: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks +License: https://tigra.mit-license.org/ +Copyright © 2018 Tigra Astronomy"; + + public string DriverVersion => "7.0"; + + public short InterfaceVersion => 2; + + public string Name => "Digital Domeworks 2018 Reboot"; + + public ArrayList SupportedActions => new ArrayList + { + Constants.ActionNameDsrSwingoutState, + Constants.ActionNameControllerStatus + }; + + public double Altitude => throw new PropertyNotImplementedException(nameof(Altitude), accessorSet: false); + + [MustBeConnected] + public bool AtHome => controller.AtHome; + + [MustBeConnected] + public bool AtPark { get; private set; } + + [MustBeConnected] + public double Azimuth => controller.AzimuthDegrees; + + public bool CanFindHome => true; + + public bool CanPark => true; + + public bool CanSetAltitude => false; + + public bool CanSetAzimuth => true; + + public bool CanSetPark => false; + + public bool CanSetShutter => true; + + public bool CanSlave => false; + + public bool CanSyncAzimuth => false; + + [MustBeConnected] + public ShutterState ShutterStatus + { + get + { + if (controller.ShutterMovementDirection == ShutterDirection.Closing) + return ShutterState.shutterClosing; + if (controller.ShutterMovementDirection == ShutterDirection.Opening) + return ShutterState.shutterOpening; + if (controller.ShutterPosition == SensorState.Closed) + return ShutterState.shutterClosed; + if (controller.ShutterPosition == SensorState.Open) + return ShutterState.shutterOpen; + return ShutterState.shutterError; + } + } + + public bool Slaved + { + get => false; + set => throw new NotImplementedException(); + } + + [MustBeConnected] + public bool Slewing => controller.IsMoving; + + [NotNull] + private string CustomActionDsrSwingoutSensorState() + { + return controller?.CurrentStatus?.DsrSensor.ToString() ?? SensorState.Indeterminate.ToString(); + } + + [NotNull] + private string CustomActionControllerStatus() + { + var status = controller.CurrentStatus; + return status?.ToString() ?? string.Empty; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomDome/Properties/AssemblyInfo.cs b/TA.DigitalDomeworks.AscomDome/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d41d58b --- /dev/null +++ b/TA.DigitalDomeworks.AscomDome/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("TA.DigitalDomeworks.AscomDome")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TA.DigitalDomeworks.AscomDome")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[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("0b22461b-2d19-4342-a87d-4e90efd3e2a9")] + +// 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/TA.DigitalDomeworks.AscomDome/TA.DigitalDomeworks.AscomDome.csproj b/TA.DigitalDomeworks.AscomDome/TA.DigitalDomeworks.AscomDome.csproj new file mode 100644 index 0000000..b5d8713 --- /dev/null +++ b/TA.DigitalDomeworks.AscomDome/TA.DigitalDomeworks.AscomDome.csproj @@ -0,0 +1,135 @@ + + + + + + Debug + AnyCPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9} + Library + Properties + TA.DigitalDomeworks.AscomDome + TA.DigitalDomeworks.AscomDome + v4.6.2 + 512 + + + + + true + full + false + ..\BuildOutput\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\BuildOutput\Release\ + TRACE + prompt + 4 + + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Astrometry.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Attributes.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Controls.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DeviceInterfaces.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DriverAccess.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Exceptions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Internal.Extensions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.SettingsProvider.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.Video.dll + True + + + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll + + + ..\packages\NLog.4.5.0\lib\net45\NLog.dll + + + ..\packages\PostSharp.Redist.5.0.48\lib\net45\PostSharp.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + {020924B9-23D5-4B92-B5B1-461423BBE23C} + TA.DigitalDomeworks.DeviceInterface + + + {3689a2cb-94c5-4012-a5cf-7e7d1dd27143} + TA.DigitalDomeworks.Server + + + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F} + TA.DigitalDomeworks.SharedTypes + + + {9CDCF319-DADC-41EB-B787-DE3862017E95} + TA.PostSharp.Aspects + + + + + + 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/TA.DigitalDomeworks.AscomDome/app.config b/TA.DigitalDomeworks.AscomDome/app.config new file mode 100644 index 0000000..5b35c91 --- /dev/null +++ b/TA.DigitalDomeworks.AscomDome/app.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomDome/packages.config b/TA.DigitalDomeworks.AscomDome/packages.config new file mode 100644 index 0000000..9c18a39 --- /dev/null +++ b/TA.DigitalDomeworks.AscomDome/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomSwitch/Properties/AssemblyInfo.cs b/TA.DigitalDomeworks.AscomSwitch/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e4f43c0 --- /dev/null +++ b/TA.DigitalDomeworks.AscomSwitch/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("TA.DigitalDomeworks.AscomSwitch")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TA.DigitalDomeworks.AscomSwitch")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[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("f95208c4-450f-4b51-810a-c805a8efbb7a")] + +// 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/TA.DigitalDomeworks.AscomSwitch/Switch.cs b/TA.DigitalDomeworks.AscomSwitch/Switch.cs new file mode 100644 index 0000000..a067119 --- /dev/null +++ b/TA.DigitalDomeworks.AscomSwitch/Switch.cs @@ -0,0 +1,133 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: Switch.cs Last modified: 2018-03-28@01:08 by Tim Long + +using System; +using System.Collections; +using System.Runtime.InteropServices; +using ASCOM; +using ASCOM.DeviceInterface; +using JetBrains.Annotations; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.Server; +using TA.PostSharp.Aspects; +using NotImplementedException = ASCOM.NotImplementedException; + +namespace TA.DigitalDomeworks.AscomSwitch + { + [ProgId(SharedResources.SwitchDriverId)] + [Guid("8f3d72d5-7fb8-4f8a-8f73-3c724a8a375c")] + [ComVisible(true)] + [ClassInterface(ClassInterfaceType.None)] + [DeviceId(SharedResources.SwitchDriverId, DeviceName = SharedResources.SwitchDriverName)] + [ServedClassName(SharedResources.SwitchDriverName)] + public class Switch : ReferenceCountedObject, ISwitchV2, IAscomDriver + { + private readonly Guid clientId; + [CanBeNull] private DeviceController controller; + + public Switch() + { + clientId = SharedResources.ConnectionManager.RegisterClient("ASCOM Switch"); + } + + public void SetupDialog() + { + SharedResources.DoSetupDialog(clientId); + } + + public string Action(string ActionName, string ActionParameters) + { + throw new NotImplementedException(); + } + + public void CommandBlind(string Command, bool Raw = false) + { + throw new NotImplementedException(); + } + + public bool CommandBool(string Command, bool Raw = false) + { + throw new NotImplementedException(); + } + + public string CommandString(string Command, bool Raw = false) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + SharedResources.ConnectionManager.GoOffline(clientId); + SharedResources.ConnectionManager.UnregisterClient(clientId); + controller = null; //[Sentinel] + } + + public string GetSwitchName(short id) => id.ToString(); + + public void SetSwitchName(short id, string name) + { + throw new NotImplementedException(); + } + + public string GetSwitchDescription(short id) => $"Switch {id}"; + + public bool CanWrite(short id) => true; + + [MustBeConnected] + public bool GetSwitch(short id) => controller?.UserPins[id] ?? false; + + [MustBeConnected] + public void SetSwitch(short id, bool state) => controller?.SetUserOutputPin(id, state); + + public double MaxSwitchValue(short id) => 1.0; + + public double MinSwitchValue(short id) => 0.0; + + public double SwitchStep(short id) => 1.0; + + [MustBeConnected] + public double GetSwitchValue(short id) => controller?.UserPins[id] ?? false ? 1 : 0; + + [MustBeConnected] + public void SetSwitchValue(short id, double value) => SetSwitch(id, value >= 0.5); + + public bool Connected + { + get => controller?.IsConnected ?? false; + set + { + if (value) + { + controller = SharedResources.ConnectionManager.GoOnline(clientId); + } + else + { + SharedResources.ConnectionManager.GoOffline(clientId); + controller = null; //[Sentinel] + } + } + } + + public string Description => "ASCOM Dome driver for Digital Domeworks"; + + public string DriverInfo => @"ASCOM Dome driver for Digital Domeworks, 2018 reboot +An open source ASCOM driver by Tigra Astronomy +Home page: http://tigra-astronomy.com +Source code: https://bitbucket.org/tigra-astronomy/ta.digitaldomeworks +License: https://tigra.mit-license.org/ +Copyright © 2018 Tigra Astronomy"; + + public string DriverVersion => "7.0"; + + public short InterfaceVersion => 2; + + public string Name => "Digital Domeworks 2018 Reboot"; + + public ArrayList SupportedActions => new ArrayList(); + + public short MaxSwitch => 4; + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomSwitch/TA.DigitalDomeworks.AscomSwitch.csproj b/TA.DigitalDomeworks.AscomSwitch/TA.DigitalDomeworks.AscomSwitch.csproj new file mode 100644 index 0000000..dd63432 --- /dev/null +++ b/TA.DigitalDomeworks.AscomSwitch/TA.DigitalDomeworks.AscomSwitch.csproj @@ -0,0 +1,113 @@ + + + + + Debug + AnyCPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A} + Library + Properties + TA.DigitalDomeworks.AscomSwitch + TA.DigitalDomeworks.AscomSwitch + v4.6.2 + 512 + + + true + full + false + ..\BuildOutput\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + ..\BuildOutput\Release\ + TRACE + prompt + 4 + + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Astrometry.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Attributes.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Controls.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DeviceInterfaces.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DriverAccess.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Exceptions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Internal.Extensions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.SettingsProvider.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.Video.dll + True + + + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll + + + + + + + + + + + + + + + + + + {020924B9-23D5-4B92-B5B1-461423BBE23C} + TA.DigitalDomeworks.DeviceInterface + + + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143} + TA.DigitalDomeworks.Server + + + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F} + TA.DigitalDomeworks.SharedTypes + + + {9CDCF319-DADC-41EB-B787-DE3862017E95} + TA.PostSharp.Aspects + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomSwitch/app.config b/TA.DigitalDomeworks.AscomSwitch/app.config new file mode 100644 index 0000000..cd002dd --- /dev/null +++ b/TA.DigitalDomeworks.AscomSwitch/app.config @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.AscomSwitch/packages.config b/TA.DigitalDomeworks.AscomSwitch/packages.config new file mode 100644 index 0000000..a0d3245 --- /dev/null +++ b/TA.DigitalDomeworks.AscomSwitch/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/ClassDiagram1.cd b/TA.DigitalDomeworks.DeviceInterface/ClassDiagram1.cd new file mode 100644 index 0000000..27cbabc --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/ClassDiagram1.cd @@ -0,0 +1,54 @@ + + + + + + AAgAAAAAEAAAAAAQAAAABAQAAAAQAAACAIAAAAAAAAA= + StateMachine\Ready.cs + + + + + + + AAgAAAAAEAAAAAAQAAAABAQAAAAQAAACAIAAAAAAAAA= + StateMachine\Rotating.cs + + + + + + + AAEEEACEABCAAQAQIAAIACIAgADAAQAAAAAAAAAARAI= + StateMachine\ControllerStateMachine.cs + + + + + + + + + + AAgAAAAAEAAAAAAQAAAAAAQAAAgQAAACAIAAAAAAAAA= + StateMachine\StateLoggingDecorator.cs + + + + + + + iQBAAAAUABCoMEAAIAAKAEAkkSCoAQEAAAAAAICAAAA= + DeviceController.cs + + + + + + + AAgAAAAAEAAAAAAQAAAAAAQAAAAQAAACAIAAAAAAAAA= + StateMachine\IControllerState.cs + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/DeviceController.cs b/TA.DigitalDomeworks.DeviceInterface/DeviceController.cs index ee949c1..87891a1 100644 --- a/TA.DigitalDomeworks.DeviceInterface/DeviceController.cs +++ b/TA.DigitalDomeworks.DeviceInterface/DeviceController.cs @@ -1,70 +1,287 @@ -using System.ComponentModel; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: DeviceController.cs Last modified: 2018-04-21@21:36 by Tim Long + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reactive.Linq; using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using NLog.Fluent; using PostSharp.Patterns.Model; using TA.Ascom.ReactiveCommunications; +using TA.Ascom.ReactiveCommunications.Diagnostics; +using TA.DigitalDomeworks.DeviceInterface.StateMachine; using TA.DigitalDomeworks.SharedTypes; namespace TA.DigitalDomeworks.DeviceInterface { [NotifyPropertyChanged] - internal class DeviceController : INotifyPropertyChanged + public class DeviceController : INotifyPropertyChanged { [NotNull] private readonly ICommunicationChannel channel; + private readonly DeviceControllerOptions configuration; + [NotNull] private readonly List disposableSubscriptions = new List(); + [NotNull] private readonly ControllerStateMachine stateMachine; [NotNull] private readonly ControllerStatusFactory statusFactory; - [CanBeNull] private ReactiveTransactionProcessor transactionProcessor; - public DeviceController(ICommunicationChannel channel, ControllerStatusFactory factory) + public DeviceController(ICommunicationChannel channel, ControllerStatusFactory factory, + ControllerStateMachine machine, DeviceControllerOptions configuration) { this.channel = channel; statusFactory = factory; + stateMachine = machine; + this.configuration = configuration; } - public void Open() + public Octet UserPins => stateMachine.UserPins; + + public int AzimuthEncoderPosition => stateMachine.AzimuthEncoderPosition; + + public float AzimuthDegrees => AzimuthEncoderPosition * DegreesPerTick; + + private float DegreesPerTick => 360f / stateMachine.HardwareStatus?.DomeCircumference ?? 100; + + /// + /// true if any part of the building is moving. + /// + public bool IsMoving => stateMachine.AzimuthMotorActive || stateMachine.ShutterMotorActive; + + public bool IsConnected => channel.IsOpen; + + public bool AzimuthMotorActive => stateMachine.AzimuthMotorActive; + + public RotationDirection AzimuthDirection => stateMachine.AzimuthDirection; + + public ShutterDirection ShutterMovementDirection => stateMachine.ShutterMovementDirection; + + public int ShutterMotorCurrent => stateMachine.ShutterMotorCurrent; + + public bool ShutterMotorActive => stateMachine.ShutterMotorActive; + + public SensorState ShutterPosition => stateMachine.ShutterPosition; + + public bool AtHome => stateMachine.AtHome; + + public IHardwareStatus CurrentStatus => stateMachine.HardwareStatus; + + public event PropertyChangedEventHandler PropertyChanged; + + public void RotateToHomePosition() => stateMachine.RotateToHomePosition(); + + public void Open(bool performOnConnectActions = true) { - var observer = new TransactionObserver(channel); - transactionProcessor = new ReactiveTransactionProcessor(); - transactionProcessor.SubscribeTransactionObserver(observer); + SubscribeControllerEvents(); channel.Open(); - PerformTasksOnConnect(); + if (performOnConnectActions) + stateMachine.Initialize(new RequestStatus(stateMachine)); + else + stateMachine.Initialize(new Ready(stateMachine)); + stateMachine.WaitForReady(TimeSpan.FromSeconds(5)); + if (performOnConnectActions && configuration.PerformShutterRecovery) PerformShutterRecovery(); } - private void PerformTasksOnConnect() + /// + /// Tries to establish a known shutter condition at startup. + /// Assumes that a valid status packet has already been received. + /// + /// + /// Thrown if shutter recovery does not complete within the + /// allotted time. + /// + private void PerformShutterRecovery() { - var transaction = new StatusTransaction(statusFactory); - transactionProcessor.CommitTransaction(transaction); - transaction.WaitForCompletionOrTimeout(); // Synchronous - transaction.ThrowIfFailed(); - CurrentStatus = transaction.ControllerStatus; + if (ShutterPosition == SensorState.Indeterminate) + { + Log.Info() + .Message("Shutter position is indeterminate, attempting to close the shutter.") + .Write(); + stateMachine.CloseShutter(); + stateMachine.WaitForReady(configuration.MaximumFullRotationTime + + configuration.MaximumShutterCloseTime); + } } - public IControllerStatus CurrentStatus { get; private set; } + private void SubscribeControllerEvents() + { + SubscribeAzimuthEncoderTicks(); + SubscribeRotationDirection(); + SubscribeShutterCurrentReadings(); + SubscribeShutterDirection(); + SubscribeStatusUpdates(); + } - public void Close() + private void SubscribeStatusUpdates() + { + var statusUpdates = channel.ObservableReceivedCharacters.StatusUpdates(statusFactory); + var statusUpdateSubscription = statusUpdates + //.ObserveOn(Scheduler.Default) + .Subscribe(StatusUpdateOnNext, + ex => throw new InvalidOperationException( + "Status Update sequence produced an unexpected error (see inner exception)", ex), + () => throw new InvalidOperationException( + "Status Update sequence completed unexpectedly, this is probably a bug") + ); + disposableSubscriptions.Add(statusUpdateSubscription); + } + + private void StatusUpdateOnNext(IHardwareStatus statusNotification) { - transactionProcessor?.Dispose(); - transactionProcessor = null; + try + { + stateMachine.HardwareStatusReceived(statusNotification); + } + catch (Exception ex) + { + Log.Error() + .Exception(ex) + .Message($"Error while processing status notification: {statusNotification}") + .Write(); + } } - public bool IsOnline => channel.IsOpen; + private void SubscribeShutterDirection() + { + var shutterDirectionSubscription = channel.ObservableReceivedCharacters.ShutterDirectionUpdates() + //.ObserveOn(Scheduler.Default) + .Subscribe( + stateMachine.ShutterDirectionReceived, + ex => throw new InvalidOperationException( + "Shutter Direction sequence produced an unexpected error (see ineer exception)", ex), + () => throw new InvalidOperationException( + "Shutter Direction sequence completed unexpectedly, this is probably a bug") + ); + disposableSubscriptions.Add(shutterDirectionSubscription); + } - public async Task GetStatus() + private void SubscribeShutterCurrentReadings() { - var getStatusTransaction = new StatusTransaction(statusFactory); - transactionProcessor.CommitTransaction(getStatusTransaction); - await getStatusTransaction.WaitForCompletionOrTimeoutAsync(CancellationToken.None); - getStatusTransaction.ThrowIfFailed(); - return getStatusTransaction.ControllerStatus; + var shutterCurrentReadings = channel.ObservableReceivedCharacters.ShutterCurrentReadings(); + var shutterCurrentSubscription = shutterCurrentReadings + //.ObserveOn(Scheduler.Default) + .Subscribe( + stateMachine.ShutterMotorCurrentReceived, + ex => throw new InvalidOperationException( + "Shutter Current sequence produced an unexpected error (see inner exception)", ex), + () => throw new InvalidOperationException( + "ShutterCurrent sequence completed unexpectedly, this is probably a bug") + ); + disposableSubscriptions.Add(shutterCurrentSubscription); } - public event PropertyChangedEventHandler PropertyChanged; + private void SubscribeRotationDirection() + { + var rotationDirectionSequence = from c in channel.ObservableReceivedCharacters + where c == 'L' || c == 'R' + let direction = c == 'L' + ? RotationDirection.CounterClockwise + : RotationDirection.Clockwise + select direction; + var rotationDirectionSubscription = rotationDirectionSequence + .Trace("RotationDirection") + //.ObserveOn(Scheduler.Default) + .Subscribe( + stateMachine.RotationDirectionReceived, + ex => throw new InvalidOperationException( + "RotationDirection sequence produced an unexpected error (see ineer exception)", ex), + () => throw new InvalidOperationException( + "RotationDirection sequence completed unexpectedly, this is probably a bug") + ); + disposableSubscriptions.Add(rotationDirectionSubscription); + } + + private void SubscribeAzimuthEncoderTicks() + { + var azimuthEncoderTicks = channel.ObservableReceivedCharacters.AzimuthEncoderTicks(); + var azimuthEncoderSubscription = azimuthEncoderTicks + //.ObserveOn(Scheduler.Default) + .Subscribe( + stateMachine.AzimuthEncoderTickReceived, + ex => throw new InvalidOperationException( + "Encoder tick sequence produced an unexpected error (see ineer exception)", ex), + () => throw new InvalidOperationException( + "Encoder tick sequence completed unexpectedly, this is probably a bug") + ); + disposableSubscriptions.Add(azimuthEncoderSubscription); + } + + private void RotationDirectionOnNext(RotationDirection direction) { } + + public void Close() + { + UnsubscribeControllerEvents(); + channel.Close(); + } + + private void UnsubscribeControllerEvents() + { + disposableSubscriptions.ForEach(m => m.Dispose()); + disposableSubscriptions.Clear(); + } [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + public void SetUserOutputPin(int pinNumber, bool state) + { + var newState = UserPins.WithBitSetTo(pinNumber, state); + stateMachine.SetUserOutputPins(newState); + } + + public void RequestEmergencyStop() + { + var pause = TimeSpan.FromSeconds(1); + stateMachine.AllStop(); + Task.Delay(pause).Wait(); + stateMachine.AllStop(); + Task.Delay(pause).Wait(); + stateMachine.AllStop(); + } + + public void SlewToAzimuth(double azimuth) + { + if (azimuth < 0.0 || azimuth >= 360.0) + throw new ArgumentOutOfRangeException(nameof(azimuth), azimuth, + "Invalid azimuth. Must be 0.0 <= azimuth < 360.0"); + stateMachine.RotateToAzimuthDegrees(azimuth); + } + + public void OpenShutter() + { + stateMachine.OpenShutter(); + } + + public void CloseShutter() + { + stateMachine.CloseShutter(); + } + + /// + /// Parks the dome by closing the shutter. + /// Blocks until completed or an error occurs. + /// + public void Park() + { + TimeSpan timeout; + if (ShutterPosition != SensorState.Closed) + { + stateMachine.CloseShutter(); + timeout = configuration.MaximumFullRotationTime + configuration.MaximumShutterCloseTime; + } + else + { + stateMachine.RotateToHomePosition(); + timeout = configuration.MaximumFullRotationTime; + } + // Potentially throws TimeoutException - let this propagate to the client application. + stateMachine.WaitForReady(timeout); + } } -} \ No newline at end of file + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/INotifyHardwareStateChanged.cs b/TA.DigitalDomeworks.DeviceInterface/INotifyHardwareStateChanged.cs new file mode 100644 index 0000000..3a8d35b --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/INotifyHardwareStateChanged.cs @@ -0,0 +1,56 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: INotifyHardwareStateChanged.cs Last modified: 2018-03-17@15:07 by Tim Long + +using System.ComponentModel; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface + { + /// + /// Properties that describe the current state of the Digital Domeworks controller. + /// All properties support change notifications via + /// + public interface INotifyHardwareStateChanged : INotifyPropertyChanged + { + /// + /// The current value of the azimuth encoder. Updates in real time as the dome rotates + /// and whenever a status report is received. + /// + int AzimuthEncoderPosition { get; } + + /// + /// A relative indication of the amount of current being drawn by the shutter motor. + /// Zero means the motor is not running and values up to about 30 are normal. + /// Updates about once per second during shutter movement and whenever a status report + /// is received. + /// + int ShutterMotorCurrent { get; } + + /// + /// The direction of rotation, if known. A value of + /// does not necessarily indicate that the motor is inactive because under some + /// circumstances the rotation direction cannot be determined. + /// + RotationDirection AzimuthDirection { get; } + + /// + /// The direction of shutter travel, if known. A value of + /// does not necessarily indicate that the motor is inactive because under some + /// circumstances the direction of travel cannot be determined. + /// + ShutterDirection ShutterMovementDirection { get; } + + /// + /// Indicates whether the azimuth motor is energized. + /// + bool AzimuthMotorActive { get; } + + /// + /// Indicates whether the shutter motor is energized. + /// + bool ShutterMotorActive { get; } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/ObservableExtensions.cs b/TA.DigitalDomeworks.DeviceInterface/ObservableExtensions.cs new file mode 100644 index 0000000..b1552f8 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/ObservableExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using NLog.Fluent; +using TA.Ascom.ReactiveCommunications.Diagnostics; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface + { + static class ObservableExtensions + { + /// + /// Extracts azimuth encoder ticks from a source sequence and emits + /// the encoder values as an observable sequence of integers. + /// + /// + public static IObservable AzimuthEncoderTicks(this IObservable source) + { + const string azimuthEncoderPattern = @"^P(?\d{1,4})[^0-9]"; + var azimuthEncoderRegex = + new Regex(azimuthEncoderPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + var buffers = source.Publish(s => s.BufferByPredicates(p => p == 'P', q => !char.IsDigit(q))); + var azimuthValues = from buffer in buffers + let message = new string(buffer.ToArray()) + let patternMatch = azimuthEncoderRegex.Match(message) + where patternMatch.Success + let azimuth = int.Parse(patternMatch.Groups["Azimuth"].Value) + select azimuth; + return azimuthValues.Trace("EncoderTicks"); + } + + public static IObservable ShutterCurrentReadings(this IObservable source) + { + const string shutterCurrentPattern = @"^Z(?\d{1,3})"; + var shutterCurrentRegex = + new Regex(shutterCurrentPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + var buffers = source.Publish(s => s.BufferByPredicates(p => p == 'Z', q => !char.IsDigit(q))); + var shutterCurrentValues = from buffer in buffers + let message = new string(buffer.ToArray()) + let patternMatch = shutterCurrentRegex.Match(message) + where patternMatch.Success + let shutterCurrent = int.Parse(patternMatch.Groups["Current"].Value) + select shutterCurrent; + return shutterCurrentValues.Trace("ShutterCurrent"); + } + + public static IObservable> BufferByPredicates(this IObservable source, + Predicate bufferOpening, Predicate bufferClosing) + { + return source.Buffer(source.Where(c => bufferOpening(c)), x => source.Where(c => bufferClosing(c))); + } + + public static IObservable StatusUpdates(this IObservable source, + ControllerStatusFactory factory) + { + const string validStatusCharacters = "V+-0123456789,"; + const string statusPattern = @"^(?V4(,\d{1,3}){22})"; + var statusRegex = new Regex(statusPattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture); + var buffers = source.Publish(s => + s.BufferByPredicates(p => p == 'V', q => !validStatusCharacters.Contains(q))); + var statusValues = from buffer in buffers + let message = new string(buffer.ToArray()) + let patternMatch = statusRegex.Match(message) + where patternMatch.Success + let status = patternMatch.Groups["Status"].Value + let harwareStatus = factory.FromStatusPacket(status) + select harwareStatus; + return statusValues.Trace("StatusUpdates"); + } + + public static IObservable ShutterDirectionUpdates(this IObservable source) + { + // Note: The zero-based index in the string must match the ordinal values in ShutterDirection + const string shutterMovementIndicators = "SCO"; + var shutterDirectionSequence = from c in source + where shutterMovementIndicators.Contains(c) + let ordinal = shutterMovementIndicators.IndexOf(c) + let direction = (ShutterDirection) ordinal + select direction; + return shutterDirectionSequence.Trace("ShutterDirection"); + } + } + } diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/ControllerStateBase.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/ControllerStateBase.cs new file mode 100644 index 0000000..75d5d22 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/ControllerStateBase.cs @@ -0,0 +1,99 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ControllerStateBase.cs Last modified: 2018-03-30@03:22 by Tim Long + +using System; +using System.Diagnostics.Contracts; +using System.Threading; +using System.Threading.Tasks; +using NLog.Fluent; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal abstract class ControllerStateBase : IControllerState + { + protected readonly ControllerStateMachine machine; + private CancellationTokenSource timeoutCancellation; + + protected ControllerStateBase(ControllerStateMachine machine) + { + Contract.Requires(machine != null); + this.machine = machine; + } + + public virtual void OnEnter() { } + + public virtual void OnExit() + { + timeoutCancellation?.Cancel(); + } + + public virtual void RotationDetected() { } + + public virtual void ShutterMovementDetected() { } + + public virtual void StatusUpdateReceived(IHardwareStatus status) { } + + public virtual string Name => GetType().Name; + + public virtual void RotateToAzimuthDegrees(double azimuth) { } + + public virtual void OpenShutter() { } + + public virtual void CloseShutter() { } + + public virtual void RotateToHomePosition() { } + + public virtual void SetUserOutputPins(Octet newState) { } + + protected void ResetTimeout(TimeSpan timeout) + { + timeoutCancellation?.Cancel(); + timeoutCancellation = new CancellationTokenSource(); + var timeoutCancellationToken = timeoutCancellation.Token; + ResetTimeoutAsync(timeout, timeoutCancellationToken); + } + + private async void ResetTimeoutAsync(TimeSpan timeout, CancellationToken cancel) + { + /* + * This code needs to be protected in a try-catch block because + * it is async void and an unhandled exception here would + * crash the process. + */ + try + { + await Task.Delay(timeout, cancel); + if (cancel.IsCancellationRequested) + return; + HandleTimeout(); + } + catch (TaskCanceledException) + { + // This is expected, no action necessary. + } + catch (Exception ex) + { + Log.Warn() + .Exception(ex) + .Message("Exception while awaiting state timeout. This is unexpected.") + .Write(); + } + } + + protected virtual void HandleTimeout() + { + Log.Warn().Message("state timed out").Write(); + } + + protected void CancelTimeout() + { + timeoutCancellation?.Cancel(); + } + + public virtual void RequestHardwareStatus() {} + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/ControllerStateMachine.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/ControllerStateMachine.cs new file mode 100644 index 0000000..14a55f0 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/ControllerStateMachine.cs @@ -0,0 +1,258 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ControllerStateMachine.cs Last modified: 2018-04-06@02:12 by Tim Long + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using NLog.Fluent; +using PostSharp.Patterns.Model; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + [NotifyPropertyChanged] + public class ControllerStateMachine : INotifyHardwareStateChanged + { + internal readonly ManualResetEvent InReadyState = new ManualResetEvent(false); + [CanBeNull] internal CancellationTokenSource KeepAliveCancellationSource; + + public ControllerStateMachine(IControllerActions controllerActions, DeviceControllerOptions options, IClock clock) + { + ControllerActions = controllerActions; + Options = options; + Clock = clock; + CurrentState = new Uninitialized(); + } + + internal IControllerActions ControllerActions { get; } + + internal DeviceControllerOptions Options { get; } + + public IClock Clock { get; } + + internal IControllerState CurrentState { get; private set; } + + [CanBeNull] + public IHardwareStatus HardwareStatus { get; private set; } + + public bool AtHome { get; set; } + + public SensorState ShutterPosition { get; set; } + + /// + /// The state of the user output pins. Bits 0..3 are significant, other bits are unused. + /// + public Octet UserPins { get; private set; } = Octet.Zero; + + public int AzimuthEncoderPosition { get; internal set; } + + public int ShutterMotorCurrent { get; internal set; } + + public RotationDirection AzimuthDirection { get; internal set; } + + public ShutterDirection ShutterMovementDirection { get; internal set; } + + public bool AzimuthMotorActive { get; internal set; } + + public bool ShutterMotorActive { get; internal set; } + + [IgnoreAutoChangeNotification] + internal SensorState InferredShutterPosition { get; set; } = SensorState.Indeterminate; + + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Initializes the state machine and optionally sets the starting state. + /// + /// + public void Initialize(IControllerState startState) + { + TransitionToState(startState); + } + + public void TransitionToState([NotNull] IControllerState targetState) + { + if (targetState == null) throw new ArgumentNullException(nameof(targetState)); + try + { + CurrentState.OnExit(); + } + catch (Exception ex) + { + Log.Error() + .Exception(ex) + .Message($"Unexpected exception leaving state {CurrentState.Name}") + .Write(); + } + + CurrentState = new StateLoggingDecorator(targetState); + try + { + CurrentState.OnEnter(); + } + catch (Exception ex) + { + Log.Error() + .Exception(ex) + .Message($"Unexpected exception entering state {targetState.Name}") + .Write(); + } + } + + [NotifyPropertyChangedInvocator] + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Update state machine properties from a record. + /// By definition, when a status report is received, all movement has ceased. + /// + /// The status report received from the hardware. + internal void UpdateStatus(IHardwareStatus status) + { + AzimuthEncoderPosition = status.CurrentAzimuth; + AzimuthMotorActive = false; + AzimuthDirection = RotationDirection.None; + ShutterMotorActive = false; + ShutterMovementDirection = ShutterDirection.None; + ShutterMotorCurrent = 0; + ShutterPosition = SetInferredShutterPosition(status.ShutterSensor); + AtHome = status.AtHome; + UserPins = status.UserPins; + } + + private SensorState SetInferredShutterPosition(SensorState statusShutterSensor) + { + if (!Options.IgnoreHardwareShutterSensor) + return statusShutterSensor; + + if (InferredShutterPosition == SensorState.Indeterminate) + return statusShutterSensor; + + return InferredShutterPosition; + } + + internal void RequestHardwareStatus() + { + ControllerActions.RequestHardwareStatus(); + } + + public void ShutterMotorCurrentReceived(int current) + { + ShutterMotorCurrent = current; + CurrentState.ShutterMovementDetected(); + } + + public void ShutterDirectionReceived(ShutterDirection direction) + { + if (direction == ShutterDirection.Closing || direction == ShutterDirection.Opening) + ShutterMovementDirection = direction; + CurrentState.ShutterMovementDetected(); + } + + public void AllStop() + { + Log.Warn().Message("Emergency Stop requested").Write(); + ControllerActions.PerformEmergencyStop(); + } + + public void RotationDirectionReceived(RotationDirection direction) + { + AzimuthDirection = direction; + CurrentState.RotationDetected(); + } + + /// + /// Waits for the state machine to enter the Ready state. If the state is not reached within + /// the specified time limit, an exception is thrown. + /// + /// THe maximum amount of time to wait. + /// + /// Thrown if the state machine is not ready within the + /// allotted time. + /// + public void WaitForReady(TimeSpan timeout) + { + var signalled = InReadyState.WaitOne(timeout); + if (!signalled) + { + var message = $"State machine did not enter the ready state within the allotted time of {timeout}"; + Log.Error().Message(message).Write(); + throw new TimeoutException(message); + } + } + + public void RotateToAzimuthDegrees(double azimuth) + { + CurrentState.RotateToAzimuthDegrees(azimuth); + } + + public void OpenShutter() + { + CurrentState.OpenShutter(); + } + + public void CloseShutter() + { + CurrentState.CloseShutter(); + } + + public void RotateToHomePosition() + { + CurrentState.RotateToHomePosition(); + } + + public void SetUserOutputPins(Octet newState) + { + UserPins = newState; + CurrentState.SetUserOutputPins(newState); + } + + internal void ResetKeepAliveTimer() + { + Log.Debug().Message("Keep-alive timer reset").Write(); + KeepAliveCancellationSource?.Cancel(); // Cancel any previous timer + KeepAliveCancellationSource = new CancellationTokenSource(); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + PollStatusAfterKeepAliveIntervalAsync(KeepAliveCancellationSource.Token); // Do not await the result +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + + private async Task PollStatusAfterKeepAliveIntervalAsync(CancellationToken cancel) + { + await Task.Delay(Options.KeepAliveTimerInterval, cancel); + if (cancel.IsCancellationRequested) + { + Log.Debug("KeepAlive poll cancelled"); + return; + } + Log.Debug() + .Message("Keep-alive timer expired - generating status request") + .Write(); + CurrentState.RequestHardwareStatus(); + } + + #region State triggers + public void AzimuthEncoderTickReceived(int encoderPosition) + { + AzimuthEncoderPosition = encoderPosition; + CurrentState.RotationDetected(); + } + + public void HardwareStatusReceived(IHardwareStatus status) + { + HardwareStatus = status; + CurrentState.StatusUpdateReceived(status); + UpdateStatus(status); + } + #endregion State triggers + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/IControllerActions.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/IControllerActions.cs new file mode 100644 index 0000000..aa659af --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/IControllerActions.cs @@ -0,0 +1,49 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: IControllerActions.cs Last modified: 2018-03-28@00:57 by Tim Long + +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + public interface IControllerActions + { + /// + /// Requests that the controller send a status report on the current state of the hardware. + /// + void RequestHardwareStatus(); + + /// + /// Instructs the controller to immediately stop all movement. + /// + void PerformEmergencyStop(); + + /// + /// Requests that the controller rotate to the specified azimuth position. + /// + void RotateToAzimuth(int degreesClockwiseFromNorth); + + /// + /// Requests that the controller open the shutter. + /// + void OpenShutter(); + + /// + /// Requests that the controller close the shutter. + /// + void CloseShutter(); + + /// + /// Requests that the controller rotates to the azimuth that it considers to be the home position. + /// + void RotateToHomePosition(); + + /// + /// Sets the state of the user output pins. + /// + /// An octet with the state of the user pins in bits 0..3. + void SetUserOutputPins(Octet newState); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/IControllerState.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/IControllerState.cs new file mode 100644 index 0000000..a657dff --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/IControllerState.cs @@ -0,0 +1,78 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: IControllerState.cs Last modified: 2018-03-28@00:58 by Tim Long + +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + public interface IControllerState + { + string Name { get; } + + /// + /// Called once when the state it first entered, but after the previous state's + /// method has been called. + /// + void OnEnter(); + + /// + /// Called once when the state exits but before the next state's + /// method is called. + /// + void OnExit(); + + /// + /// Trigger: called to signal that dome rotation is detected. + /// This can be triggered by a dome rotation direction notification, + /// or by an azimuth encoder tick. States are not interested in the actual + /// encoder position, only that movement is detected. + /// + void RotationDetected(); + + /// + /// Trigger: called to signal that a shutter motor current measurement + /// has been received. + /// + void ShutterMovementDetected(); + + /// + /// Trigger: called when a status report is received from the controller. + /// + /// An object containing the current hardware state. + void StatusUpdateReceived(IHardwareStatus status); + + /// + /// Requests that the dome rotate to the specified azimuth in degrees, + /// measured from North clockwise. + /// + void RotateToAzimuthDegrees(double azimuth); + + /// + /// Action: Open Shutter + /// + void OpenShutter(); + + /// + /// Action: Close Shutter + /// + void CloseShutter(); + + /// + /// Action: requests that the dome is rotated to the home position. + /// + void RotateToHomePosition(); + + /// + /// Action: set the state of the user output pins + /// + void SetUserOutputPins(Octet newState); + + /// + /// Request a hardware status update. + /// + void RequestHardwareStatus(); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/Ready.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/Ready.cs new file mode 100644 index 0000000..2e6bbff --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/Ready.cs @@ -0,0 +1,80 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: Ready.cs Last modified: 2018-03-20@01:00 by Tim Long + +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal sealed class Ready : ControllerStateBase + { + public Ready(ControllerStateMachine machine) : base(machine) { } + + public override void OnEnter() + { + machine.ResetKeepAliveTimer(); + machine.InReadyState.Set(); + } + + public override void OnExit() + { + machine.InReadyState.Reset(); + machine.ResetKeepAliveTimer(); + } + + public override void RotationDetected() + { + machine.TransitionToState(new Rotating(machine)); + } + + public override void ShutterMovementDetected() + { + machine.TransitionToState(new ShutterMoving(machine)); + } + + public override void RotateToAzimuthDegrees(double azimuth) + { + base.RotateToAzimuthDegrees(azimuth); + machine.ControllerActions.RotateToAzimuth((int) azimuth); + machine.TransitionToState(new Rotating(machine)); + } + + public override void OpenShutter() + { + base.OpenShutter(); + machine.ShutterMovementDirection = ShutterDirection.Opening; + machine.ControllerActions.OpenShutter(); + machine.TransitionToState(new Rotating(machine)); + } + + public override void CloseShutter() + { + base.CloseShutter(); + machine.ShutterMovementDirection = ShutterDirection.Closing; + machine.ControllerActions.CloseShutter(); + machine.TransitionToState(new Rotating(machine)); + } + + public override void RotateToHomePosition() + { + base.RotateToHomePosition(); + machine.ControllerActions.RotateToHomePosition(); + machine.TransitionToState(new Rotating(machine)); + } + + public override void SetUserOutputPins(Octet newState) + { + base.SetUserOutputPins(newState); + machine.ControllerActions.SetUserOutputPins(newState); + } + + public override void RequestHardwareStatus() + { + base.RequestHardwareStatus(); + machine.ResetKeepAliveTimer(); + machine.RequestHardwareStatus(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/RequestStatus.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/RequestStatus.cs new file mode 100644 index 0000000..39ceb52 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/RequestStatus.cs @@ -0,0 +1,51 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: RequestStatus.cs Last modified: 2018-03-20@00:55 by Tim Long + +using NLog.Fluent; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal sealed class RequestStatus : ControllerStateBase + { + public RequestStatus(ControllerStateMachine machine) : base(machine) { } + + public override void OnEnter() + { + machine.RequestHardwareStatus(); + } + + public override void RotationDetected() + { + Log.Warn() + .Message("Rotation detected while expecting status. Issuing AllStop and re-requesting status.") + .Write(); + EmergencyStopAndRequestStatus(); + } + + private void EmergencyStopAndRequestStatus() + { + machine.AllStop(); + machine.RequestHardwareStatus(); + } + + /// + /// This trigger is not valid in this state, so we basically ignore it. + /// + public override void ShutterMovementDetected() + { + Log.Warn() + .Message("Shutter movement detected while expecting status. Issuing AllStop and re-requesting status.") + .Write(); + EmergencyStopAndRequestStatus(); + } + + public override void StatusUpdateReceived(IHardwareStatus status) + { + machine.TransitionToState(new Ready(machine)); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/Rotating.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/Rotating.cs new file mode 100644 index 0000000..a83dbbc --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/Rotating.cs @@ -0,0 +1,63 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: Rotating.cs Last modified: 2018-03-30@03:23 by Tim Long + +using System; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal sealed class Rotating : ControllerStateBase + { + private static readonly TimeSpan RotationTimeout = TimeSpan.FromSeconds(5); + + public Rotating(ControllerStateMachine machine) : base(machine) { } + + public override void OnEnter() + { + base.OnEnter(); + ResetTimeout(RotationTimeout); + machine.AzimuthMotorActive = true; + machine.AtHome = false; + } + + public override void OnExit() + { + base.OnExit(); + machine.AzimuthMotorActive = false; + machine.AzimuthDirection = RotationDirection.None; + } + + /// + /// Trigger: updates the encoder position + /// + public override void RotationDetected() => ResetTimeout(RotationTimeout); + + /// + /// Trigger: invalid for this state. + /// + public override void ShutterMovementDetected() + { + base.ShutterMovementDetected(); + machine.TransitionToState(new ShutterMoving(machine)); + } + + /// + /// Trigger: => Ready. + /// + /// + public override void StatusUpdateReceived(IHardwareStatus status) + { + CancelTimeout(); + machine.TransitionToState(new Ready(machine)); + } + + protected override void HandleTimeout() + { + base.HandleTimeout(); + machine.TransitionToState(new RequestStatus(machine)); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/RxControllerActions.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/RxControllerActions.cs new file mode 100644 index 0000000..1d0e7dc --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/RxControllerActions.cs @@ -0,0 +1,58 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: RxControllerActions.cs Last modified: 2018-03-24@22:27 by Tim Long + +using TA.Ascom.ReactiveCommunications; +using TA.DigitalDomeworks.SharedTypes; +using TI.DigitalDomeWorks; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + public class RxControllerActions : IControllerActions + { + private readonly ICommunicationChannel channel; + + public RxControllerActions(ICommunicationChannel channel) + { + this.channel = channel; + } + + public void RequestHardwareStatus() + { + channel.Send(Constants.CmdGetInfo); + } + + public void PerformEmergencyStop() + { + channel.Send(Constants.CmdEmergencyStop); + } + + public void RotateToAzimuth(int degreesClockwiseFromNorth) + { + var cmd = string.Format(Constants.CmdGotoAz, degreesClockwiseFromNorth); + channel.Send(cmd); + } + + public void OpenShutter() + { + channel.Send(Constants.CmdOpen); + } + + public void CloseShutter() + { + channel.Send(Constants.CmdClose); + } + + public void RotateToHomePosition() + { + channel.Send(Constants.CmdGotoHome); + } + + public void SetUserOutputPins(Octet newState) + { + channel.Send(string.Format(Constants.CmdSetUserPins, (byte)newState)); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/ShutterMoving.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/ShutterMoving.cs new file mode 100644 index 0000000..30bf65a --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/ShutterMoving.cs @@ -0,0 +1,103 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ShutterMoving.cs Last modified: 2018-03-20@00:56 by Tim Long + +using System; +using NLog.Fluent; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal sealed class ShutterMoving : ControllerStateBase + { + /// + /// If no shutter movement indications are received for this long, + /// the state will time out and attempt to request a status update. + /// + private static readonly TimeSpan shutterTimeout = TimeSpan.FromSeconds(5); + /// + /// Records the moment when movement was first detected. + /// + private DateTime movementStartTime; + /// + /// Starts at false and transitions to true when the current draw + /// exceeds a specified threshold. + /// + private bool currentDrawThresholdReached; + + public ShutterMoving(ControllerStateMachine machine) : base(machine) { } + + public override void OnEnter() + { + base.OnEnter(); + ResetTimeout(shutterTimeout); + movementStartTime = machine.Clock.GetCurrentTime(); + currentDrawThresholdReached = false; + machine.ShutterMotorActive = true; + } + + public override void OnExit() + { + base.OnExit(); + machine.ShutterMotorCurrent = 0; + machine.ShutterMotorActive = false; + machine.ShutterMovementDirection = ShutterDirection.None; + } + + public override void RotationDetected() + { + base.RotationDetected(); + Log.Error() + .Message($"Invalid trigger: {nameof(RotationDetected)}") + .Write(); + } + + public override void ShutterMovementDetected() + { + base.ShutterMovementDetected(); + ResetTimeout(shutterTimeout); + InferShutterState(machine.ShutterMotorCurrent); + } + + public override void StatusUpdateReceived(IHardwareStatus status) + { + base.StatusUpdateReceived(status); + CancelTimeout(); + machine.TransitionToState(new Ready(machine)); + } + + private void InferShutterState(int shutterMotorCurrent) + { + var timeNow = machine.Clock.GetCurrentTime(); + var elapsedMoveTime = timeNow - movementStartTime; + var elapsedSeconds = elapsedMoveTime.TotalSeconds; + var minimumRequiredMoveSeconds = machine.Options.MaximumShutterCloseTime.TotalSeconds / 2; + if (shutterMotorCurrent >= machine.Options.CurrentDrawDetectionThreshold) + currentDrawThresholdReached = true; + if (currentDrawThresholdReached && elapsedSeconds >= minimumRequiredMoveSeconds) + { + switch (machine.ShutterMovementDirection) + { + case ShutterDirection.Closing: + machine.InferredShutterPosition = SensorState.Closed; + break; + case ShutterDirection.Opening: + machine.InferredShutterPosition = SensorState.Open; + break; + } + } + else + { + machine.InferredShutterPosition = SensorState.Indeterminate; + } + } + + protected override void HandleTimeout() + { + base.HandleTimeout(); + machine.TransitionToState(new RequestStatus(machine)); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/StateLoggingDecorator.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/StateLoggingDecorator.cs new file mode 100644 index 0000000..8261dc3 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/StateLoggingDecorator.cs @@ -0,0 +1,119 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: StateLoggingDecorator.cs Last modified: 2018-03-30@02:02 by Tim Long + +using System.Diagnostics.Contracts; +using NLog.Fluent; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal sealed class StateLoggingDecorator : IControllerState + { + private readonly IControllerState decoratedState; + + public StateLoggingDecorator(IControllerState targetState) + { + decoratedState = targetState; + } + + [ContractInvariantMethod] + private void ObjectInvariant() + { + Contract.Invariant(decoratedState != null); + } + + public string Name => decoratedState.Name; + + public void RotateToAzimuthDegrees(double azimuth) + { + Log.Info() + .Message($"Rotate to azimuth {azimuth}° requested") + .Write(); + decoratedState.RotateToAzimuthDegrees(azimuth); + } + + public void OpenShutter() + { + Log.Info() + .Message("Open Shutter requested") + .Write(); + decoratedState.OpenShutter(); + } + + public void CloseShutter() + { + Log.Info() + .Message("Close Shutter requested") + .Write(); + decoratedState.CloseShutter(); + } + + public void RotateToHomePosition() + { + Log.Info() + .Message("Rotate To Home requested") + .Write(); + decoratedState.RotateToHomePosition(); + } + + public void SetUserOutputPins(Octet newState) + { + Log.Info() + .Message($"User output pins set to {newState}") + .Write(); + decoratedState.SetUserOutputPins(newState); + } + + public void OnEnter() + { + Log.Info() + .Message($"Entering state {decoratedState.Name}") + .Write(); + decoratedState.OnEnter(); + } + + public void OnExit() + { + Log.Info() + .Message($"Exiting state {decoratedState.Name}") + .Write(); + decoratedState.OnExit(); + } + + public void RotationDetected() + { + Log.Info() + .Message($"[{decoratedState.Name}] Trigger: Rotation detected") + .Write(); + decoratedState.RotationDetected(); + } + + public void ShutterMovementDetected() + { + Log.Info() + .Message($"[{decoratedState.Name}] Trigger: Shutter movement detected") + .Write(); + decoratedState.ShutterMovementDetected(); + } + + public void StatusUpdateReceived(IHardwareStatus status) + { + Log.Info() + .Message($"[{decoratedState.Name}] Trigger: Status update") + .Property("status", status) + .Write(); + decoratedState.StatusUpdateReceived(status); + } + + public void RequestHardwareStatus() + { + Log.Info() + .Message($"[{decoratedState.Name}] Action: Request hardware status") + .Write(); + decoratedState.RequestHardwareStatus(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StateMachine/Uninitialized.cs b/TA.DigitalDomeworks.DeviceInterface/StateMachine/Uninitialized.cs new file mode 100644 index 0000000..3b1a111 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/StateMachine/Uninitialized.cs @@ -0,0 +1,71 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: Uninitialized.cs Last modified: 2018-03-18@16:59 by Tim Long + +using System; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.DeviceInterface.StateMachine + { + internal class Uninitialized : IControllerState + { + private readonly InvalidOperationException uninitialized = + new InvalidOperationException("Call Initialize() before using the state machine"); + + public void OnEnter() + { + throw uninitialized; + } + + public void OnExit() { } + + public void RotationDetected() + { + throw uninitialized; + } + + public void ShutterMovementDetected() + { + throw uninitialized; + } + + public void StatusUpdateReceived(IHardwareStatus status) + { + throw uninitialized; + } + + public string Name => nameof(Uninitialized); + + public void RotateToAzimuthDegrees(double azimuth) + { + throw uninitialized; + } + + public void OpenShutter() + { + throw uninitialized; + } + + public void CloseShutter() + { + throw uninitialized; + } + + public void RotateToHomePosition() + { + throw uninitialized; + } + + public void SetUserOutputPins(Octet newState) + { + throw uninitialized; + } + + public void RequestHardwareStatus() + { + throw uninitialized; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/StatusTransaction.cs b/TA.DigitalDomeworks.DeviceInterface/StatusTransaction.cs index 922ad44..7aa8205 100644 --- a/TA.DigitalDomeworks.DeviceInterface/StatusTransaction.cs +++ b/TA.DigitalDomeworks.DeviceInterface/StatusTransaction.cs @@ -16,7 +16,7 @@ public StatusTransaction(ControllerStatusFactory factory) : base(Constants.CmdGe this.factory = factory; } - public IControllerStatus ControllerStatus { get; private set; } + public IHardwareStatus HardwareStatus { get; private set; } public override void ObserveResponse(IObservable source) { @@ -33,7 +33,7 @@ protected override void OnCompleted() { var responseString = Response.Single(); var status = factory.FromStatusPacket(responseString); - ControllerStatus = status; + HardwareStatus = status; } base.OnCompleted(); } diff --git a/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.csproj b/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.csproj index ca65ac5..1055cb1 100644 --- a/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.csproj +++ b/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.csproj @@ -1,7 +1,46 @@  - + + + + True + False + True + False + False + True + True + True + True + True + True + True + True + True + False + True + False + False + False + True + True + True + True + False + False + False + False + True + Full + 0 + True + True + True + True + Build + True + Debug AnyCPU @@ -20,7 +59,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG prompt 4 @@ -37,25 +76,27 @@ ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll - ..\packages\NLog.4.4.13\lib\net45\NLog.dll + ..\packages\NLog.4.5.0\lib\net45\NLog.dll - - ..\packages\NodaTime.2.2.4\lib\net45\NodaTime.dll + + ..\packages\PostSharp.Redist.5.0.48\lib\net45\PostSharp.dll - - ..\packages\PostSharp.Redist.5.0.46\lib\net45\PostSharp.dll + + ..\packages\PostSharp.Patterns.Aggregation.Redist.5.0.48\lib\net45\PostSharp.Patterns.Aggregation.dll - - ..\packages\PostSharp.Patterns.Aggregation.Redist.5.0.46\lib\net45\PostSharp.Patterns.Aggregation.dll + + ..\packages\PostSharp.Patterns.Common.Redist.5.0.48\lib\net46\PostSharp.Patterns.Common.dll - - ..\packages\PostSharp.Patterns.Common.Redist.5.0.46\lib\net46\PostSharp.Patterns.Common.dll + + ..\packages\PostSharp.Patterns.Model.Redist.5.0.48\lib\net40\PostSharp.Patterns.Model.dll - - ..\packages\PostSharp.Patterns.Model.Redist.5.0.46\lib\net40\PostSharp.Patterns.Model.dll + + ..\packages\PostSharp.Patterns.Threading.Redist.5.0.48\lib\net45\PostSharp.Patterns.Threading.dll + + ..\packages\System.Reactive.Core.3.1.1\lib\net46\System.Reactive.Core.dll @@ -71,6 +112,9 @@ ..\packages\System.Reactive.Windows.Threading.3.1.1\lib\net45\System.Reactive.Windows.Threading.dll + + + @@ -78,26 +122,44 @@ - - ..\packages\TA.Ascom.ReactiveCommunications.0.5.1\lib\net45\TA.Ascom.ReactiveCommunications.dll + + ..\packages\TA.Ascom.ReactiveCommunications.1.0.0\lib\net45\TA.Ascom.ReactiveCommunications.dll + + + + + + + + + + + + + + + + {86B17C99-41B6-4611-AD1D-26B7D6C70A22} + TA.DigitalDomeworks.HardwareSimulator + {adbb1165-e995-4c75-8de9-1dbffcf34d6f} TA.DigitalDomeworks.SharedTypes @@ -109,8 +171,11 @@ 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/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.v3.ncrunchproject b/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.v3.ncrunchproject new file mode 100644 index 0000000..e8c1f79 --- /dev/null +++ b/TA.DigitalDomeworks.DeviceInterface/TA.DigitalDomeworks.DeviceInterface.v3.ncrunchproject @@ -0,0 +1,7 @@ + + + + CodeContractsReferenceBuildsAreEnabled + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.DeviceInterface/packages.config b/TA.DigitalDomeworks.DeviceInterface/packages.config index 1f9b3e4..262f3c5 100644 --- a/TA.DigitalDomeworks.DeviceInterface/packages.config +++ b/TA.DigitalDomeworks.DeviceInterface/packages.config @@ -1,16 +1,18 @@  + - - - - - - - - - - + + + + + + + + + + + @@ -22,5 +24,5 @@ - + \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/SimulatorCommunicationsChannel.cs b/TA.DigitalDomeworks.HardwareSimulator/SimulatorCommunicationsChannel.cs new file mode 100644 index 0000000..74185bd --- /dev/null +++ b/TA.DigitalDomeworks.HardwareSimulator/SimulatorCommunicationsChannel.cs @@ -0,0 +1,72 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: SimulatorCommunicationsChannel.cs Last modified: 2018-03-28@22:45 by Tim Long + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using TA.Ascom.ReactiveCommunications; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.HardwareSimulator + { + /// + /// Provides a direct in-memory communications link to the hardware simulator. + /// + public class SimulatorCommunicationsChannel : ICommunicationChannel + { + private readonly SimulatorStateMachine simulator; + + /// + /// Creates a simulator communications channel from a valid endpoint. + /// + /// A valid simulator endpoint. + public SimulatorCommunicationsChannel(SimulatorEndpoint endpoint) + { + Contract.Requires(endpoint != null); + Endpoint = endpoint; + simulator = new SimulatorStateMachine(endpoint.Realtime, new SystemDateTimeUtcClock()); + } + + /// + /// Keeps a log of all commands sent to the simulator. + /// + public List SendLog { get; } = new List(); + + /// + public void Dispose() + { + simulator?.InputObserver.OnCompleted(); + } + + /// + public void Open() + { + IsOpen = true; + } + + /// + public void Close() + { + IsOpen = false; + } + + /// + public void Send(string txData) + { + SendLog.Add(txData); + foreach (var c in txData) simulator.InputObserver.OnNext(c); + } + + /// + public IObservable ObservableReceivedCharacters => simulator.ObservableResponses; + + /// + public bool IsOpen { get; private set; } + + /// + public DeviceEndpoint Endpoint { get; } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/SimulatorEndpoint.cs b/TA.DigitalDomeworks.HardwareSimulator/SimulatorEndpoint.cs new file mode 100644 index 0000000..e81b166 --- /dev/null +++ b/TA.DigitalDomeworks.HardwareSimulator/SimulatorEndpoint.cs @@ -0,0 +1,97 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: SimulatorEndpoint.cs Last modified: 2018-03-28@15:51 by Tim Long + +using System; +using System.Diagnostics.Contracts; +using System.Text.RegularExpressions; +using NLog.Fluent; +using TA.Ascom.ReactiveCommunications; + +namespace TA.DigitalDomeworks.HardwareSimulator + { + /// + /// Endpoint representing the hardware simulator. + /// Connection string format: Simulator:Realtime or Simulator:Fast + /// + public class SimulatorEndpoint : DeviceEndpoint + { + private const string realtime = "Realtime"; + private const string fast = "Fast"; + private const string connectionPattern = @"^Simulator(:(?(Realtime)|(Fast)))?$"; + private static readonly Regex connectionRegex = new Regex(connectionPattern, Options); + private string connectionString; + + private SimulatorEndpoint(string connectionString) + { + this.connectionString = connectionString; + } + + /// + /// When true, delays are added to simulate realtime operation. + /// When false, all operations complete nearly instantly. This is + /// useful for unit testing. + /// + public bool Realtime { get; set; } + + /// + /// Creates a simulator endpoint from a valid simulator connection string. + /// + /// A valid simulator connection string. + /// Creates and returns an endpoint object for the connection string. + /// + /// Thrown if the connection string is empty or invalid. + /// + public static SimulatorEndpoint FromConnectionString(string connection) + { + Contract.Requires(!string.IsNullOrWhiteSpace(connection)); + Contract.Ensures(Contract.Result() != null); + var matches = connectionRegex.Match(connection); + if (!matches.Success) + throw new ArgumentException($"The connection string '{connection}' is not valid for the simulator", + nameof(connection)); + var speed = CaptureGroupOrDefault(matches, "Speed", "Fast"); + var timing = speed.Equals(realtime, StringComparison.InvariantCultureIgnoreCase); + return new SimulatorEndpoint(connection) {Realtime = timing}; + } + + /// + /// Tests whether a connection string is valid for the simulator. + /// + /// + /// + /// true if the connection string is valid; false otherwise. + /// + [Pure] + public static bool IsConnectionStringValid(string connection) + { + Contract.Requires(connection != null); + try + { + return connectionRegex.IsMatch(connection); + } + catch (RegexMatchTimeoutException ex) + { + Log.Error() + .Message("Regex match timeout when validating simulator connection string {connection}") + .Exception(ex) + .Property("connection", connection) + .Property(nameof(connectionRegex), connectionRegex.ToString()) + .Write(); + return false; + } + } + + /// + /// Gets the connection string for this endpoint. + /// + /// + public override string ToString() + { + Contract.Ensures(IsConnectionStringValid(Contract.Result())); + return $"Simulator:{(Realtime ? realtime : fast)}"; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/SimulatorState.cs b/TA.DigitalDomeworks.HardwareSimulator/SimulatorState.cs index 899ef36..bf4cba4 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/SimulatorState.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/SimulatorState.cs @@ -6,6 +6,7 @@ // Last modified: 2016-06-21@10:01 by Tim using System; +using System.Diagnostics.Contracts; using NLog; namespace TA.DigitalDomeworks.HardwareSimulator @@ -20,7 +21,11 @@ public class SimulatorState /// Provides logging services /// protected static readonly Logger Log = LogManager.GetCurrentClassLogger(); - protected readonly SimulatorStateMachine machine; + + /// + /// A reference to the state machine that created the state. + /// + protected readonly SimulatorStateMachine Machine; /// /// Initializes the simulator state with a reference to the parent state machine. @@ -28,7 +33,7 @@ public class SimulatorState /// The associated state machine. internal SimulatorState(SimulatorStateMachine machine) { - this.machine = machine; + this.Machine = machine; } /// @@ -131,11 +136,11 @@ public static void Transition(SimulatorState newState) /// Raises the event. /// /// The instance containing the event data. + /// A delegate callback throws an exception. private static void RaiseStateChanged(StateEventArgs e) { - var handler = StateChanged; - if (handler != null) - handler(e); + Contract.Requires(e != null); + StateChanged?.Invoke(e); } #endregion } diff --git a/TA.DigitalDomeworks.HardwareSimulator/SimulatorStateMachine.cs b/TA.DigitalDomeworks.HardwareSimulator/SimulatorStateMachine.cs index 3f1dfe1..37267cb 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/SimulatorStateMachine.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/SimulatorStateMachine.cs @@ -1,18 +1,17 @@ -// This file is part of the TI.DigitalDomeWorks project +// This file is part of the TA.DigitalDomeworks project // -// Copyright © 2016 TiGra Astronomy, all rights reserved. +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: SimulatorStateMachine.cs Created: 2016-06-20@18:14 -// Last modified: 2016-09-11@00:43 by Tim +// File: SimulatorStateMachine.cs Last modified: 2018-03-28@17:49 by Tim Long using System; +using System.Diagnostics.Contracts; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Text; using System.Threading; using NLog; -using NodaTime; using TA.DigitalDomeworks.SharedTypes; namespace TA.DigitalDomeworks.HardwareSimulator @@ -30,13 +29,13 @@ public sealed class SimulatorStateMachine private readonly Logger log = LogManager.GetCurrentClassLogger(); private readonly Subject receiveSubject = new Subject(); private readonly IDisposable receiveSubscription; - private readonly SystemClock timeSource = SystemClock.Instance; + private readonly IClock timeSource; private readonly Subject transmitSubject = new Subject(); /// /// Stores all of the parameters for the simulated hardware status. /// - internal ControllerStatus HardwareStatus; + internal HardwareStatus HardwareStatus; /// /// Characters received from the serial port, which accumulate until a valid command has been received. @@ -55,18 +54,19 @@ public sealed class SimulatorStateMachine /// /// Initializes a new instance of the class. /// - /// A pre-configured and opened instance. - /// The state in which to start. /// /// When true the simulator introduces pauses that are representative of real equipment. /// When false, the simulation proceeds at an accelerated pace with no pauses. /// - public SimulatorStateMachine(bool realTime) + /// A source of the current time. + public SimulatorStateMachine(bool realTime, IClock timeSource) { + Contract.Requires(timeSource != null); RealTime = realTime; + this.timeSource = timeSource; DomeSupportRingOpen = false; ShutterStuck = false; - HardwareStatus = new ControllerStatus + HardwareStatus = new HardwareStatus { AtHome = false, Coast = 1, @@ -86,7 +86,7 @@ public SimulatorStateMachine(bool realTime) Snow = 255, // For weather items, 255 means no data Temperature = 255, - TimeStamp = timeSource.GetCurrentInstant(), + TimeStamp = timeSource.GetCurrentTime(), UserPins = 0, WeatherAge = 128, WindDirection = 255, @@ -102,8 +102,16 @@ public SimulatorStateMachine(bool realTime) receiveSubscription = receiveObservable.Subscribe(InputStimulus, EndOfSimulation); } + /// + /// An observable sequence of characters that simulates data arriving from + /// the dome controller to the PC serial port. + /// public IObservable ObservableResponses => transmitSubject.AsObservable(); + /// + /// Simulate sending characters to the dome controller by calling the observer's + /// method. + /// public IObserver InputObserver => receiveSubject.AsObserver(); /// @@ -136,7 +144,7 @@ internal int TargetAzimuthDegrees /// /// The azimuth. /// - /// When setting the value, the result is 'wrapped' at . + /// When setting the value, the result is 'wrapped' at . /// internal int AzimuthTicks { @@ -219,8 +227,8 @@ internal void WriteLine(string txData) /// /// The azimuth. /// - /// true if the azimuth is between - /// and . + /// true if the azimuth is between + /// and . /// public bool InHomeRange(int azimuth) { @@ -293,9 +301,7 @@ public void Dispose() internal void InvokeMotorConfigurationChanged(MotorConfigurationEventArgs e) { - var handler = MotorConfigurationChanged; - if (handler != null) - handler(null, e); + MotorConfigurationChanged?.Invoke(this, e); } /// @@ -312,9 +318,7 @@ internal void InvokeMotorConfigurationChanged(MotorConfigurationEventArgs e) /// internal void InvokeAzimuthChanged(AzimuthChangedEventArgs e) { - var handler = AzimuthChanged; - if (handler != null) - handler(null, e); + AzimuthChanged?.Invoke(this,e); } /// @@ -328,9 +332,7 @@ internal void InvokeAzimuthChanged(AzimuthChangedEventArgs e) /// The instance containing the event data. public void InvokeReceivedData(EventArgs e) { - var handler = ReceivedData; - if (handler != null) - handler(null, e); + ReceivedData?.Invoke(this, e); } /// @@ -344,10 +346,14 @@ public void InvokeReceivedData(EventArgs e) /// The instance containing the event data. private static void InvokeSentData(EventArgs e) { - var handler = SentData; - if (handler != null) - handler(null, e); + SentData?.Invoke(null, e); } #endregion + + [ContractInvariantMethod] + private void ObjectInvariant() + { + Contract.Invariant(InReadyState != null); + } } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateEmergencyStop.cs b/TA.DigitalDomeworks.HardwareSimulator/StateEmergencyStop.cs index 934a2a6..6275bbd 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateEmergencyStop.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateEmergencyStop.cs @@ -32,9 +32,9 @@ internal StateEmergencyStop(SimulatorStateMachine machine) : base(machine) { } public override void OnEnter() { base.OnEnter(); - machine.InvokeMotorConfigurationChanged(MotorConfigurationEventArgs.AllStopped); + Machine.InvokeMotorConfigurationChanged(MotorConfigurationEventArgs.AllStopped); Thread.Sleep(1000); - Transition(new StateSendStatus(machine)); + Transition(new StateSendStatus(Machine)); } /// diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateExecutingCommand.cs b/TA.DigitalDomeworks.HardwareSimulator/StateExecutingCommand.cs index 97efd9b..c9aab95 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateExecutingCommand.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateExecutingCommand.cs @@ -24,17 +24,17 @@ protected internal StateExecutingCommand(SimulatorStateMachine machine) : base(m public override void OnEnter() { base.OnEnter(); - Log.Debug($"Processing command [{machine.ReceivedChars}]"); - if (machine.ReceivedChars.Length == 0) + Log.Debug($"Processing command [{Machine.ReceivedChars}]"); + if (Machine.ReceivedChars.Length == 0) { Log.Warn("Nothing received"); - Transition(new StateReceivingCommand(machine)); + Transition(new StateReceivingCommand(Machine)); } - if (machine.ReceivedChars.Length != 4) + if (Machine.ReceivedChars.Length != 4) { Log.Warn("Received string not 4 characters"); - Transition(new StateReceivingCommand(machine)); + Transition(new StateReceivingCommand(Machine)); } ExecuteCommand(); @@ -51,7 +51,7 @@ public override void Stimulus(char value) if (value == (char) AsciiSymbols.CR) return; // Ignore carriage returns - Transition(new StateEmergencyStop(machine)); + Transition(new StateEmergencyStop(Machine)); } /// @@ -72,24 +72,24 @@ public override void Stimulus(char value) /// private void ExecuteCommand() { - var command = machine.ReceivedChars.ToString(); + var command = Machine.ReceivedChars.ToString(); switch (command) { case Constants.CmdCancelTimeout: return; case Constants.CmdClose: - Transition(new StateRotatingForShutterClose(machine)); + Transition(new StateRotatingForShutterClose(Machine)); return; case Constants.CmdFastTrack: return; case Constants.CmdGetInfo: - Transition(new StateSendStatus(machine)); + Transition(new StateSendStatus(Machine)); return; case Constants.CmdGotoHome: - Transition(new StateRotatingToHome(machine)); + Transition(new StateRotatingToHome(Machine)); return; case Constants.CmdOpen: - Transition(new StateRotatingForShutterOpen(machine)); + Transition(new StateRotatingForShutterOpen(Machine)); return; case Constants.CmdPark: return; @@ -118,11 +118,11 @@ private void ExecuteCommand() out pins)) { // Successfully parsed a user I/O pin packet. - machine.HardwareStatus.UserPins = pins; - Transition(new StateSendStatus(machine)); + Machine.HardwareStatus.UserPins = pins; + Transition(new StateSendStatus(Machine)); } } - else if (machine.ReceivedChars[0] == 'G') + else if (Machine.ReceivedChars[0] == 'G') { int targetDegrees; if (int.TryParse(command.Substring(1, 3), @@ -131,19 +131,19 @@ private void ExecuteCommand() out targetDegrees)) { // Successfully recognised a rotation command. - if (machine.InDeadZone(targetDegrees)) + if (Machine.InDeadZone(targetDegrees)) { - Transition(new StateSendStatus(machine)); + Transition(new StateSendStatus(Machine)); return; } - machine.TargetAzimuthDegrees = targetDegrees; - Transition(new StateRotating(machine)); + Machine.TargetAzimuthDegrees = targetDegrees; + Transition(new StateRotating(Machine)); } else { Log.Warn("Gxxx command had invalid numeric part (ignored)"); - Transition(new StateReceivingCommand(machine)); + Transition(new StateReceivingCommand(Machine)); } } //ToDo: Smart Tracking will likely be implemented in its own state. @@ -155,7 +155,7 @@ private void ExecuteCommand() else { // Anything else must be an invalid or corrupted command, so discard it. - Transition(new StateReceivingCommand(machine)); + Transition(new StateReceivingCommand(Machine)); } } } diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateReceivingCommand.cs b/TA.DigitalDomeworks.HardwareSimulator/StateReceivingCommand.cs index b15b36b..0c60ac2 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateReceivingCommand.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateReceivingCommand.cs @@ -29,13 +29,13 @@ internal StateReceivingCommand(SimulatorStateMachine machine) : base(machine) { public override void Stimulus(char value) { base.Stimulus(value); - machine.ReceivedChars.Append(value); - machine.InvokeReceivedData(EventArgs.Empty); - if (machine.ReceivedChars.Length >= 4) // All DDW commands are 4 characters. + Machine.ReceivedChars.Append(value); + Machine.InvokeReceivedData(EventArgs.Empty); + if (Machine.ReceivedChars.Length >= 4) // All DDW commands are 4 characters. { - Transition(new StateExecutingCommand(machine)); + Transition(new StateExecutingCommand(Machine)); } - else if (machine.RealTime) + else if (Machine.RealTime) { // Reset the inter-character timeout. interCharTimeout.Stop(); @@ -49,8 +49,8 @@ public override void Stimulus(char value) /// public override void OnExit() { - machine.InReadyState.Reset(); // Signal dependent threads that they have to wait for us. - if (machine.RealTime) + Machine.InReadyState.Reset(); // Signal dependent threads that they have to wait for us. + if (Machine.RealTime) { interCharTimeout.Stop(); // Ensure the timeout timer is stopped. interCharTimeout.Elapsed -= InterCharTimeoutElapsed; // Withdraw our delegate. @@ -66,10 +66,10 @@ public override void OnExit() public override void OnEnter() { base.OnEnter(); - machine.ReceivedChars.Clear(); - if (machine.RealTime) + Machine.ReceivedChars.Clear(); + if (Machine.RealTime) interCharTimeout.Elapsed += InterCharTimeoutElapsed; - machine.InReadyState.Set(); // Signal waiting threads that we are ready to rock. + Machine.InReadyState.Set(); // Signal waiting threads that we are ready to rock. } /// diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateRotating.cs b/TA.DigitalDomeworks.HardwareSimulator/StateRotating.cs index a0c5b7a..65f485a 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateRotating.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateRotating.cs @@ -6,6 +6,7 @@ using System; using System.Timers; +using TA.DigitalDomeworks.SharedTypes; namespace TA.DigitalDomeworks.HardwareSimulator { @@ -34,7 +35,7 @@ public override void OnEnter() { base.OnEnter(); int target; - var azimuth = machine.HardwareStatus.CurrentAzimuth; + var azimuth = Machine.HardwareStatus.CurrentAzimuth; /* * If the Dome Support Ring is open and we are in the Home position, @@ -42,17 +43,17 @@ public override void OnEnter() * the target azimuth (i.e. no rotation required) then we allow the operation * to continue, so that shutter operations can complete even when DSR is open. */ - if (machine.DomeSupportRingOpen) - if (machine.InHomeRange(azimuth) && azimuth != machine.TargetAzimuthTicks) + if (Machine.DomeSupportRingOpen) + if (Machine.InHomeRange(azimuth) && azimuth != Machine.TargetAzimuthTicks) { - Transition(new StateSendStatus(machine)); + Transition(new StateSendStatus(Machine)); return; } - if (machine.TargetAzimuthTicks < machine.HardwareStatus.CurrentAzimuth) - target = machine.TargetAzimuthTicks + 360; + if (Machine.TargetAzimuthTicks < Machine.HardwareStatus.CurrentAzimuth) + target = Machine.TargetAzimuthTicks + 360; else - target = machine.TargetAzimuthTicks; + target = Machine.TargetAzimuthTicks; var clockwiseDistance = target - azimuth; var counterclockwiseDistance = 360 - clockwiseDistance; @@ -72,14 +73,14 @@ public override void OnEnter() direction = RotationDirection.Clockwise; azimuthMotorConfiguration = MotorConfiguration.Forward; delta = clockwiseDistance; - machine.WriteLine("R"); + Machine.WriteLine("R"); } else { direction = RotationDirection.CounterClockwise; azimuthMotorConfiguration = MotorConfiguration.Reverse; delta = counterclockwiseDistance; - machine.WriteLine("L"); + Machine.WriteLine("L"); } Log.Debug("Rotating {0} delta={1} ticks", direction, delta); @@ -88,8 +89,8 @@ public override void OnEnter() AzimuthMotor = azimuthMotorConfiguration, ShutterMotor = MotorConfiguration.Stopped }; - machine.InvokeMotorConfigurationChanged(motorEventArgs); - rotationTimer.Interval = machine.RealTime + Machine.InvokeMotorConfigurationChanged(motorEventArgs); + rotationTimer.Interval = Machine.RealTime ? Properties.Settings.Default.RotationRateMsPerTick : 1; rotationTimer.Elapsed += RotationTimerElapsed; @@ -105,8 +106,8 @@ public override void OnExit() rotationTimer.Stop(); rotationTimer.Elapsed -= RotationTimerElapsed; // Set the AtHome property. - machine.HardwareStatus.AtHome = - machine.InHomeRange(machine.HardwareStatus.CurrentAzimuth); + Machine.HardwareStatus.AtHome = + Machine.InHomeRange(Machine.HardwareStatus.CurrentAzimuth); } /// @@ -117,7 +118,7 @@ public override void OnExit() public override void Stimulus(char value) { base.Stimulus(value); - Transition(new StateEmergencyStop(machine)); + Transition(new StateEmergencyStop(Machine)); } /// @@ -127,7 +128,7 @@ public override void Stimulus(char value) /// The instance containing the event data. private void RotationTimerElapsed(object sender, ElapsedEventArgs e) { - if (machine.HardwareStatus.CurrentAzimuth == machine.TargetAzimuthTicks) + if (Machine.HardwareStatus.CurrentAzimuth == Machine.TargetAzimuthTicks) { rotationTimer.Stop(); TransitionToNextState(); @@ -138,10 +139,10 @@ private void RotationTimerElapsed(object sender, ElapsedEventArgs e) switch (direction) { case RotationDirection.CounterClockwise: - newAzimuth = machine.HardwareStatus.CurrentAzimuth - 1; + newAzimuth = Machine.HardwareStatus.CurrentAzimuth - 1; break; case RotationDirection.Clockwise: - newAzimuth = machine.HardwareStatus.CurrentAzimuth + 1; + newAzimuth = Machine.HardwareStatus.CurrentAzimuth + 1; break; default: throw new ArgumentOutOfRangeException(); @@ -150,17 +151,17 @@ private void RotationTimerElapsed(object sender, ElapsedEventArgs e) // When wrapping the azimuth value, remember that if the circumference is 360 encoder ticks, // then those are numbered 0 to 359, so the highest azimuth position is actually one less // than the circumference. - if (newAzimuth >= machine.HardwareStatus.DomeCircumference) + if (newAzimuth >= Machine.HardwareStatus.DomeCircumference) newAzimuth = 0; if (newAzimuth < 0) - newAzimuth = machine.HardwareStatus.DomeCircumference - 1; - machine.HardwareStatus.CurrentAzimuth = newAzimuth; - machine.SetAzimuthDependentSensorsAndStates(); + newAzimuth = Machine.HardwareStatus.DomeCircumference - 1; + Machine.HardwareStatus.CurrentAzimuth = newAzimuth; + Machine.SetAzimuthDependentSensorsAndStates(); var tx = string.Format("P{0:D3}", newAzimuth); Log.Debug("=>{0}", tx); - machine.WriteLine(tx); - machine.InvokeAzimuthChanged(new AzimuthChangedEventArgs {NewAzimuth = newAzimuth}); + Machine.WriteLine(tx); + Machine.InvokeAzimuthChanged(new AzimuthChangedEventArgs {NewAzimuth = newAzimuth}); rotationTimer.Start(); // Prime the pump for the next tick. } } @@ -171,7 +172,7 @@ private void RotationTimerElapsed(object sender, ElapsedEventArgs e) /// protected virtual void TransitionToNextState() { - Transition(new StateSendStatus(machine)); + Transition(new StateSendStatus(Machine)); } } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterClose.cs b/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterClose.cs index a67d971..8cbbf6c 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterClose.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterClose.cs @@ -22,7 +22,7 @@ internal StateRotatingForShutterClose(SimulatorStateMachine machine) : base(mach /// protected override void TransitionToNextState() { - Transition(new StateShutterClosing(machine)); + Transition(new StateShutterClosing(Machine)); } /// @@ -30,7 +30,7 @@ protected override void TransitionToNextState() /// public override void OnEnter() { - machine.TargetAzimuthTicks = machine.HardwareStatus.HomePosition; + Machine.TargetAzimuthTicks = Machine.HardwareStatus.HomePosition; base.OnEnter(); } } diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterOpen.cs b/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterOpen.cs index 71bf1fe..357bdbd 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterOpen.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateRotatingForShutterOpen.cs @@ -22,7 +22,7 @@ internal StateRotatingForShutterOpen(SimulatorStateMachine machine) : base(machi /// protected override void TransitionToNextState() { - Transition(new StateShutterOpening(machine)); + Transition(new StateShutterOpening(Machine)); } /// @@ -30,7 +30,7 @@ protected override void TransitionToNextState() /// public override void OnEnter() { - machine.TargetAzimuthTicks = machine.HardwareStatus.HomePosition; + Machine.TargetAzimuthTicks = Machine.HardwareStatus.HomePosition; base.OnEnter(); } } diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateRotatingToHome.cs b/TA.DigitalDomeworks.HardwareSimulator/StateRotatingToHome.cs index d09023d..f72686c 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateRotatingToHome.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateRotatingToHome.cs @@ -15,7 +15,7 @@ internal StateRotatingToHome(SimulatorStateMachine machine) : base(machine) { } public override void OnEnter() { - machine.TargetAzimuthTicks = machine.HardwareStatus.HomePosition; + Machine.TargetAzimuthTicks = Machine.HardwareStatus.HomePosition; base.OnEnter(); } } diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateSendStatus.cs b/TA.DigitalDomeworks.HardwareSimulator/StateSendStatus.cs index 88e3ba6..3f934e1 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateSendStatus.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateSendStatus.cs @@ -26,11 +26,11 @@ protected internal StateSendStatus(SimulatorStateMachine machine) : base(machine public override void OnEnter() { base.OnEnter(); - machine.InvokeMotorConfigurationChanged(MotorConfigurationEventArgs.AllStopped); - if (machine.RealTime) + Machine.InvokeMotorConfigurationChanged(MotorConfigurationEventArgs.AllStopped); + if (Machine.RealTime) { pauseTimer.Elapsed += PauseTimerElapsed; - pauseTimer.Interval = machine.RealTime ? 500 : 1; + pauseTimer.Interval = Machine.RealTime ? 500 : 1; pauseTimer.Start(); } else @@ -53,14 +53,14 @@ private void PauseTimerElapsed(object sender, ElapsedEventArgs e) private void SendStatus() { // Update state that could have been affected by the last operation. - machine.SetAzimuthDependentSensorsAndStates(); - machine.WriteLine(machine.HardwareStatus.ToString()); - Transition(new StateReceivingCommand(machine)); + Machine.SetAzimuthDependentSensorsAndStates(); + Machine.WriteLine(Machine.HardwareStatus.ToString()); + Transition(new StateReceivingCommand(Machine)); } public override void OnExit() { - if (machine.RealTime) + if (Machine.RealTime) { pauseTimer.Stop(); pauseTimer.Elapsed -= PauseTimerElapsed; diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateShutterClosing.cs b/TA.DigitalDomeworks.HardwareSimulator/StateShutterClosing.cs index 4fcc1ed..1878b4b 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateShutterClosing.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateShutterClosing.cs @@ -27,8 +27,8 @@ public StateShutterClosing(SimulatorStateMachine machine) : base(machine, MotorC /// public override void OnEnter() { - if (machine.SimulatedShutterSensor == SensorState.Closed || machine.ShutterStuck) - Transition(new StateSendStatus(machine)); + if (Machine.SimulatedShutterSensor == SensorState.Closed || Machine.ShutterStuck) + Transition(new StateSendStatus(Machine)); else base.OnEnter(); } @@ -36,11 +36,11 @@ public override void OnEnter() public override void OnExit() { base.OnExit(); - if (!machine.ShutterStuck) - machine.SimulatedShutterSensor = ShutterTicksRemaining > 0 + if (!Machine.ShutterStuck) + Machine.SimulatedShutterSensor = ShutterTicksRemaining > 0 ? SensorState.Indeterminate : SensorState.Closed; - machine.HardwareStatus.ShutterSensor = machine.SimulatedShutterSensor; + Machine.HardwareStatus.ShutterSensor = Machine.SimulatedShutterSensor; } } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateShutterMoving.cs b/TA.DigitalDomeworks.HardwareSimulator/StateShutterMoving.cs index 9284456..c85a9dd 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateShutterMoving.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateShutterMoving.cs @@ -1,9 +1,8 @@ -// This file is part of the TI.DigitalDomeWorks project +// This file is part of the TA.DigitalDomeworks project // -// Copyright © 2014 TiGra Astronomy, all rights reserved. +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: StateShutterMoving.cs Created: 2014-10-05@00:56 -// Last modified: 2014-11-12@05:56 by Tim +// File: StateShutterMoving.cs Last modified: 2018-04-06@01:49 by Tim Long using System; using System.Timers; @@ -35,18 +34,18 @@ protected StateShutterMoving(SimulatorStateMachine machine, MotorConfiguration d public override void OnEnter() { base.OnEnter(); - ShutterTicksRemaining = machine.HardwareStatus.ShutterSensor == SensorState.Indeterminate + ShutterTicksRemaining = Machine.HardwareStatus.ShutterSensor == SensorState.Indeterminate ? 20 : 40; - machine.HardwareStatus.ShutterSensor = SensorState.Indeterminate; - machine.SimulatedShutterSensor = SensorState.Indeterminate; + Machine.HardwareStatus.ShutterSensor = SensorState.Indeterminate; + Machine.SimulatedShutterSensor = SensorState.Indeterminate; //SimulatorStateMachine.InvokeStatusChanged(new StatusChangedEventArgs(SimulatorStateMachine.HardwareStatus)); - machine.InvokeMotorConfigurationChanged(new MotorConfigurationEventArgs + Machine.InvokeMotorConfigurationChanged(new MotorConfigurationEventArgs { AzimuthMotor = MotorConfiguration.Stopped, ShutterMotor = direction }); - shutter_tick_timer.Interval = machine.RealTime + shutter_tick_timer.Interval = Machine.RealTime ? Properties.Settings.Default.RotationRateMsPerTick : 1; shutter_tick_timer.Elapsed += ShutterTickTimerElapsed; @@ -61,11 +60,11 @@ public override void OnEnter() private void ShutterTickTimerElapsed(object sender, ElapsedEventArgs e) { shutter_tick_timer.Stop(); - machine.WriteLine(string.Format("Z{0:D3}", motorCurrent.Next(6, 12))); + Machine.WriteLine(string.Format("SZ{0:D3}", motorCurrent.Next(6, 25))); if (--ShutterTicksRemaining > 0) shutter_tick_timer.Start(); else - Transition(new StateSendStatus(machine)); + Transition(new StateSendStatus(Machine)); } /// @@ -86,7 +85,7 @@ public override void OnExit() public override void Stimulus(char value) { base.Stimulus(value); - Transition(new StateEmergencyStop(machine)); + Transition(new StateEmergencyStop(Machine)); } } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateShutterOpening.cs b/TA.DigitalDomeworks.HardwareSimulator/StateShutterOpening.cs index 397eb46..e684733 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateShutterOpening.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateShutterOpening.cs @@ -26,8 +26,8 @@ public StateShutterOpening(SimulatorStateMachine machine) : base(machine, MotorC /// public override void OnEnter() { - if (machine.SimulatedShutterSensor == SensorState.Open || machine.ShutterStuck) - Transition(new StateSendStatus(machine)); + if (Machine.SimulatedShutterSensor == SensorState.Open || Machine.ShutterStuck) + Transition(new StateSendStatus(Machine)); else base.OnEnter(); } @@ -35,11 +35,11 @@ public override void OnEnter() public override void OnExit() { base.OnExit(); - if (!machine.ShutterStuck) - machine.SimulatedShutterSensor = ShutterTicksRemaining > 0 + if (!Machine.ShutterStuck) + Machine.SimulatedShutterSensor = ShutterTicksRemaining > 0 ? SensorState.Indeterminate : SensorState.Open; - machine.HardwareStatus.ShutterSensor = machine.SimulatedShutterSensor; + Machine.HardwareStatus.ShutterSensor = Machine.SimulatedShutterSensor; } } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/StateStartup.cs b/TA.DigitalDomeworks.HardwareSimulator/StateStartup.cs index 4d2d6df..abbd9ce 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/StateStartup.cs +++ b/TA.DigitalDomeworks.HardwareSimulator/StateStartup.cs @@ -26,12 +26,12 @@ public override void OnEnter() try { // If all initialization succeeded, immediately transition to the Ready state. - Transition(new StateReceivingCommand(machine)); + Transition(new StateReceivingCommand(Machine)); } catch (Exception ex) { Log.Error(ex, "Exception opening serial port"); - Transition(new StateStalled(machine)); + Transition(new StateStalled(Machine)); } } } diff --git a/TA.DigitalDomeworks.HardwareSimulator/TA.DigitalDomeworks.HardwareSimulator.csproj b/TA.DigitalDomeworks.HardwareSimulator/TA.DigitalDomeworks.HardwareSimulator.csproj index be2405c..1eb6b6a 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/TA.DigitalDomeworks.HardwareSimulator.csproj +++ b/TA.DigitalDomeworks.HardwareSimulator/TA.DigitalDomeworks.HardwareSimulator.csproj @@ -1,5 +1,47 @@  + + + + True + False + True + False + False + True + True + True + True + True + True + True + True + True + False + True + False + False + False + True + True + True + True + False + False + False + False + True + Full + 0 + True + True + True + True + Build + True + + + Debug AnyCPU @@ -46,14 +88,15 @@ ..\TIDigitalDomeWorks.snk + - ..\packages\NLog.4.4.13\lib\net45\NLog.dll - - - ..\packages\NodaTime.2.2.4\lib\net45\NodaTime.dll + ..\packages\NLog.4.5.0\lib\net45\NLog.dll + + + ..\packages\System.Reactive.Core.3.1.1\lib\net46\System.Reactive.Core.dll @@ -69,7 +112,14 @@ ..\packages\System.Reactive.Windows.Threading.3.1.1\lib\net45\System.Reactive.Windows.Threading.dll + + + + + + ..\packages\TA.Ascom.ReactiveCommunications.1.0.0\lib\net45\TA.Ascom.ReactiveCommunications.dll + @@ -79,13 +129,14 @@ - Settings.settings True True + + @@ -127,6 +178,14 @@ + + + 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}. + + + + + + + + + + + + + + Installed OR MaintenanceMode="Modify" OR UPGRADINGPRODUCTCODE + NOT REMOVE OR Installed + + + + + + + + + 1 + $(var.DomeDriverProgId) + $(var.DriverDescription) + + + 2 + $(var.SwitchDriverProgId) + $(var.DriverDescription) + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/AscomDomeDriver.wxs b/TA.DigitalDomeworks.Installer/AscomDomeDriver.wxs new file mode 100644 index 0000000..009a2d0 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/AscomDomeDriver.wxs @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/AscomLocalServer.wxs b/TA.DigitalDomeworks.Installer/AscomLocalServer.wxs new file mode 100644 index 0000000..2d184ee --- /dev/null +++ b/TA.DigitalDomeworks.Installer/AscomLocalServer.wxs @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/AscomSwitchDriver.wxs b/TA.DigitalDomeworks.Installer/AscomSwitchDriver.wxs new file mode 100644 index 0000000..6efc0bf --- /dev/null +++ b/TA.DigitalDomeworks.Installer/AscomSwitchDriver.wxs @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/CommunicationsLayer.wxs b/TA.DigitalDomeworks.Installer/CommunicationsLayer.wxs new file mode 100644 index 0000000..89ebecb --- /dev/null +++ b/TA.DigitalDomeworks.Installer/CommunicationsLayer.wxs @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/TA.DigitalDomeworks.Installer/Config.wxi b/TA.DigitalDomeworks.Installer/Config.wxi new file mode 100644 index 0000000..2850143 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/Config.wxi @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/Directories.wxs b/TA.DigitalDomeworks.Installer/Directories.wxs new file mode 100644 index 0000000..cb7e4f7 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/Directories.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/TA.DigitalDomeworks.Installer/FeatureTree.wxs b/TA.DigitalDomeworks.Installer/FeatureTree.wxs new file mode 100644 index 0000000..8f2aa7b --- /dev/null +++ b/TA.DigitalDomeworks.Installer/FeatureTree.wxs @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/HardwareSimulator.wxs b/TA.DigitalDomeworks.Installer/HardwareSimulator.wxs new file mode 100644 index 0000000..1bf137c --- /dev/null +++ b/TA.DigitalDomeworks.Installer/HardwareSimulator.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/NLog.wxs b/TA.DigitalDomeworks.Installer/NLog.wxs new file mode 100644 index 0000000..9f0f62f --- /dev/null +++ b/TA.DigitalDomeworks.Installer/NLog.wxs @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/Ninject.wxs b/TA.DigitalDomeworks.Installer/Ninject.wxs new file mode 100644 index 0000000..efc081d --- /dev/null +++ b/TA.DigitalDomeworks.Installer/Ninject.wxs @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/PostSharp.wxs b/TA.DigitalDomeworks.Installer/PostSharp.wxs new file mode 100644 index 0000000..5b4ddf0 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/PostSharp.wxs @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/PostSharpAspects.wxs b/TA.DigitalDomeworks.Installer/PostSharpAspects.wxs new file mode 100644 index 0000000..630de0e --- /dev/null +++ b/TA.DigitalDomeworks.Installer/PostSharpAspects.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/TA.DigitalDomeworks.Installer/Product.wxs b/TA.DigitalDomeworks.Installer/Product.wxs new file mode 100644 index 0000000..9b95092 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/Product.wxs @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + VersionNT64 + + NOT VersionNT64 + + + + + + + = 601)]]> + + = 601)]]> + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/ReactiveASCOM.wxs b/TA.DigitalDomeworks.Installer/ReactiveASCOM.wxs new file mode 100644 index 0000000..665b0ea --- /dev/null +++ b/TA.DigitalDomeworks.Installer/ReactiveASCOM.wxs @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/ReactiveExtensions.wxs b/TA.DigitalDomeworks.Installer/ReactiveExtensions.wxs new file mode 100644 index 0000000..8e1eca7 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/ReactiveExtensions.wxs @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/SharedTypes.wxs b/TA.DigitalDomeworks.Installer/SharedTypes.wxs new file mode 100644 index 0000000..06fb810 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/SharedTypes.wxs @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Installer/TA.DigitalDomeworks.Installer.wixproj b/TA.DigitalDomeworks.Installer/TA.DigitalDomeworks.Installer.wixproj new file mode 100644 index 0000000..af6226c --- /dev/null +++ b/TA.DigitalDomeworks.Installer/TA.DigitalDomeworks.Installer.wixproj @@ -0,0 +1,157 @@ + + + + + Debug + x64 + 3.10 + 62bc6e28-4239-47f9-b0bc-69ccdcea6f75 + 2.0 + TA.DigitalDomeworks.Installer + Package + + + + + bin\Debug\x86\ + obj\Debug\x86\ + Debug + + + bin\Release\x86\ + obj\Release\x86\ + + + bin\Debug\x64\ + obj\Debug\x64\ + Debug + + + bin\Release\x64\ + obj\Release\x64\ + + + + + + + + + + + + + + + + + + + + + + + + + + ..\packages\WiX.3.11.0\tools\WixNetFxExtension.dll + WixNetFxExtension + + + ..\packages\WiX.3.11.0\tools\WixUtilExtension.dll + WixUtilExtension + + + + + AscomDome + {0b22461b-2d19-4342-a87d-4e90efd3e2a9} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + AscomSwitch + {f95208c4-450f-4b51-810a-c805a8efbb7a} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + DeviceInterface + {020924b9-23d5-4b92-b5b1-461423bbe23c} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + HardwareSimulator + {86b17c99-41b6-4611-ad1d-26b7d6c70a22} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + AscomServer + {3689a2cb-94c5-4012-a5cf-7e7d1dd27143} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + SharedTypes + {adbb1165-e995-4c75-8de9-1dbffcf34d6f} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + Aspects + {9cdcf319-dadc-41eb-b787-de3862017e95} + True + True + Binaries;Content;Satellites + INSTALLFOLDER + + + + + + + + + + + + + + + + + + + + + + + + + + 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/TA.DigitalDomeworks.Installer/packages.config b/TA.DigitalDomeworks.Installer/packages.config new file mode 100644 index 0000000..c487091 --- /dev/null +++ b/TA.DigitalDomeworks.Installer/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ASCOM.png b/TA.DigitalDomeworks.Server/ASCOM.png new file mode 100644 index 0000000..a83b77b Binary files /dev/null and b/TA.DigitalDomeworks.Server/ASCOM.png differ diff --git a/TA.DigitalDomeworks.Server/AboutBox.Designer.cs b/TA.DigitalDomeworks.Server/AboutBox.Designer.cs new file mode 100644 index 0000000..f68f15c --- /dev/null +++ b/TA.DigitalDomeworks.Server/AboutBox.Designer.cs @@ -0,0 +1,233 @@ +namespace TA.DigitalDomeworks.Server +{ + partial class AboutBox + { + /// + /// 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() + { + System.Windows.Forms.Label DriverVersionLabel; + System.Windows.Forms.Label label3; + this.ProductIcon = new System.Windows.Forms.PictureBox(); + this.LogoBanner = new System.Windows.Forms.PictureBox(); + this.TigraLogo = new System.Windows.Forms.PictureBox(); + this.OkCommand = new System.Windows.Forms.Button(); + this.ProductTitle = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.DriverVersion = new System.Windows.Forms.Label(); + this.linkLabel1 = new System.Windows.Forms.LinkLabel(); + this.ShowUserGuideCommand = new System.Windows.Forms.Button(); + this.InformationalVersion = new System.Windows.Forms.Label(); + DriverVersionLabel = new System.Windows.Forms.Label(); + label3 = new System.Windows.Forms.Label(); + ((System.ComponentModel.ISupportInitialize)(this.ProductIcon)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.LogoBanner)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.TigraLogo)).BeginInit(); + this.SuspendLayout(); + // + // DriverVersionLabel + // + DriverVersionLabel.AutoSize = true; + DriverVersionLabel.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + DriverVersionLabel.Location = new System.Drawing.Point(13, 211); + DriverVersionLabel.Name = "DriverVersionLabel"; + DriverVersionLabel.Size = new System.Drawing.Size(111, 21); + DriverVersionLabel.TabIndex = 5; + DriverVersionLabel.Text = "Build Number"; + // + // label3 + // + label3.AutoSize = true; + label3.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + label3.Location = new System.Drawing.Point(15, 232); + label3.Name = "label3"; + label3.Size = new System.Drawing.Size(126, 21); + label3.TabIndex = 11; + label3.Text = "Product Version"; + // + // ProductIcon + // + this.ProductIcon.Cursor = System.Windows.Forms.Cursors.Hand; + this.ProductIcon.Image = global::TA.DigitalDomeworks.Server.Properties.Resources.DigitalDomeworks; + this.ProductIcon.Location = new System.Drawing.Point(324, 144); + this.ProductIcon.Name = "ProductIcon"; + this.ProductIcon.Size = new System.Drawing.Size(200, 200); + this.ProductIcon.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.ProductIcon.TabIndex = 0; + this.ProductIcon.TabStop = false; + this.ProductIcon.Tag = "http://homedome.com/"; + this.ProductIcon.Click += new System.EventHandler(this.NavigateToWebPage); + // + // LogoBanner + // + this.LogoBanner.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Stretch; + this.LogoBanner.Cursor = System.Windows.Forms.Cursors.Hand; + this.LogoBanner.Dock = System.Windows.Forms.DockStyle.Top; + this.LogoBanner.Image = global::TA.DigitalDomeworks.Server.Properties.Resources.AuroraWideWithText; + this.LogoBanner.Location = new System.Drawing.Point(0, 0); + this.LogoBanner.Name = "LogoBanner"; + this.LogoBanner.Size = new System.Drawing.Size(536, 138); + this.LogoBanner.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage; + this.LogoBanner.TabIndex = 1; + this.LogoBanner.TabStop = false; + this.LogoBanner.Tag = "http://www.geminitelescope.com/"; + this.LogoBanner.Click += new System.EventHandler(this.NavigateToWebPage); + // + // TigraLogo + // + this.TigraLogo.Cursor = System.Windows.Forms.Cursors.Hand; + this.TigraLogo.Image = global::TA.DigitalDomeworks.Server.Properties.Resources.TigraAstronomyLogo; + this.TigraLogo.Location = new System.Drawing.Point(324, 350); + this.TigraLogo.Name = "TigraLogo"; + this.TigraLogo.Size = new System.Drawing.Size(200, 200); + this.TigraLogo.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; + this.TigraLogo.TabIndex = 0; + this.TigraLogo.TabStop = false; + this.TigraLogo.Tag = "http://tigra-astronomy.com"; + this.TigraLogo.Click += new System.EventHandler(this.NavigateToWebPage); + // + // OkCommand + // + this.OkCommand.DialogResult = System.Windows.Forms.DialogResult.OK; + this.OkCommand.Location = new System.Drawing.Point(324, 577); + this.OkCommand.Name = "OkCommand"; + this.OkCommand.Size = new System.Drawing.Size(200, 23); + this.OkCommand.TabIndex = 2; + this.OkCommand.Text = "OK"; + this.OkCommand.UseVisualStyleBackColor = true; + this.OkCommand.Click += new System.EventHandler(this.OkCommand_Click); + // + // ProductTitle + // + this.ProductTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 15.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.ProductTitle.Location = new System.Drawing.Point(13, 166); + this.ProductTitle.Name = "ProductTitle"; + this.ProductTitle.Size = new System.Drawing.Size(305, 30); + this.ProductTitle.TabIndex = 3; + this.ProductTitle.Text = "Digital Domeworks 2018"; + // + // label1 + // + this.label1.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, ((System.Drawing.FontStyle)((System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Italic))), System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.Location = new System.Drawing.Point(15, 350); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(303, 177); + this.label1.TabIndex = 4; + this.label1.Text = "ASCOM Multi-instance Server\r\nProfessionally produced by\r\n\r\nTigra Astronomy\r\n\r\nWe " + + "are available for hire to create your ASCOM driver, firmware, or application"; + this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + this.label1.Click += new System.EventHandler(this.label1_Click); + // + // DriverVersion + // + this.DriverVersion.AutoSize = true; + this.DriverVersion.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.DriverVersion.Location = new System.Drawing.Point(154, 211); + this.DriverVersion.Name = "DriverVersion"; + this.DriverVersion.Size = new System.Drawing.Size(60, 21); + this.DriverVersion.TabIndex = 5; + this.DriverVersion.Text = "(unset)"; + this.DriverVersion.Click += new System.EventHandler(this.DriverVersion_Click); + // + // linkLabel1 + // + this.linkLabel1.Cursor = System.Windows.Forms.Cursors.Hand; + this.linkLabel1.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.linkLabel1.Location = new System.Drawing.Point(12, 527); + this.linkLabel1.Name = "linkLabel1"; + this.linkLabel1.Size = new System.Drawing.Size(297, 23); + this.linkLabel1.TabIndex = 6; + this.linkLabel1.TabStop = true; + this.linkLabel1.Text = "http://tigra-astronomy.com"; + this.linkLabel1.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // ShowUserGuideCommand + // + this.ShowUserGuideCommand.AutoSize = true; + this.ShowUserGuideCommand.Enabled = false; + this.ShowUserGuideCommand.Font = new System.Drawing.Font("Segoe UI Semibold", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.ShowUserGuideCommand.Location = new System.Drawing.Point(107, 300); + this.ShowUserGuideCommand.Name = "ShowUserGuideCommand"; + this.ShowUserGuideCommand.Size = new System.Drawing.Size(121, 27); + this.ShowUserGuideCommand.TabIndex = 9; + this.ShowUserGuideCommand.Tag = "http://www.geminitelescope.com/Manuals/Integra85_manual.pdf"; + this.ShowUserGuideCommand.Text = "Show User Guide"; + this.ShowUserGuideCommand.UseVisualStyleBackColor = true; + this.ShowUserGuideCommand.Click += new System.EventHandler(this.NavigateToWebPage); + // + // InformationalVersion + // + this.InformationalVersion.AutoSize = true; + this.InformationalVersion.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.InformationalVersion.Location = new System.Drawing.Point(156, 232); + this.InformationalVersion.Name = "InformationalVersion"; + this.InformationalVersion.Size = new System.Drawing.Size(60, 21); + this.InformationalVersion.TabIndex = 10; + this.InformationalVersion.Text = "(unset)"; + // + // AboutBox + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CausesValidation = false; + this.ClientSize = new System.Drawing.Size(536, 612); + this.Controls.Add(this.InformationalVersion); + this.Controls.Add(label3); + this.Controls.Add(this.ShowUserGuideCommand); + this.Controls.Add(this.linkLabel1); + this.Controls.Add(this.DriverVersion); + this.Controls.Add(DriverVersionLabel); + this.Controls.Add(this.label1); + this.Controls.Add(this.ProductTitle); + this.Controls.Add(this.OkCommand); + this.Controls.Add(this.LogoBanner); + this.Controls.Add(this.TigraLogo); + this.Controls.Add(this.ProductIcon); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Name = "AboutBox"; + this.Text = "About this software"; + this.Load += new System.EventHandler(this.AboutBox_Load); + ((System.ComponentModel.ISupportInitialize)(this.ProductIcon)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.LogoBanner)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.TigraLogo)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.PictureBox ProductIcon; + private System.Windows.Forms.PictureBox LogoBanner; + private System.Windows.Forms.PictureBox TigraLogo; + private System.Windows.Forms.Button OkCommand; + private System.Windows.Forms.Label ProductTitle; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label DriverVersion; + private System.Windows.Forms.LinkLabel linkLabel1; + private System.Windows.Forms.Button ShowUserGuideCommand; + private System.Windows.Forms.Label InformationalVersion; + } +} \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/AboutBox.cs b/TA.DigitalDomeworks.Server/AboutBox.cs new file mode 100644 index 0000000..fa04e9d --- /dev/null +++ b/TA.DigitalDomeworks.Server/AboutBox.cs @@ -0,0 +1,58 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: AboutBox.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Diagnostics; +using System.Reflection; +using System.Windows.Forms; + +namespace TA.DigitalDomeworks.Server + { + public partial class AboutBox : Form + { + public AboutBox() + { + InitializeComponent(); + } + + private void label1_Click(object sender, EventArgs e) { } + + private void AboutBox_Load(object sender, EventArgs e) + { + var me = Assembly.GetExecutingAssembly(); + var name = me.GetName(); + var driverVersion = name.Version; + var productVersion = me.GetCustomAttribute().InformationalVersion; + DriverVersion.Text = driverVersion.ToString(); + InformationalVersion.Text = productVersion; + } + + private void DriverVersion_Click(object sender, EventArgs e) { } + + private void NavigateToWebPage(object sender, EventArgs e) + { + var control = sender as Control; + if (control == null) + return; + var url = control.Tag.ToString(); + if (!url.StartsWith("http:")) + return; + try + { + Process.Start(url); + } + catch (Exception) + { + // Just fail silently + } + } + + private void OkCommand_Click(object sender, EventArgs e) + { + Close(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/AboutBox.resx b/TA.DigitalDomeworks.Server/AboutBox.resx new file mode 100644 index 0000000..0e640e8 --- /dev/null +++ b/TA.DigitalDomeworks.Server/AboutBox.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + False + + + False + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/AppDomainIsolated.cs b/TA.DigitalDomeworks.Server/AppDomainIsolated.cs new file mode 100644 index 0000000..c49254a --- /dev/null +++ b/TA.DigitalDomeworks.Server/AppDomainIsolated.cs @@ -0,0 +1,40 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: AppDomainIsolated.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; + +namespace TA.DigitalDomeworks.Server + { + internal class AppDomainIsolated : IDisposable + where TWorker : MarshalByRefObject + { + private AppDomain domain; + + public AppDomainIsolated() + { + domain = AppDomain.CreateDomain("Isolated:" + Guid.NewGuid(), + null, AppDomain.CurrentDomain.SetupInformation); + var type = typeof(TWorker); + Worker = (TWorker) domain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName); + } + + /// + /// Gets the worker object that will run in the isolated app domain. + /// + /// The worker. + public TWorker Worker { get; } + + public void Dispose() + { + if (domain != null) + { + AppDomain.Unload(domain); + + domain = null; + } + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/CLientStatus.cs b/TA.DigitalDomeworks.Server/CLientStatus.cs new file mode 100644 index 0000000..8b2fed9 --- /dev/null +++ b/TA.DigitalDomeworks.Server/CLientStatus.cs @@ -0,0 +1,93 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: CLientStatus.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; + +namespace TA.DigitalDomeworks.Server + { + /// + /// Records the connection status of a client. + /// + internal class ClientStatus : IEquatable + { + /// + /// Gets or sets the client unique identifier. + /// + /// The client identifier. + public Guid ClientId { get; set; } + + /// + /// Gets or sets the client name. Each client provides its name upon first registration and + /// typically this will be the name of the executable that owns the client's process. + /// + /// The name. + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether the is "online". A client is considered to be + /// online if it has requested to open the communications channel, and "offline" if it has + /// requested to close it. + /// + /// true if online; otherwise, false. + public bool Online { get; set; } + + public bool Equals(ClientStatus other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return ClientId.Equals(other.ClientId); + } + + public bool Equals(Guid other) + { + return ClientId.Equals(other); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != typeof(ClientStatus)) return false; + return Equals((ClientStatus) obj); + } + + public override int GetHashCode() + { + return ClientId.GetHashCode(); + } + + public static bool operator ==(ClientStatus left, ClientStatus right) + { + return Equals(left, right); + } + + public static bool operator ==(ClientStatus left, Guid right) + { + return left?.Equals(right) ?? false; + } + + public static bool operator !=(ClientStatus left, ClientStatus right) + { + return !Equals(left, right); + } + + public static bool operator !=(ClientStatus left, Guid right) + { + return !left?.Equals(right) ?? false; + } + + public override string ToString() + { + return $"{nameof(ClientId)}: {ClientId}, {nameof(Name)}: {Name}, {nameof(Online)}: {Online}"; + } + + public string ToDisplayString() + { + var online = Online ? "Online" : "Idle "; + return $"{ClientId} {online} {Name}"; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ClassFactory.cs b/TA.DigitalDomeworks.Server/ClassFactory.cs new file mode 100644 index 0000000..2aad0c1 --- /dev/null +++ b/TA.DigitalDomeworks.Server/ClassFactory.cs @@ -0,0 +1,237 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ClassFactory.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Collections; +using System.Runtime.InteropServices; + +namespace TA.DigitalDomeworks.Server + { + #region C# Definition of IClassFactory + // + // Provide a definition of theCOM IClassFactory interface. + // + [ComImport] + [ComVisible(false)] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + [Guid("00000001-0000-0000-C000-000000000046")] + 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] + private 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] + private 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")] + private 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")] + private 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")] + private static extern int CoSuspendClassObjects(); + + // + // CoRevokeClassObject() is used to unregister a Class Factory + // from COM's internal table of Class Factories. + // + [DllImport("ole32.dll")] + private 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 uint 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 (var 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 + var i = CoRegisterClassObject + ( + ref m_ClassId, + this, + m_ClassContext, + m_Flags, + out m_Cookie + ); + return i == 0; + } + + public bool RevokeClassObject() + { + var i = CoRevokeClassObject(m_Cookie); + return i == 0; + } + + public static bool ResumeClassObjects() + { + var i = CoResumeClassObjects(); + return i == 0; + } + + public static bool SuspendClassObjects() + { + var 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) + { + var 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)); + else if (riid == IID_IUnknown) + ppvObject = Marshal.GetIUnknownForObject(Activator.CreateInstance(m_ClassType)); + else + 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 + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ClickCommand.cs b/TA.DigitalDomeworks.Server/ClickCommand.cs new file mode 100644 index 0000000..5397678 --- /dev/null +++ b/TA.DigitalDomeworks.Server/ClickCommand.cs @@ -0,0 +1,79 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ClickCommand.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Windows.Forms; +using JetBrains.Annotations; + +namespace TA.DigitalDomeworks.Server + { + internal class ClickCommand : IDisposable + { + [CanBeNull] private readonly Func canExecute; + [NotNull] private readonly Control control; + [NotNull] private readonly Action execute; + + public ClickCommand(Control control, Action execute, Func canExecute = null) + { + this.control = control; + this.execute = execute; + this.canExecute = canExecute; + control.Click += Execute; + } + + public void Execute(object sender, EventArgs eventArgs) => execute.Invoke(); + + public void CanExecuteChanged() + { + control.Enabled = canExecute?.Invoke() ?? true; + } + + #region IDisposable Pattern + // The IDisposable pattern, as described at + // http://www.codeproject.com/Articles/15360/Implementing-IDisposable-and-the-Dispose-Pattern-P + + + /// + /// Finalizes this instance (called prior to garbage collection by the CLR) + /// + ~ClickCommand() + { + Dispose(fromUserCode: false); + } + + public void Dispose() + { + Dispose(fromUserCode: true); + GC.SuppressFinalize(this); + } + + private bool disposed; + + protected virtual void Dispose(bool fromUserCode) + { + if (!disposed) + if (fromUserCode) + control.Click -= Execute; + + disposed = true; + + // ToDo: Call the base class's Dispose(Boolean) method, if available. + // base.Dispose(fromUserCode); + } + #endregion + } + + internal static class ControlExtensions + { + public static ClickCommand AttachCommand(this Control clickableControl, Action execute, + Func canExecute = null) + { + var command = new ClickCommand(clickableControl, execute, canExecute); + command.CanExecuteChanged(); + return command; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ClientConnectionManager.cs b/TA.DigitalDomeworks.Server/ClientConnectionManager.cs new file mode 100644 index 0000000..10bdfb8 --- /dev/null +++ b/TA.DigitalDomeworks.Server/ClientConnectionManager.cs @@ -0,0 +1,244 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ClientConnectionManager.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Ninject; +using NLog; +using PostSharp.Patterns.Model; +using PostSharp.Patterns.Threading; +using TA.Ascom.ReactiveCommunications; +using TA.DigitalDomeworks.DeviceInterface; + +namespace TA.DigitalDomeworks.Server + { + /// + /// Manages client (driver) connections to the shared device controller. Uses the Reader + /// Writer Lock pattern to ensure thread safety. + /// + [ReaderWriterSynchronized] + public class ClientConnectionManager + { + [Reference] private readonly ILogger log = LogManager.GetCurrentClassLogger(); + private readonly bool performActionsOnOpen; + [Reference] private Maybe controllerInstance; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A factory class that can create and destroy transaction processors (and by implication, + /// the entire communications stack). + /// + public ClientConnectionManager() : this(performActionsOnOpen: true) { } + + /// + /// Initializes a new instance of the class and allows + /// control of whether an On Connected actions will be performed. This is primarily intended + /// for use in unit testing so is not visible to clients. + /// + /// + /// A factory class that can construct instances of a trnasaction processor (and by + /// implication, the entire communications stack). + /// + /// + /// if set to true [perform actions on open]. + /// + internal ClientConnectionManager(bool performActionsOnOpen) + { + this.performActionsOnOpen = performActionsOnOpen; + Clients = new List(); + controllerInstance = Maybe.Empty; + } + + [Reference] + internal List Clients { get; } + + /// + /// Gets the number of connected clients. + /// + /// The connected client count. + public int RegisteredClientCount => Clients.Count(); + + public int OnlineClientCount => Clients.Count(p => p.Online); + + /// + /// Gets the controller instance if it has been created. + /// + /// The controller instance. + internal Maybe MaybeControllerInstance => controllerInstance; + + /// + /// Gets the controller instance, ensuring that it is open and ready for use. + /// + /// + /// The client must provide it's ID which has previously been obtained by calling + /// . + /// + /// IIntegraController. + /// + /// Clients must release previous controller instances before requesting another + /// + [Writer] + public DeviceController GoOnline(Guid clientId) + { + log.Info($"Go online for client {clientId}"); + ClientStatus client = null; + try + { + client = Clients.Single(p => p.Equals(clientId)); + } + catch (InvalidOperationException e) + { + var message = $"Attempt to go online with unregistered client {clientId}"; + log.Error(e, message); + //ThrowOnUnrecognizedClient(clientId, e, message); + } + + try + { + EnsureControllerInstanceCreatedAndOpen(); + } + catch (TimeoutException tex) + { + log.Error(tex, $"Not connected because state machine did not become ready"); + DestroyControllerInstance(); + return null; + } + + client.Online = true; + RaiseClientStatusChanged(); + return controllerInstance.Single(); + } + + internal event EventHandler ClientStatusChanged; + + protected void RaiseClientStatusChanged() + { + ClientStatusChanged?.Invoke(this, EventArgs.Empty); + } + + [Writer] + private void EnsureControllerInstanceCreatedAndOpen() + { + if (!controllerInstance.Any()) + { + CompositionRoot.BeginSessionScope(); + var controller = CompositionRoot.Kernel.Get(); + controllerInstance = new Maybe(controller); + } + + var instance = controllerInstance.Single(); + if (!instance.IsConnected) + instance.Open(performActionsOnOpen); + } + + [Writer] + private void DestroyControllerInstance() + { + if (controllerInstance.Any()) + controllerInstance.Single().Close(); + controllerInstance = Maybe.Empty; + } + + [Writer] + public void GoOffline(Guid clientId) + { + log.Info($"Go offline for client {clientId}"); + ClientStatus client = null; + try + { + client = Clients.Single(p => p.Equals(clientId)); + } + catch (InvalidOperationException e) + { + var message = $"Attempt to go offline by unecognized client {clientId}"; + log.Error(e, message); + //ThrowOnUnrecognizedClient(clientId, e, message); + } + + client.Online = false; + RaiseClientStatusChanged(); + if (OnlineClientCount == 0) + { + log.Warn($"The last client has gone offline - closing connection"); + + if (controllerInstance.Any()) + { + var controller = controllerInstance.Single(); + controller.Close(); + } + + controllerInstance = Maybe.Empty; + } + } + + + /// + /// Determines whether the client with the specified ID is registered. + /// + /// The client unique identifier. + /// true if the client is connected; otherwise, false. + [Reader] + public bool IsClientRegistered(Guid clientId) => Clients.Any(p => p.Equals(clientId)); + + /// + /// Gets a new unique client identifier. + /// + /// Guid. + [Writer] + public Guid RegisterClient(string name = null) + { + var id = Guid.NewGuid(); + var status = new ClientStatus {ClientId = id, Name = name ?? id.ToString(), Online = false}; + Clients.Add(status); + RaiseClientStatusChanged(); + return id; + } + + [Writer] + public void UnregisterClient(Guid clientId) + { + log.Info($"Unregistering client {clientId}"); + var previousClientCount = RegisteredClientCount; + try + { + Clients.Remove(Clients.Single(p => p.Equals(clientId))); + RaiseClientStatusChanged(); + } + catch (InvalidOperationException e) + { + var message = $"Attempt to unregister unknown client {clientId}"; + log.Error(e, message); + //ThrowOnUnrecognizedClient(clientId, e, "Attempt to unregister an unknown client"); + } + + if (previousClientCount == 1 && RegisteredClientCount == 0) + { + DestroyControllerInstance(); + Server.TerminateLocalServer(); + } + } + + /// + /// Throws an ASCOM. with information about registered clients. + /// + /// The client identifier causing the original exception. + /// The original (inner) exception. + /// The error message. + [ContractAnnotation("=>halt")] + private void ThrowOnUnrecognizedClient(Guid clientId, Exception e, string message) + { + var ex = new ASCOM.InvalidOperationException($"Connection Manager: {message}", e); + ex.Data["RegisteredClients"] = Clients; + ex.Data["UnknownClient"] = clientId; + log.Error(ex, $"Client not found: {clientId}"); + throw ex; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/CommunicationSettingsControl.Designer.cs b/TA.DigitalDomeworks.Server/CommunicationSettingsControl.Designer.cs new file mode 100644 index 0000000..5ce71f9 --- /dev/null +++ b/TA.DigitalDomeworks.Server/CommunicationSettingsControl.Designer.cs @@ -0,0 +1,115 @@ +namespace TA.DigitalDomeworks.Server +{ + partial class CommunicationSettingsControl + { + /// + /// 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 Component 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.ConnectionString = new System.Windows.Forms.Label(); + this.CommPortName = new System.Windows.Forms.ComboBox(); + this.UseSimulator = new System.Windows.Forms.CheckBox(); + this.label3 = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(3, 3); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(64, 13); + this.label1.TabIndex = 1; + this.label1.Text = "Connection:"; + // + // ConnectionString + // + this.ConnectionString.AutoSize = true; + this.ConnectionString.DataBindings.Add(new System.Windows.Forms.Binding("Text", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "ConnectionString", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.ConnectionString.Location = new System.Drawing.Point(104, 23); + this.ConnectionString.Name = "ConnectionString"; + this.ConnectionString.Size = new System.Drawing.Size(64, 13); + this.ConnectionString.TabIndex = 2; + this.ConnectionString.Text = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.ConnectionString; + // + // CommPortName + // + this.CommPortName.DataBindings.Add(new System.Windows.Forms.Binding("Text", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "CommPortName", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.CommPortName.FormattingEnabled = true; + this.CommPortName.Location = new System.Drawing.Point(73, 0); + this.CommPortName.Name = "CommPortName"; + this.CommPortName.Size = new System.Drawing.Size(80, 21); + this.CommPortName.TabIndex = 0; + this.CommPortName.Text = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.CommPortName; + this.CommPortName.SelectedIndexChanged += new System.EventHandler(this.CommPortName_SelectedIndexChanged); + // + // UseSimulator + // + this.UseSimulator.AutoSize = true; + this.UseSimulator.Location = new System.Drawing.Point(159, 2); + this.UseSimulator.Name = "UseSimulator"; + this.UseSimulator.Size = new System.Drawing.Size(91, 17); + this.UseSimulator.TabIndex = 3; + this.UseSimulator.Text = "Use Simulator"; + this.UseSimulator.UseVisualStyleBackColor = true; + this.UseSimulator.CheckedChanged += new System.EventHandler(this.UseSimulator_CheckedChanged); + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(6, 23); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(92, 13); + this.label3.TabIndex = 4; + this.label3.Text = "Connection string:"; + // + // CommunicationSettingsControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSize = true; + this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.Controls.Add(this.label3); + this.Controls.Add(this.UseSimulator); + this.Controls.Add(this.ConnectionString); + this.Controls.Add(this.label1); + this.Controls.Add(this.CommPortName); + this.Name = "CommunicationSettingsControl"; + this.Size = new System.Drawing.Size(253, 36); + this.Load += new System.EventHandler(this.CommunicationSettingsControl_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ComboBox CommPortName; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.CheckBox UseSimulator; + private System.Windows.Forms.Label label3; + internal System.Windows.Forms.Label ConnectionString; + } +} diff --git a/TA.DigitalDomeworks.Server/CommunicationSettingsControl.cs b/TA.DigitalDomeworks.Server/CommunicationSettingsControl.cs new file mode 100644 index 0000000..704b80b --- /dev/null +++ b/TA.DigitalDomeworks.Server/CommunicationSettingsControl.cs @@ -0,0 +1,78 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: CommunicationSettingsControl.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using TA.Ascom.ReactiveCommunications; +using TA.DigitalDomeworks.HardwareSimulator; +using TA.DigitalDomeworks.Server.Properties; +using SerialPort = System.IO.Ports.SerialPort; + +namespace TA.DigitalDomeworks.Server + { + public partial class CommunicationSettingsControl : UserControl + { + public CommunicationSettingsControl() + { + InitializeComponent(); + } + + public void Save() + { + Settings.Default.Save(); + } + + private void CommunicationSettingsControl_Load(object sender, EventArgs e) + { + var connection = Settings.Default.ConnectionString; + var useSimulator = SimulatorEndpoint.IsConnectionStringValid(connection); + UseSimulator.Checked = useSimulator; + CommPortName.Enabled = !useSimulator; + var currentSelection = Settings.Default.CommPortName; + var ports = new SortedSet(SerialPort.GetPortNames()); + if (!ports.Contains(currentSelection)) ports.Add(currentSelection); + CommPortName.Items.Clear(); + CommPortName.Items.AddRange(ports.ToArray()); + var currentIndex = CommPortName.Items.IndexOf(currentSelection); + CommPortName.SelectedIndex = currentIndex; + } + + private void UseSimulator_CheckedChanged(object sender, EventArgs e) + { + CommPortName.Enabled = !UseSimulator.Checked; + BuildConnectionString(); + } + + private void BuildConnectionString() + { + var useSimulator = UseSimulator.Checked; + if (useSimulator) + BuildSimulatorConnectionString(); + else + BuildSerialConnectionString(); + } + + private void BuildSimulatorConnectionString() + { + var candidate = SimulatorEndpoint.FromConnectionString("Simulator"); + candidate.Realtime = true; + Settings.Default.ConnectionString = candidate.ToString(); + } + + private void BuildSerialConnectionString() + { + var candidate = new SerialDeviceEndpoint(CommPortName.Text); + Settings.Default.ConnectionString = candidate.ToString(); + } + + private void CommPortName_SelectedIndexChanged(object sender, EventArgs e) + { + BuildConnectionString(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/CommunicationSettingsControl.resx b/TA.DigitalDomeworks.Server/CommunicationSettingsControl.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/TA.DigitalDomeworks.Server/CommunicationSettingsControl.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/TA.DigitalDomeworks.Server/CompositionRoot.cs b/TA.DigitalDomeworks.Server/CompositionRoot.cs new file mode 100644 index 0000000..8660e99 --- /dev/null +++ b/TA.DigitalDomeworks.Server/CompositionRoot.cs @@ -0,0 +1,107 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: CompositionRoot.cs Last modified: 2018-06-17@17:22 by Tim Long + +using System; +using Ninject; +using Ninject.Activation; +using Ninject.Modules; +using Ninject.Syntax; +using NLog.Fluent; +using TA.Ascom.ReactiveCommunications; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.DeviceInterface.StateMachine; +using TA.DigitalDomeworks.HardwareSimulator; +using TA.DigitalDomeworks.Server.Properties; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.Server + { + public static class CompositionRoot + { + static CompositionRoot() + { + Kernel = new StandardKernel(new CoreModule()); + } + + private static ScopeObject CurrentScope { get; set; } + + public static IKernel Kernel { get; } + + public static void BeginSessionScope() + { + var scope = new ScopeObject(); + Log.Info() + .Message($"Beginning session scope id={scope.ScopeId}") + .Write(); + CurrentScope = scope; + } + + public static IBindingNamedWithOrOnSyntax InSessionScope(this IBindingInSyntax binding) + { + return binding.InScope(ctx => CurrentScope); + } + } + + internal class ScopeObject + { + private static int scopeId; + + public ScopeObject() + { + ++scopeId; + } + + public int ScopeId => scopeId; + } + + internal class CoreModule : NinjectModule + { + public override void Load() + { + Bind().ToSelf().InSessionScope(); + Bind() + .ToMethod(BuildCommunicationsChannel) + .InSessionScope(); + Bind().ToMethod(BuildChannelFactory).InSessionScope(); + Bind().To().InSingletonScope(); + Bind().To().InSessionScope(); + Bind().ToMethod(BuildDeviceOptions).InSessionScope(); + } + + private ICommunicationChannel BuildCommunicationsChannel(IContext context) + { + var channelFactory = Kernel.Get(); + var channel = channelFactory.FromConnectionString(Settings.Default.ConnectionString); + return channel; + } + + private ChannelFactory BuildChannelFactory(IContext arg) + { + var factory = new ChannelFactory(); + factory.RegisterChannelType( + SimulatorEndpoint.IsConnectionStringValid, + SimulatorEndpoint.FromConnectionString, + endpoint => new SimulatorCommunicationsChannel((SimulatorEndpoint) endpoint) + ); + return factory; + } + + private DeviceControllerOptions BuildDeviceOptions(IContext arg) + { + var options = new DeviceControllerOptions + { + PerformShutterRecovery = Settings.Default.PerformShutterRecovery, + MaximumShutterCloseTime = TimeSpan.FromSeconds((double) Settings.Default.ShutterOpenCloseTimeSeconds), + MaximumFullRotationTime = TimeSpan.FromSeconds((double) Settings.Default.FullRotationTimeSeconds), + KeepAliveTimerInterval = Settings.Default.KeepAliveTimerPeriod, + CurrentDrawDetectionThreshold = Settings.Default.CurrentDrawDetectionThreshold, + IgnoreHardwareShutterSensor = Settings.Default.IgnoreHardwareShutterSensor, + ShutterTickTimeout = Settings.Default.ShutterTickTimeout + }; + return options; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/GarbageCollection.cs b/TA.DigitalDomeworks.Server/GarbageCollection.cs new file mode 100644 index 0000000..a02eef1 --- /dev/null +++ b/TA.DigitalDomeworks.Server/GarbageCollection.cs @@ -0,0 +1,64 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: GarbageCollection.cs Last modified: 2018-03-28@22:19 by Tim Long + +using System; +using System.Threading; + +namespace TA.DigitalDomeworks.Server + { + /// + /// Summary description for GarbageCollection. + /// + internal class GarbageCollection + { + protected volatile bool m_bContinueThread; + protected ManualResetEvent m_EventThreadEnded; + protected bool m_GCWatchStopped; + protected int m_iInterval; + + 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(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/LocalServer.cs b/TA.DigitalDomeworks.Server/LocalServer.cs new file mode 100644 index 0000000..7750c96 --- /dev/null +++ b/TA.DigitalDomeworks.Server/LocalServer.cs @@ -0,0 +1,658 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: LocalServer.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Threading; +using System.Windows.Forms; +using ASCOM; +using ASCOM.Utilities; +using Microsoft.Win32; +using NLog; + +namespace TA.DigitalDomeworks.Server + { + public static class Server + { + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + // 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 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) + { + var 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", + "ASCOM LocalServer", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); + break; + } + else + StartedByCOM = false; + + return bRet; + } + #endregion + + #region SERVER ENTRY POINT (main) + // + // ================== + // SERVER ENTRY POINT + // ================== + // + [STAThread] + private static void Main(string[] args) + { + // Manage unhandled exceptions + Application.ThreadException += UnhandledThreadException; + AppDomain.CurrentDomain.UnhandledException += UnhandledException; + + var foundTypes = LoadComObjectAssemblies(); + if (foundTypes < 1) + return; // There is no point continuing if we found nothing to serve. + + 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 ServerStatusDisplay(); +//#if !DEBUG +//// Only show the application main window if it was started manually by the user. +// if (StartedByCOM) s_MainForm.WindowState = FormWindowState.Minimized; +//#endif + + // Register the class factories of the served objects + RegisterClassFactories(); + + // Start up the garbage collection thread. + var GarbageCollector = new GarbageCollection(10000); + var GCThread = new Thread(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(); + Application.ThreadException -= UnhandledThreadException; + AppDomain.CurrentDomain.UnhandledException -= UnhandledException; + } + } + #endregion + + private static void UnhandledException(object sender, UnhandledExceptionEventArgs ea) + { + Log.Error((Exception) ea.ExceptionObject, "Unhandled exception"); + } + + private static void UnhandledThreadException(object sender, ThreadExceptionEventArgs ea) + { + Log.Error(ea.Exception, "Unhandled thread exception"); + } + + // ----------------- + // PRIVATE FUNCTIONS + // ----------------- + + #region Dynamic Driver Assembly Loader + /// + /// Load the assemblies that we will serve via COM. These will be located in the same + /// folder as out executable and will have at least one type decorated with a + /// + /// + /// The count of types found. + private static int LoadComObjectAssemblies() + { + Log.Info("Loading served COM classes"); + try + { + // Discover assemblies and types to be served in Reflection Only context in an isolated AppDomain + using (var reflectionContext = new AppDomainIsolated()) + { + reflectionContext.Worker.DiscoverServedClasses(); + s_ComObjectAssys = new List(reflectionContext.Worker.DiscoveredAssemblyNames); + s_ComObjectTypes = new List(reflectionContext.Worker.DiscoveredTypes); + } + + // Now load the discovered assemblies into the current domain's execution context + foreach (var assemblyName in s_ComObjectAssys) Assembly.Load(assemblyName); + return s_ComObjectTypes.Count; + } + finally + { + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= CurrentDomainOnReflectionOnlyAssemblyResolve; + } + } + + private static Assembly CurrentDomainOnReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args) + { + try + { + Log.Info( + $"Event: ReflectionOnlyResolveAssembly for {args.Name} requested by {args.RequestingAssembly.GetName().Name}", + args.Name); + var resolved = Assembly.ReflectionOnlyLoad(args.Name); + Log.Info($"Successfully resolved assembly with {resolved.FullName}"); + return resolved; + } + catch (FileLoadException ex) + { + Log.Error(ex, $"Failed to resolve assembly: {args.Name}"); + return null; // Let the app raise its own error. + } + } + #endregion + + #region Access to kernel32.dll, user32.dll, and ole32.dll functions + [Flags] + private 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] + private 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] + private 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")] + private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit); + + // CoUninitialize() is used to uninitialize a COM thread. + [DllImport("ole32.dll")] + private 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")] + private 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")] + private 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 ServerStatusDisplay s_MainForm; // Reference to our main form + private static List s_ComObjectAssys; // Dynamically loaded assemblies containing served COM objects + private static List s_ComObjectTypes; // Served COM object types + private static ArrayList s_ClassFactories; // Served COM object class factories + private static readonly string s_appId = "{0bac7ff1-ebe6-4df7-87df-d433a81d65cb}"; // Our AppId + private static readonly object lockObject = new object(); + #endregion + + #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) + { + var wParam = new UIntPtr(0); + var lParam = new IntPtr(0); + PostThreadMessage(MainThreadId, 0x0012, wParam, lParam); + } + } + } + + public static void TerminateLocalServer() + { + if (StartedByCOM) + Application.Exit(); + } + #endregion + + #region COM Registration and Unregistration + // + // Test if running elevated + // + private static bool IsAdministrator + { + get + { + var i = WindowsIdentity.GetCurrent(); + var p = new WindowsPrincipal(i); + return p.IsInRole(WindowsBuiltInRole.Administrator); + } + } + + // + // Elevate by re-running ourselves with elevation dialog + // + private static void ElevateSelf(string arg) + { + var si = new ProcessStartInfo(); + si.Arguments = arg; + si.WorkingDirectory = Environment.CurrentDirectory; + si.FileName = Application.ExecutablePath; + si.Verb = "runas"; + try + { + Process.Start(si); + } + catch (Win32Exception) + { + MessageBox.Show( + "The server was not " + (arg == "/register" ? "registered" : "unregistered") + + " because you did not allow it.", "ASCOM LocalServer", MessageBoxButtons.OK, + MessageBoxIcon.Warning); + } + catch (Exception ex) + { + MessageBox.Show(ex.ToString(), "ASCOM LocalServer", MessageBoxButtons.OK, + MessageBoxIcon.Stop); + } + } + + // + // 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 + // + + var assy = Assembly.GetExecutingAssembly(); + var attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyTitleAttribute)); + var assyTitle = ((AssemblyTitleAttribute) attr).Title; + attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyDescriptionAttribute)); + var assyDescription = ((AssemblyDescriptionAttribute) attr).Description; + + // + // Local server's DCOM/AppID information + // + try + { + // + // HKCR\APPID\appid + // + using (var key = Registry.ClassesRoot.CreateSubKey("APPID\\" + s_appId)) + { + key.SetValue(null, assyDescription); + key.SetValue("AppID", s_appId); + key.SetValue("AuthenticationLevel", 1, RegistryValueKind.DWord); + } + + // + // HKCR\APPID\exename.ext + // + using (var 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, + "ASCOM LocalServer", MessageBoxButtons.OK, MessageBoxIcon.Stop); + return; + } + finally { } + + // + // For each of the driver assemblies + // + foreach (var type in s_ComObjectTypes) + { + var bFail = false; + try + { + // + // HKCR\CLSID\clsid + // + var clsid = Marshal.GenerateGuidForType(type).ToString("B"); + var progid = Marshal.GenerateProgIdForType(type); + //PWGS Generate device type from the Class name + var deviceType = type.Name; + + using (var 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 (var key2 = key.CreateSubKey("Implemented Categories")) + { + key2.CreateSubKey("{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}"); + } + + using (var key2 = key.CreateSubKey("ProgId")) + { + key2.SetValue(null, progid); + } + + key.CreateSubKey("Programmable"); + using (var key2 = key.CreateSubKey("LocalServer32")) + { + key2.SetValue(null, Application.ExecutablePath); + } + } + + // + // HKCR\progid + // + using (var key = Registry.ClassesRoot.CreateSubKey(progid)) + { + key.SetValue(null, assyTitle); + using (var 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 + var 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, + "ASCOM LocalServer", 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 (var type in s_ComObjectTypes) + { + var clsid = Marshal.GenerateGuidForType(type).ToString("B"); + var progid = Marshal.GenerateProgIdForType(type); + var 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 (var type in s_ComObjectTypes) + { + var 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, + "ASCOM LocalServer", 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 + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/LocalServer.snk b/TA.DigitalDomeworks.Server/LocalServer.snk new file mode 100644 index 0000000..97e4406 Binary files /dev/null and b/TA.DigitalDomeworks.Server/LocalServer.snk differ diff --git a/TA.DigitalDomeworks.Server/NLog.Debug.config b/TA.DigitalDomeworks.Server/NLog.Debug.config new file mode 100644 index 0000000..2164fd1 --- /dev/null +++ b/TA.DigitalDomeworks.Server/NLog.Debug.config @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/NLog.Release.config b/TA.DigitalDomeworks.Server/NLog.Release.config new file mode 100644 index 0000000..05f1c09 --- /dev/null +++ b/TA.DigitalDomeworks.Server/NLog.Release.config @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/NLog.config b/TA.DigitalDomeworks.Server/NLog.config new file mode 100644 index 0000000..101befe --- /dev/null +++ b/TA.DigitalDomeworks.Server/NLog.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/NLog.xsd b/TA.DigitalDomeworks.Server/NLog.xsd new file mode 100644 index 0000000..1bd61d8 --- /dev/null +++ b/TA.DigitalDomeworks.Server/NLog.xsd @@ -0,0 +1,3162 @@ + + + + + + + + + + + + + + + Watch config file for changes and reload automatically. + + + + + Print internal NLog messages to the console. Default value is: false + + + + + Print internal NLog messages to the console error output. Default value is: false + + + + + Write internal NLog messages to the specified file. + + + + + Log level threshold for internal log messages. Default value is: Info. + + + + + Global log level threshold for application log messages. Messages below this level won't be logged.. + + + + + Throw an exception when there is an internal error. Default value is: false. + + + + + Throw an exception when there is a configuration error. If not set, determined by throwExceptions. + + + + + Gets or sets a value indicating whether Variables should be kept on configuration reload. Default value is: false. + + + + + Write internal NLog messages to the System.Diagnostics.Trace. Default value is: false. + + + + + Write timestamps for internal NLog messages. Default value is: true. + + + + + Use InvariantCulture as default culture instead of CurrentCulture. Default value is: false. + + + + + Perform mesage template parsing and formatting of LogEvent messages (true = Always, false = Never, empty = Auto Detect). Default value is: empty. + + + + + + + + + + + + + + Make all targets within this section asynchronous (creates additional threads but the calling thread isn't blocked by any target writes). + + + + + + + + + + + + + + + + + Prefix for targets/layout renderers/filters/conditions loaded from this assembly. + + + + + Load NLog extensions from the specified file (*.dll) + + + + + Load NLog extensions from the specified assembly. Assembly name should be fully qualified. + + + + + + + + + + Name of the logger. May include '*' character which acts like a wildcard. Allowed forms are: *, Name, *Name, Name* and *Name* + + + + + Comma separated list of levels that this rule matches. + + + + + Minimum level that this rule matches. + + + + + Maximum level that this rule matches. + + + + + Level that this rule matches. + + + + + Comma separated list of target names. + + + + + Ignore further rules if this one matches. + + + + + Enable or disable logging rule. Disabled rules are ignored. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the file to be included. You could use * wildcard. The name is relative to the name of the current config file. + + + + + Ignore any errors in the include file. + + + + + + + Variable name. + + + + + Variable value. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Number of log events that should be processed in a batch by the lazy writer thread. + + + + + Limit of full s to write before yielding into Performance is better when writing many small batches, than writing a single large batch + + + + + Action to be taken when the lazy writer thread request queue count exceeds the set limit. + + + + + Limit on the number of requests in the lazy writer thread request queue. + + + + + Time in milliseconds to sleep between batches. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + Delay the flush until the LogEvent has been confirmed as written + + + + + Condition expression. Log events who meet this condition will cause a flush on the wrapped target. + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Number of log events to be buffered. + + + + + Timeout (in milliseconds) after which the contents of buffer will be flushed if there's no write in the specified period of time. Use -1 to disable timed flushes. + + + + + Action to take if the buffer overflows. + + + + + Indicates whether to use sliding timeout. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Encoding to be used. + + + + + Instance of that is used to format log messages. + + + + + End of line value if a newline is appended at the end of log message . + + + + + Maximum message size in bytes. + + + + + Indicates whether to append newline at the end of log message. + + + + + Action that should be taken if the will be more connections than . + + + + + Maximum queue size. + + + + + Maximum current connections. 0 = no maximum. + + + + + Indicates whether to keep connection open whenever possible. + + + + + Size of the connection cache (number of connections which are kept alive). + + + + + Network address. + + + + + Action that should be taken if the message is larger than maxMessageSize. + + + + + NDLC item separator. + + + + + NDC item separator. + + + + + Indicates whether to include NLog-specific extensions to log4j schema. + + + + + Indicates whether to include source info (file name and line number) in the information sent over the network. + + + + + Indicates whether to include contents of the stack. + + + + + Indicates whether to include stack contents. + + + + + Indicates whether to include dictionary contents. + + + + + Indicates whether to include dictionary contents. + + + + + Indicates whether to include call site (class and method name) in the information sent over the network. + + + + + Option to include all properties from the log events + + + + + AppInfo field. By default it's the friendly name of the current AppDomain. + + + + + Renderer for log4j:event logger-xml-attribute (Default ${logger}) + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + Layout that should be use to calcuate the value for the parameter. + + + + + Viewer parameter name. + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether to auto-check if the console is available. - Disables console writing if Environment.UserInteractive = False (Windows Service) - Disables console writing if Console Standard Input is not available (Non-Console-App) + + + + + The encoding for writing messages to the . + + + + + Indicates whether the error stream (stderr) should be used instead of the output stream (stdout). + + + + + Indicates whether to use default row highlighting rules. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Condition that must be met in order to set the specified foreground and background color. + + + + + Background color. + + + + + Foreground color. + + + + + + + + + + + + + + + + Compile the ? This can improve the performance, but at the costs of more memory usage. If false, the Regex Cache is used. + + + + + Indicates whether to ignore case when comparing texts. + + + + + Regular expression to be matched. You must specify either text or regex. + + + + + Text to be matched. You must specify either text or regex. + + + + + Indicates whether to match whole words only. + + + + + Background color. + + + + + Foreground color. + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether to auto-check if the console is available - Disables console writing if Environment.UserInteractive = False (Windows Service) - Disables console writing if Console Standard Input is not available (Non-Console-App) + + + + + The encoding for writing messages to the . + + + + + Indicates whether to send the log messages to the standard error instead of the standard output. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Obsolete - value will be ignored! The logging code always runs outside of transaction. Gets or sets a value indicating whether to use database transactions. Some data providers require this. + + + + + Database user name. If the ConnectionString is not provided this value will be used to construct the "User ID=" part of the connection string. + + + + + Name of the database provider. + + + + + Database password. If the ConnectionString is not provided this value will be used to construct the "Password=" part of the connection string. + + + + + Indicates whether to keep the database connection open between the log events. + + + + + Database name. If the ConnectionString is not provided this value will be used to construct the "Database=" part of the connection string. + + + + + Name of the connection string (as specified in <connectionStrings> configuration section. + + + + + Connection string. When provided, it overrides the values specified in DBHost, DBUserName, DBPassword, DBDatabase. + + + + + Database host name. If the ConnectionString is not provided this value will be used to construct the "Server=" part of the connection string. + + + + + Connection string using for installation and uninstallation. If not provided, regular ConnectionString is being used. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + Text of the SQL command to be run on each log level. + + + + + Type of the SQL command to be run on each log level. + + + + + + + + + + + + + + + + + + + + + + + Type of the command. + + + + + Connection string to run the command against. If not provided, connection string from the target is used. + + + + + Indicates whether to ignore failures. + + + + + Command text. + + + + + + + + + + + + + + Layout that should be use to calcuate the value for the parameter. + + + + + Database parameter name. + + + + + Database parameter precision. + + + + + Database parameter scale. + + + + + Database parameter size. + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Layout that renders event Category. + + + + + Optional entrytype. When not set, or when not convertable to then determined by + + + + + Layout that renders event ID. + + + + + Name of the Event Log to write to. This can be System, Application or any user-defined name. + + + + + Name of the machine on which Event Log service is running. + + + + + Maximum Event log size in kilobytes. If null, the value won't be set. Default is 512 Kilobytes as specified by Eventlog API + + + + + Message length limit to write to the Event Log. + + + + + Value to be used as the event Source. + + + + + Action to take if the message is larger than the option. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Indicates whether to return to the first target after any successful write. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + File encoding. + + + + + Line ending mode. + + + + + Indicates whether to compress archive files into the zip archive format. + + + + + Way file archives are numbered. + + + + + Name of the file to be used for an archive. + + + + + Is the an absolute or relative path? + + + + + Indicates whether to automatically archive log files every time the specified time passes. + + + + + Size in bytes above which log files will be automatically archived. Warning: combining this with isn't supported. We cannot create multiple archive files, if they should have the same name. Choose: + + + + + Maximum number of archive files that should be kept. + + + + + Indicates whether the footer should be written only when the file is archived. + + + + + Maximum number of log filenames that should be stored as existing. + + + + + Name of the file to write to. + + + + + Value specifying the date format to use when archiving files. + + + + + Indicates whether to archive old log file on startup. + + + + + Cleanup invalid values in a filename, e.g. slashes in a filename. If set to true, this can impact the performance of massive writes. If set to false, nothing gets written when the filename is wrong. + + + + + Indicates whether to create directories if they do not exist. + + + + + Indicates whether to delete old log file on startup. + + + + + File attributes (Windows only). + + + + + Indicates whether to write BOM (byte order mark) in created files + + + + + Indicates whether to enable log file(s) to be deleted. + + + + + Indicates whether to replace file contents on each write instead of appending log message at the end. + + + + + Value indicationg whether file creation calls should be synchronized by a system global mutex. + + + + + Gets or set a value indicating whether a managed file stream is forced, instead of using the native implementation. + + + + + Is the an absolute or relative path? + + + + + Indicates whether concurrent writes to the log file by multiple processes on the same host. + + + + + Whether or not this target should just discard all data that its asked to write. Mostly used for when testing NLog Stack except final write + + + + + Delay in milliseconds to wait before attempting to write to the file again. + + + + + Indicates whether concurrent writes to the log file by multiple processes on different network hosts. + + + + + Number of files to be kept open. Setting this to a higher value may improve performance in a situation where a single File target is writing to many files (such as splitting by level or by logger). + + + + + Maximum number of seconds that files are kept open. If this number is negative the files are not automatically closed after a period of inactivity. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + Log file buffer size in bytes. + + + + + Indicates whether to automatically flush the file buffers after each log message. + + + + + Number of times the write is appended on the file before NLog discards the log message. + + + + + Indicates whether to keep log file open instead of opening and closing it on each logging event. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Condition expression. Log events who meet this condition will be forwarded to the wrapped target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Windows domain name to change context to. + + + + + Required impersonation level. + + + + + Type of the logon provider. + + + + + Logon Type. + + + + + User account password. + + + + + Indicates whether to revert to the credentials of the process instead of impersonating another user. + + + + + Username to change context to. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Interval in which messages will be written up to the number of messages. + + + + + Maximum allowed number of messages written per . + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Endpoint address. + + + + + Name of the endpoint configuration in WCF configuration file. + + + + + Indicates whether to use a WCF service contract that is one way (fire and forget) or two way (request-reply) + + + + + Client ID. + + + + + Indicates whether to include per-event properties in the payload sent to the server. + + + + + Indicates whether to use binary message encoding. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + Layout that should be use to calculate the value for the parameter. + + + + + Name of the parameter. + + + + + Type of the parameter. + + + + + Type of the parameter. Obsolete alias for + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Text to be rendered. + + + + + Header. + + + + + Footer. + + + + + Indicates whether NewLine characters in the body should be replaced with tags. + + + + + Priority used for sending mails. + + + + + Encoding to be used for sending e-mail. + + + + + BCC email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). + + + + + CC email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). + + + + + Indicates whether to add new lines between log entries. + + + + + Indicates whether to send message as HTML instead of plain text. + + + + + Sender's email address (e.g. joe@domain.com). + + + + + Mail message body (repeated for each log message send in one mail). + + + + + Mail subject. + + + + + Recipients' email addresses separated by semicolons (e.g. john@domain.com;jane@domain.com). + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + Indicates the SMTP client timeout. + + + + + SMTP Server to be used for sending. + + + + + SMTP Authentication mode. + + + + + Username used to connect to SMTP server (used when SmtpAuthentication is set to "basic"). + + + + + Password used to authenticate against SMTP server (used when SmtpAuthentication is set to "basic"). + + + + + Indicates whether SSL (secure sockets layer) should be used when communicating with SMTP server. + + + + + Port number that SMTP Server is listening on. + + + + + Indicates whether the default Settings from System.Net.MailSettings should be used. + + + + + Folder where applications save mail messages to be processed by the local SMTP server. + + + + + Specifies how outgoing email messages will be handled. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Class name. + + + + + Method name. The method must be public and static. Use the AssemblyQualifiedName , https://msdn.microsoft.com/en-us/library/system.type.assemblyqualifiedname(v=vs.110).aspx e.g. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Encoding to be used. + + + + + End of line value if a newline is appended at the end of log message . + + + + + Maximum message size in bytes. + + + + + Indicates whether to append newline at the end of log message. + + + + + Network address. + + + + + Size of the connection cache (number of connections which are kept alive). + + + + + Indicates whether to keep connection open whenever possible. + + + + + Maximum current connections. 0 = no maximum. + + + + + Maximum queue size. + + + + + Action that should be taken if the will be more connections than . + + + + + Action that should be taken if the message is larger than maxMessageSize. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Encoding to be used. + + + + + Instance of that is used to format log messages. + + + + + End of line value if a newline is appended at the end of log message . + + + + + Maximum message size in bytes. + + + + + Indicates whether to append newline at the end of log message. + + + + + Action that should be taken if the will be more connections than . + + + + + Maximum queue size. + + + + + Maximum current connections. 0 = no maximum. + + + + + Indicates whether to keep connection open whenever possible. + + + + + Size of the connection cache (number of connections which are kept alive). + + + + + Network address. + + + + + Action that should be taken if the message is larger than maxMessageSize. + + + + + NDLC item separator. + + + + + NDC item separator. + + + + + Indicates whether to include NLog-specific extensions to log4j schema. + + + + + Indicates whether to include source info (file name and line number) in the information sent over the network. + + + + + Indicates whether to include contents of the stack. + + + + + Indicates whether to include stack contents. + + + + + Indicates whether to include dictionary contents. + + + + + Indicates whether to include dictionary contents. + + + + + Indicates whether to include call site (class and method name) in the information sent over the network. + + + + + Option to include all properties from the log events + + + + + AppInfo field. By default it's the friendly name of the current AppDomain. + + + + + Renderer for log4j:event logger-xml-attribute (Default ${logger}) + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Indicates whether to perform layout calculation. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Indicates whether performance counter should be automatically created. + + + + + Name of the performance counter category. + + + + + Counter help text. + + + + + Name of the performance counter. + + + + + Performance counter type. + + + + + The value by which to increment the counter. + + + + + Performance counter instance name. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Default filter to be applied when no specific rule matches. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + Condition to be tested. + + + + + Resulting filter to be applied when the condition matches. + + + + + + + + + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + Number of times to repeat each log message. + + + + + + + + + + + + + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + Number of retries that should be attempted on the wrapped target in case of a failure. + + + + + Time to wait between retries in milliseconds. + + + + + + + + + + + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + Name of the target. + + + + + Layout used to format log messages. + + + + + Always use independent of + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Name of the target. + + + + + Target supports reuse of internal buffers, and doesn't have to constantly allocate new buffers Required for legacy NLog-targets, that expects buffers to remain stable after Write-method exit + + + + + Should we include the BOM (Byte-order-mark) for UTF? Influences the property. This will only work for UTF-8. + + + + + Web service method name. Only used with Soap. + + + + + Web service namespace. Only used with Soap. + + + + + Protocol to be used when calling web service. + + + + + Custom proxy address, include port separated by a colon + + + + + Encoding. + + + + + Web service URL. + + + + + Value whether escaping be done according to the old NLog style (Very non-standard) + + + + + Value whether escaping be done according to Rfc3986 (Supports Internationalized Resource Identifiers - IRIs) + + + + + Indicates whether to pre-authenticate the HttpWebRequest (Requires 'Authorization' in parameters) + + + + + Name of the root XML element, if POST of XML document chosen. If so, this property must not be null. (see and ). + + + + + (optional) root namespace of the XML document, if POST of XML document chosen. (see and ). + + + + + Proxy configuration when calling web service + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Footer layout. + + + + + Header layout. + + + + + Body layout (can be repeated multiple times). + + + + + Custom column delimiter value (valid when ColumnDelimiter is set to 'Custom'). + + + + + Column delimiter. + + + + + Quote Character. + + + + + Quoting mode. + + + + + Indicates whether CVS should include header. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Layout of the column. + + + + + Name of the column. + + + + + + + + + + + + + + + + + + + List of property names to exclude when is true + + + + + Option to include all properties from the log event (as JSON) + + + + + Indicates whether to include contents of the dictionary. + + + + + Indicates whether to include contents of the dictionary. + + + + + Option to render the empty object value {} + + + + + Option to suppress the extra spaces in the output json + + + + + How far should the JSON serializer follow object references before backing off + + + + + + + + + + + + + + + + Layout that will be rendered as the attribute's value. + + + + + Name of the attribute. + + + + + Determines wether or not this attribute will be Json encoded. + + + + + Indicates whether to escape non-ascii characters + + + + + Whether an attribute with empty value should be included in the output + + + + + + + + + + + + + + Footer layout. + + + + + Header layout. + + + + + Body layout (can be repeated multiple times). + + + + + + + + + + + + + + + + + + Option to include all properties from the log events + + + + + Indicates whether to include contents of the dictionary. + + + + + Indicates whether to include contents of the dictionary. + + + + + Indicates whether to include contents of the stack. + + + + + Indicates whether to include contents of the stack. + + + + + + + + + + + + + + Layout text. + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Condition expression. + + + + + + + + + + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + Substring to be matched. + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + String to compare the layout to. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + Substring to be matched. + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + String to compare the layout to. + + + + + Indicates whether to ignore case when comparing strings. + + + + + Layout to be used to filter log messages. + + + + + + + + + + + + + + + + + + + + + + + + Action to be taken when filter matches. + + + + + Default number of unique filter values to expect, will automatically increase if needed + + + + + Applies the configured action to the initial logevent that starts the timeout period. Used to configure that it should ignore all events until timeout. + + + + + Layout to be used to filter log messages. + + + + + Max number of unique filter values to expect simultaneously + + + + + Max length of filter values, will truncate if above limit + + + + + How long before a filter expires, and logging is accepted again + + + + + Default buffer size for the internal buffers + + + + + Reuse internal buffers, and doesn't have to constantly allocate new buffers + + + + + Append FilterCount to the when an event is no longer filtered + + + + + Insert FilterCount value into when an event is no longer filtered + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/Properties/AssemblyInfo.cs b/TA.DigitalDomeworks.Server/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6c5d06e --- /dev/null +++ b/TA.DigitalDomeworks.Server/Properties/AssemblyInfo.cs @@ -0,0 +1,14 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: AssemblyInfo.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System.Reflection; + +// 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 server for Digital Domeworks")] +[assembly: AssemblyDescription("ASCOM multi-interface server for Digital Domeworks")] \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/Properties/Resources.Designer.cs b/TA.DigitalDomeworks.Server/Properties/Resources.Designer.cs new file mode 100644 index 0000000..bd30fea --- /dev/null +++ b/TA.DigitalDomeworks.Server/Properties/Resources.Designer.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// 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 TA.DigitalDomeworks.Server.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", "15.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("TA.DigitalDomeworks.Server.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.Bitmap. + /// + internal static System.Drawing.Bitmap AuroraWideWithText { + get { + object obj = ResourceManager.GetObject("AuroraWideWithText", 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)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap DigitalDomeworks { + get { + object obj = ResourceManager.GetObject("DigitalDomeworks", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap TigraAstronomyLogo { + get { + object obj = ResourceManager.GetObject("TigraAstronomyLogo", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/TA.DigitalDomeworks.Server/Properties/Resources.resx b/TA.DigitalDomeworks.Server/Properties/Resources.resx new file mode 100644 index 0000000..c69d913 --- /dev/null +++ b/TA.DigitalDomeworks.Server/Properties/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\AuroraWideWithText.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\ASCOM.ico;System.Drawing.Icon, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\TiGra Astronomy logo 800x900.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\DigitalDomeworks.jpg;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/Properties/Settings.Designer.cs b/TA.DigitalDomeworks.Server/Properties/Settings.Designer.cs new file mode 100644 index 0000000..07960fc --- /dev/null +++ b/TA.DigitalDomeworks.Server/Properties/Settings.Designer.cs @@ -0,0 +1,152 @@ +//------------------------------------------------------------------------------ +// +// 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 TA.DigitalDomeworks.Server.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.6.0.0")] + public 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; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("COM1")] + public string CommPortName { + get { + return ((string)(this["CommPortName"])); + } + set { + this["CommPortName"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("COM1:9600")] + public string ConnectionString { + get { + return ((string)(this["ConnectionString"])); + } + set { + this["ConnectionString"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0, 0")] + public global::System.Drawing.Point SetupDialogLocation { + get { + return ((global::System.Drawing.Point)(this["SetupDialogLocation"])); + } + set { + this["SetupDialogLocation"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0, 0")] + public global::System.Drawing.Point MainFormLocation { + get { + return ((global::System.Drawing.Point)(this["MainFormLocation"])); + } + set { + this["MainFormLocation"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool PerformShutterRecovery { + get { + return ((bool)(this["PerformShutterRecovery"])); + } + set { + this["PerformShutterRecovery"] = value; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("00:03:00")] + public global::System.TimeSpan KeepAliveTimerPeriod { + get { + return ((global::System.TimeSpan)(this["KeepAliveTimerPeriod"])); + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("60")] + public decimal FullRotationTimeSeconds { + get { + return ((decimal)(this["FullRotationTimeSeconds"])); + } + set { + this["FullRotationTimeSeconds"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("120")] + public decimal ShutterOpenCloseTimeSeconds { + get { + return ((decimal)(this["ShutterOpenCloseTimeSeconds"])); + } + set { + this["ShutterOpenCloseTimeSeconds"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool IgnoreHardwareShutterSensor { + get { + return ((bool)(this["IgnoreHardwareShutterSensor"])); + } + set { + this["IgnoreHardwareShutterSensor"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("10")] + public int CurrentDrawDetectionThreshold { + get { + return ((int)(this["CurrentDrawDetectionThreshold"])); + } + set { + this["CurrentDrawDetectionThreshold"] = value; + } + } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("00:00:05")] + public global::System.TimeSpan ShutterTickTimeout { + get { + return ((global::System.TimeSpan)(this["ShutterTickTimeout"])); + } + } + } +} diff --git a/TA.DigitalDomeworks.Server/Properties/Settings.settings b/TA.DigitalDomeworks.Server/Properties/Settings.settings new file mode 100644 index 0000000..d57e0e5 --- /dev/null +++ b/TA.DigitalDomeworks.Server/Properties/Settings.settings @@ -0,0 +1,39 @@ + + + + + + COM1 + + + COM1:9600 + + + 0, 0 + + + 0, 0 + + + True + + + 00:03:00 + + + 60 + + + 120 + + + False + + + 10 + + + 00:00:05 + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ReferenceCountedObject.cs b/TA.DigitalDomeworks.Server/ReferenceCountedObject.cs new file mode 100644 index 0000000..0c7d71d --- /dev/null +++ b/TA.DigitalDomeworks.Server/ReferenceCountedObject.cs @@ -0,0 +1,29 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ReferenceCountedObject.cs Last modified: 2018-03-28@22:19 by Tim Long + +using System.Runtime.InteropServices; + +namespace TA.DigitalDomeworks.Server + { + [ComVisible(false)] + public abstract class ReferenceCountedObject + { + public ReferenceCountedObject() + { + // We increment the global count of objects. + Server.CountObject(); + } + + ~ReferenceCountedObject() + { + // 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(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/Resources/ASCOM.bmp b/TA.DigitalDomeworks.Server/Resources/ASCOM.bmp new file mode 100644 index 0000000..55516c7 Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/ASCOM.bmp differ diff --git a/TA.DigitalDomeworks.Server/Resources/ASCOM.ico b/TA.DigitalDomeworks.Server/Resources/ASCOM.ico new file mode 100644 index 0000000..9bf8f41 Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/ASCOM.ico differ diff --git a/TA.DigitalDomeworks.Server/Resources/ASCOM.png b/TA.DigitalDomeworks.Server/Resources/ASCOM.png new file mode 100644 index 0000000..a83b77b Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/ASCOM.png differ diff --git a/TA.DigitalDomeworks.Server/Resources/AuroraWideWithText.png b/TA.DigitalDomeworks.Server/Resources/AuroraWideWithText.png new file mode 100644 index 0000000..846d9ce Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/AuroraWideWithText.png differ diff --git a/TA.DigitalDomeworks.Server/Resources/DigitalDomeworks.jpg b/TA.DigitalDomeworks.Server/Resources/DigitalDomeworks.jpg new file mode 100644 index 0000000..3f7e43c Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/DigitalDomeworks.jpg differ diff --git a/TA.DigitalDomeworks.Server/Resources/Integra-85-focuser-rotators.jpg b/TA.DigitalDomeworks.Server/Resources/Integra-85-focuser-rotators.jpg new file mode 100644 index 0000000..481aef4 Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/Integra-85-focuser-rotators.jpg differ diff --git a/TA.DigitalDomeworks.Server/Resources/TiGra Astronomy Icon 256x256.png b/TA.DigitalDomeworks.Server/Resources/TiGra Astronomy Icon 256x256.png new file mode 100644 index 0000000..a16d8b5 Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/TiGra Astronomy Icon 256x256.png differ diff --git a/TA.DigitalDomeworks.Server/Resources/TiGra Astronomy logo 800x900.png b/TA.DigitalDomeworks.Server/Resources/TiGra Astronomy logo 800x900.png new file mode 100644 index 0000000..fa0abc9 Binary files /dev/null and b/TA.DigitalDomeworks.Server/Resources/TiGra Astronomy logo 800x900.png differ diff --git a/TA.DigitalDomeworks.Server/ServedComClassLocator.cs b/TA.DigitalDomeworks.Server/ServedComClassLocator.cs new file mode 100644 index 0000000..a472a96 --- /dev/null +++ b/TA.DigitalDomeworks.Server/ServedComClassLocator.cs @@ -0,0 +1,142 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ServedComClassLocator.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using ASCOM; +using NLog; + +namespace TA.DigitalDomeworks.Server + { + internal class ServedComClassLocator : MarshalByRefObject + { + private readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + public ServedComClassLocator() + { + DiscoveredAssemblyNames = new List(); + DiscoveredTypes = new List(); + } + + /// + /// Gets the list of the names of assemblies that contain ASCOM drivers to be served by the LocalServer. + /// + /// The discovered assembly full names. + public List DiscoveredAssemblyNames { get; } + + /// + /// Gets the discovered types that were found to be decorated with the + /// attribute. + /// + /// A list of the discovered types. + public List DiscoveredTypes { get; } + + /// + /// Discovers types (and their declaring assemblies) that are decorated with the + /// , which identifies an ASCOM driver that should be served + /// by the LocalServer. + /// + /// + /// Assemblies are loaded using to prevent any code execution. + /// This would otherwise be a potential malware attack vector, since untrusted assemblies would be loaded and + /// potentially into + /// a privileged user context. If any of the loaded assemblies must later execute, then this operation should be + /// performed + /// in an isolated application domain so that the domain and all the loaded assemblies can later be unloaded. Unloaing + /// an entire + /// AppDomain is the only way to remove assemblies from memory. + /// The class inherits from specifically so that it can be proxied across an + /// App Domain boundary. + /// + /// + public void DiscoverServedClasses() + { + Log.Info("Loading served COM classes"); + + // put everything into one folder, the same as the server. + var assyPath = Assembly.GetExecutingAssembly().Location; + assyPath = Path.GetDirectoryName(assyPath); + Log.Debug($"Assembly load path is {assyPath}"); + + var d = new DirectoryInfo(assyPath); + var fileInfos = d.GetFiles("*.dll"); + Log.Debug($"Discovered {fileInfos.Length} candidate assemblies"); + try + { + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += HandleReflectionOnlyAssemblyResolve; + foreach (var fi in fileInfos) + { + Log.Trace($"Examining types in {fi.Name}"); + var aPath = fi.FullName; + try + { + Log.Trace($"Attempting reflection only load for {aPath}"); + var so = Assembly.ReflectionOnlyLoad(Path.GetFileNameWithoutExtension(fi.Name)); + var soShortName = so.GetName().Name; + var types = so.GetTypes(); + Log.Trace($"Reflection found {types.Length} types in assembly {so.FullName}"); + var servedClasses = from type in types.AsParallel() + let memberInfo = (MemberInfo) type + let safeAttributes = CustomAttributeData.GetCustomAttributes(memberInfo) + where safeAttributes.Any( + p => p.AttributeType.Name == nameof(ServedClassNameAttribute)) + select type; + var discoveredTypes = servedClasses.ToList(); + if (discoveredTypes.Any()) + { + DiscoveredTypes.AddRange(discoveredTypes); + DiscoveredAssemblyNames.Add(so.FullName); + Log.Warn($"Discovered {discoveredTypes.Count} served clases in assembly {soShortName}"); + } + } + catch (BadImageFormatException ex) + { + Log.Warn(ex, $"BadImageFormat: {fi.Name}.{fi.Extension} continuing"); + // Probably an attempt to load a Win32 DLL (i.e. not a .net assembly) + // Just swallow the exception and continue to the next item. + } + catch (Exception ex) + { + Log.Error(ex, $"Unexpected error processing {fi.Name}: {ex.Message}"); + //return false; + } + } + } + finally + { + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= HandleReflectionOnlyAssemblyResolve; + } + } + + + /// + /// Handles the reflection only assembly resolve. + /// + /// The sender. + /// The instance containing the event data. + /// Assembly. + private Assembly HandleReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args) + { + try + { + Log.Info( + $"Event: ReflectionOnlyResolveAssembly for {args.Name} requested by {args.RequestingAssembly.GetName().Name}", + args.Name); + var resolved = Assembly.ReflectionOnlyLoad(args.Name); + Log.Info($"Successfully resolved assembly with {resolved.FullName}"); + return resolved; + } + catch (FileLoadException ex) + { + Log.Error(ex, $"Failed to resolve assembly: {args.Name}"); + return null; // Let the app raise its own error. + } + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ServerStatusDisplay.Designer.cs b/TA.DigitalDomeworks.Server/ServerStatusDisplay.Designer.cs new file mode 100644 index 0000000..840aa34 --- /dev/null +++ b/TA.DigitalDomeworks.Server/ServerStatusDisplay.Designer.cs @@ -0,0 +1,516 @@ +namespace TA.DigitalDomeworks.Server +{ + partial class ServerStatusDisplay + { + /// + /// 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.registeredClientCount = new System.Windows.Forms.Label(); + this.OnlineClients = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.annunciatorPanel1 = new ASCOM.Controls.AnnunciatorPanel(); + this.RotationTitle = new ASCOM.Controls.Annunciator(); + this.CounterClockwiseAnnunciator = new ASCOM.Controls.Annunciator(); + this.AzimuthMotorAnnunciator = new ASCOM.Controls.Annunciator(); + this.ClockwiseAnnunciator = new ASCOM.Controls.Annunciator(); + this.AzimuthPositionAnnunciator = new ASCOM.Controls.Annunciator(); + this.AtHomeAnnunciator = new ASCOM.Controls.Annunciator(); + this.UserPin1Annunciator = new ASCOM.Controls.Annunciator(); + this.UserPin2Annunciator = new ASCOM.Controls.Annunciator(); + this.UserPin3Annunciator = new ASCOM.Controls.Annunciator(); + this.UserPin4Annunciator = new ASCOM.Controls.Annunciator(); + this.ShutterTitle = new ASCOM.Controls.Annunciator(); + this.ShutterOpeningAnnunciator = new ASCOM.Controls.Annunciator(); + this.ShutterMotorAnnunciator = new ASCOM.Controls.Annunciator(); + this.ShutterClosingAnnunciator = new ASCOM.Controls.Annunciator(); + this.ShutterCurrentAnnunciator = new ASCOM.Controls.Annunciator(); + this.ShutterOpenAnnunciator = new ASCOM.Controls.Annunciator(); + this.ShutterClosedAnnunciator = new ASCOM.Controls.Annunciator(); + this.ShutterIndeterminateAnnunciator = new ASCOM.Controls.Annunciator(); + this.SetupCommand = new System.Windows.Forms.Button(); + this.ShutterCurrentBar = new System.Windows.Forms.ProgressBar(); + this.OpenButton = new System.Windows.Forms.Button(); + this.CloseButton = new System.Windows.Forms.Button(); + this.label2 = new System.Windows.Forms.Label(); + this.annunciatorPanel1.SuspendLayout(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(93, 75); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(94, 13); + this.label1.TabIndex = 0; + this.label1.Text = "Registered clients:"; + // + // registeredClientCount + // + this.registeredClientCount.AutoSize = true; + this.registeredClientCount.Location = new System.Drawing.Point(193, 75); + this.registeredClientCount.Name = "registeredClientCount"; + this.registeredClientCount.Size = new System.Drawing.Size(13, 13); + this.registeredClientCount.TabIndex = 1; + this.registeredClientCount.Text = "0"; + // + // OnlineClients + // + this.OnlineClients.AutoSize = true; + this.OnlineClients.Location = new System.Drawing.Point(291, 75); + this.OnlineClients.Name = "OnlineClients"; + this.OnlineClients.Size = new System.Drawing.Size(13, 13); + this.OnlineClients.TabIndex = 3; + this.OnlineClients.Text = "0"; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(245, 75); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(40, 13); + this.label3.TabIndex = 2; + this.label3.Text = "Online:"; + // + // annunciatorPanel1 + // + this.annunciatorPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.annunciatorPanel1.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.annunciatorPanel1.Controls.Add(this.RotationTitle); + this.annunciatorPanel1.Controls.Add(this.CounterClockwiseAnnunciator); + this.annunciatorPanel1.Controls.Add(this.AzimuthMotorAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ClockwiseAnnunciator); + this.annunciatorPanel1.Controls.Add(this.AzimuthPositionAnnunciator); + this.annunciatorPanel1.Controls.Add(this.AtHomeAnnunciator); + this.annunciatorPanel1.Controls.Add(this.UserPin1Annunciator); + this.annunciatorPanel1.Controls.Add(this.UserPin2Annunciator); + this.annunciatorPanel1.Controls.Add(this.UserPin3Annunciator); + this.annunciatorPanel1.Controls.Add(this.UserPin4Annunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterTitle); + this.annunciatorPanel1.Controls.Add(this.ShutterOpeningAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterMotorAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterClosingAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterCurrentAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterOpenAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterClosedAnnunciator); + this.annunciatorPanel1.Controls.Add(this.ShutterIndeterminateAnnunciator); + this.annunciatorPanel1.Location = new System.Drawing.Point(96, 12); + this.annunciatorPanel1.Name = "annunciatorPanel1"; + this.annunciatorPanel1.Size = new System.Drawing.Size(387, 37); + this.annunciatorPanel1.TabIndex = 5; + // + // RotationTitle + // + this.RotationTitle.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.RotationTitle.AutoSize = true; + this.RotationTitle.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.RotationTitle.Font = new System.Drawing.Font("Consolas", 10F); + this.RotationTitle.ForeColor = System.Drawing.Color.White; + this.RotationTitle.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.RotationTitle.Location = new System.Drawing.Point(3, 0); + this.RotationTitle.Mute = false; + this.RotationTitle.Name = "RotationTitle"; + this.RotationTitle.Size = new System.Drawing.Size(64, 17); + this.RotationTitle.TabIndex = 1; + this.RotationTitle.Text = "Azimuth"; + // + // CounterClockwiseAnnunciator + // + this.CounterClockwiseAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.CounterClockwiseAnnunciator.AutoSize = true; + this.CounterClockwiseAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.CounterClockwiseAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.CounterClockwiseAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.CounterClockwiseAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.CounterClockwiseAnnunciator.Location = new System.Drawing.Point(73, 0); + this.CounterClockwiseAnnunciator.Mute = false; + this.CounterClockwiseAnnunciator.Name = "CounterClockwiseAnnunciator"; + this.CounterClockwiseAnnunciator.Size = new System.Drawing.Size(16, 17); + this.CounterClockwiseAnnunciator.TabIndex = 2; + this.CounterClockwiseAnnunciator.Text = "â—„"; + // + // AzimuthMotorAnnunciator + // + this.AzimuthMotorAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AzimuthMotorAnnunciator.AutoSize = true; + this.AzimuthMotorAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.AzimuthMotorAnnunciator.Cadence = ASCOM.Controls.CadencePattern.BlinkAlarm; + this.AzimuthMotorAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.AzimuthMotorAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AzimuthMotorAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AzimuthMotorAnnunciator.Location = new System.Drawing.Point(95, 0); + this.AzimuthMotorAnnunciator.Mute = false; + this.AzimuthMotorAnnunciator.Name = "AzimuthMotorAnnunciator"; + this.AzimuthMotorAnnunciator.Size = new System.Drawing.Size(48, 17); + this.AzimuthMotorAnnunciator.TabIndex = 3; + this.AzimuthMotorAnnunciator.Text = "Motor"; + // + // ClockwiseAnnunciator + // + this.ClockwiseAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ClockwiseAnnunciator.AutoSize = true; + this.ClockwiseAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ClockwiseAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ClockwiseAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ClockwiseAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ClockwiseAnnunciator.Location = new System.Drawing.Point(149, 0); + this.ClockwiseAnnunciator.Mute = false; + this.ClockwiseAnnunciator.Name = "ClockwiseAnnunciator"; + this.ClockwiseAnnunciator.Size = new System.Drawing.Size(16, 17); + this.ClockwiseAnnunciator.TabIndex = 4; + this.ClockwiseAnnunciator.Text = "â–º"; + // + // AzimuthPositionAnnunciator + // + this.AzimuthPositionAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AzimuthPositionAnnunciator.AutoSize = true; + this.AzimuthPositionAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.AzimuthPositionAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.AzimuthPositionAnnunciator.ForeColor = System.Drawing.Color.PaleGoldenrod; + this.AzimuthPositionAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AzimuthPositionAnnunciator.Location = new System.Drawing.Point(171, 0); + this.AzimuthPositionAnnunciator.Mute = false; + this.AzimuthPositionAnnunciator.Name = "AzimuthPositionAnnunciator"; + this.AzimuthPositionAnnunciator.Size = new System.Drawing.Size(40, 17); + this.AzimuthPositionAnnunciator.TabIndex = 5; + this.AzimuthPositionAnnunciator.Tag = "{0:D3}°"; + this.AzimuthPositionAnnunciator.Text = "000°"; + // + // AtHomeAnnunciator + // + this.AtHomeAnnunciator.ActiveColor = System.Drawing.Color.DarkSeaGreen; + this.AtHomeAnnunciator.AutoSize = true; + this.AtHomeAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.AtHomeAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.AtHomeAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AtHomeAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.AtHomeAnnunciator.Location = new System.Drawing.Point(217, 0); + this.AtHomeAnnunciator.Mute = false; + this.AtHomeAnnunciator.Name = "AtHomeAnnunciator"; + this.AtHomeAnnunciator.Size = new System.Drawing.Size(40, 17); + this.AtHomeAnnunciator.TabIndex = 12; + this.AtHomeAnnunciator.Text = "Home"; + // + // UserPin1Annunciator + // + this.UserPin1Annunciator.ActiveColor = System.Drawing.Color.DarkSeaGreen; + this.UserPin1Annunciator.AutoSize = true; + this.UserPin1Annunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.UserPin1Annunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.UserPin1Annunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin1Annunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin1Annunciator.Location = new System.Drawing.Point(263, 0); + this.UserPin1Annunciator.Mute = false; + this.UserPin1Annunciator.Name = "UserPin1Annunciator"; + this.UserPin1Annunciator.Size = new System.Drawing.Size(24, 17); + this.UserPin1Annunciator.TabIndex = 16; + this.UserPin1Annunciator.Text = " 1"; + // + // UserPin2Annunciator + // + this.UserPin2Annunciator.ActiveColor = System.Drawing.Color.DarkSeaGreen; + this.UserPin2Annunciator.AutoSize = true; + this.UserPin2Annunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.UserPin2Annunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.UserPin2Annunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin2Annunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin2Annunciator.Location = new System.Drawing.Point(293, 0); + this.UserPin2Annunciator.Mute = false; + this.UserPin2Annunciator.Name = "UserPin2Annunciator"; + this.UserPin2Annunciator.Size = new System.Drawing.Size(24, 17); + this.UserPin2Annunciator.TabIndex = 17; + this.UserPin2Annunciator.Text = " 2"; + // + // UserPin3Annunciator + // + this.UserPin3Annunciator.ActiveColor = System.Drawing.Color.DarkSeaGreen; + this.UserPin3Annunciator.AutoSize = true; + this.UserPin3Annunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.UserPin3Annunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.UserPin3Annunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin3Annunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin3Annunciator.Location = new System.Drawing.Point(323, 0); + this.UserPin3Annunciator.Mute = false; + this.UserPin3Annunciator.Name = "UserPin3Annunciator"; + this.UserPin3Annunciator.Size = new System.Drawing.Size(24, 17); + this.UserPin3Annunciator.TabIndex = 17; + this.UserPin3Annunciator.Text = " 3"; + // + // UserPin4Annunciator + // + this.UserPin4Annunciator.ActiveColor = System.Drawing.Color.DarkSeaGreen; + this.UserPin4Annunciator.AutoSize = true; + this.UserPin4Annunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.UserPin4Annunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.UserPin4Annunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin4Annunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.UserPin4Annunciator.Location = new System.Drawing.Point(353, 0); + this.UserPin4Annunciator.Mute = false; + this.UserPin4Annunciator.Name = "UserPin4Annunciator"; + this.UserPin4Annunciator.Size = new System.Drawing.Size(24, 17); + this.UserPin4Annunciator.TabIndex = 17; + this.UserPin4Annunciator.Text = " 4"; + // + // ShutterTitle + // + this.ShutterTitle.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterTitle.AutoSize = true; + this.ShutterTitle.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterTitle.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterTitle.ForeColor = System.Drawing.Color.White; + this.ShutterTitle.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterTitle.Location = new System.Drawing.Point(3, 17); + this.ShutterTitle.Mute = false; + this.ShutterTitle.Name = "ShutterTitle"; + this.ShutterTitle.Size = new System.Drawing.Size(64, 17); + this.ShutterTitle.TabIndex = 6; + this.ShutterTitle.Text = "Shutter"; + // + // ShutterOpeningAnnunciator + // + this.ShutterOpeningAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterOpeningAnnunciator.AutoSize = true; + this.ShutterOpeningAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterOpeningAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterOpeningAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterOpeningAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterOpeningAnnunciator.Location = new System.Drawing.Point(73, 17); + this.ShutterOpeningAnnunciator.Mute = false; + this.ShutterOpeningAnnunciator.Name = "ShutterOpeningAnnunciator"; + this.ShutterOpeningAnnunciator.Size = new System.Drawing.Size(16, 17); + this.ShutterOpeningAnnunciator.TabIndex = 7; + this.ShutterOpeningAnnunciator.Text = "â–²"; + // + // ShutterMotorAnnunciator + // + this.ShutterMotorAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterMotorAnnunciator.AutoSize = true; + this.ShutterMotorAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterMotorAnnunciator.Cadence = ASCOM.Controls.CadencePattern.BlinkAlarm; + this.ShutterMotorAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterMotorAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterMotorAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterMotorAnnunciator.Location = new System.Drawing.Point(95, 17); + this.ShutterMotorAnnunciator.Mute = false; + this.ShutterMotorAnnunciator.Name = "ShutterMotorAnnunciator"; + this.ShutterMotorAnnunciator.Size = new System.Drawing.Size(48, 17); + this.ShutterMotorAnnunciator.TabIndex = 8; + this.ShutterMotorAnnunciator.Text = "Motor"; + // + // ShutterClosingAnnunciator + // + this.ShutterClosingAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterClosingAnnunciator.AutoSize = true; + this.ShutterClosingAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterClosingAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterClosingAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterClosingAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterClosingAnnunciator.Location = new System.Drawing.Point(149, 17); + this.ShutterClosingAnnunciator.Mute = false; + this.ShutterClosingAnnunciator.Name = "ShutterClosingAnnunciator"; + this.ShutterClosingAnnunciator.Size = new System.Drawing.Size(16, 17); + this.ShutterClosingAnnunciator.TabIndex = 9; + this.ShutterClosingAnnunciator.Text = "â–¼"; + // + // ShutterCurrentAnnunciator + // + this.ShutterCurrentAnnunciator.ActiveColor = System.Drawing.Color.LightGoldenrodYellow; + this.ShutterCurrentAnnunciator.AutoSize = true; + this.ShutterCurrentAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterCurrentAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterCurrentAnnunciator.ForeColor = System.Drawing.Color.LightGoldenrodYellow; + this.ShutterCurrentAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterCurrentAnnunciator.Location = new System.Drawing.Point(171, 17); + this.ShutterCurrentAnnunciator.Mute = false; + this.ShutterCurrentAnnunciator.Name = "ShutterCurrentAnnunciator"; + this.ShutterCurrentAnnunciator.Size = new System.Drawing.Size(32, 17); + this.ShutterCurrentAnnunciator.TabIndex = 10; + this.ShutterCurrentAnnunciator.Tag = "{0:D3}"; + this.ShutterCurrentAnnunciator.Text = "000"; + // + // ShutterOpenAnnunciator + // + this.ShutterOpenAnnunciator.ActiveColor = System.Drawing.Color.LightGoldenrodYellow; + this.ShutterOpenAnnunciator.AutoSize = true; + this.ShutterOpenAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterOpenAnnunciator.Cadence = ASCOM.Controls.CadencePattern.Wink; + this.ShutterOpenAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterOpenAnnunciator.ForeColor = System.Drawing.Color.LightGoldenrodYellow; + this.ShutterOpenAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterOpenAnnunciator.Location = new System.Drawing.Point(209, 17); + this.ShutterOpenAnnunciator.Mute = false; + this.ShutterOpenAnnunciator.Name = "ShutterOpenAnnunciator"; + this.ShutterOpenAnnunciator.Size = new System.Drawing.Size(40, 17); + this.ShutterOpenAnnunciator.TabIndex = 11; + this.ShutterOpenAnnunciator.Text = "Open"; + // + // ShutterClosedAnnunciator + // + this.ShutterClosedAnnunciator.ActiveColor = System.Drawing.Color.DarkSeaGreen; + this.ShutterClosedAnnunciator.AutoSize = true; + this.ShutterClosedAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterClosedAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterClosedAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterClosedAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterClosedAnnunciator.Location = new System.Drawing.Point(255, 17); + this.ShutterClosedAnnunciator.Mute = false; + this.ShutterClosedAnnunciator.Name = "ShutterClosedAnnunciator"; + this.ShutterClosedAnnunciator.Size = new System.Drawing.Size(56, 17); + this.ShutterClosedAnnunciator.TabIndex = 13; + this.ShutterClosedAnnunciator.Text = "Closed"; + // + // ShutterIndeterminateAnnunciator + // + this.ShutterIndeterminateAnnunciator.ActiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterIndeterminateAnnunciator.AutoSize = true; + this.ShutterIndeterminateAnnunciator.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterIndeterminateAnnunciator.Cadence = ASCOM.Controls.CadencePattern.BlinkAlarm; + this.ShutterIndeterminateAnnunciator.Font = new System.Drawing.Font("Consolas", 10F); + this.ShutterIndeterminateAnnunciator.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterIndeterminateAnnunciator.InactiveColor = System.Drawing.Color.FromArgb(((int)(((byte)(96)))), ((int)(((byte)(4)))), ((int)(((byte)(4))))); + this.ShutterIndeterminateAnnunciator.Location = new System.Drawing.Point(317, 17); + this.ShutterIndeterminateAnnunciator.Mute = false; + this.ShutterIndeterminateAnnunciator.Name = "ShutterIndeterminateAnnunciator"; + this.ShutterIndeterminateAnnunciator.Size = new System.Drawing.Size(64, 17); + this.ShutterIndeterminateAnnunciator.TabIndex = 15; + this.ShutterIndeterminateAnnunciator.Text = "Unknown"; + // + // SetupCommand + // + this.SetupCommand.Location = new System.Drawing.Point(12, 70); + this.SetupCommand.Name = "SetupCommand"; + this.SetupCommand.Size = new System.Drawing.Size(75, 23); + this.SetupCommand.TabIndex = 8; + this.SetupCommand.Text = "Setup..."; + this.SetupCommand.UseVisualStyleBackColor = true; + // + // ShutterCurrentBar + // + this.ShutterCurrentBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ShutterCurrentBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(0)))), ((int)(((byte)(0))))); + this.ShutterCurrentBar.Location = new System.Drawing.Point(210, 58); + this.ShutterCurrentBar.Maximum = 20; + this.ShutterCurrentBar.Name = "ShutterCurrentBar"; + this.ShutterCurrentBar.Size = new System.Drawing.Size(273, 10); + this.ShutterCurrentBar.TabIndex = 11; + // + // OpenButton + // + this.OpenButton.Location = new System.Drawing.Point(12, 12); + this.OpenButton.Name = "OpenButton"; + this.OpenButton.Size = new System.Drawing.Size(75, 23); + this.OpenButton.TabIndex = 12; + this.OpenButton.Text = "Open"; + this.OpenButton.UseVisualStyleBackColor = true; + // + // CloseButton + // + this.CloseButton.Location = new System.Drawing.Point(12, 41); + this.CloseButton.Name = "CloseButton"; + this.CloseButton.Size = new System.Drawing.Size(75, 23); + this.CloseButton.TabIndex = 13; + this.CloseButton.Text = "Close"; + this.CloseButton.UseVisualStyleBackColor = true; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(93, 55); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(108, 13); + this.label2.TabIndex = 14; + this.label2.Text = "Shutter Motor Current"; + // + // ServerStatusDisplay + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoSize = true; + this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.ClientSize = new System.Drawing.Size(499, 105); + this.Controls.Add(this.label2); + this.Controls.Add(this.CloseButton); + this.Controls.Add(this.OpenButton); + this.Controls.Add(this.SetupCommand); + this.Controls.Add(this.annunciatorPanel1); + this.Controls.Add(this.OnlineClients); + this.Controls.Add(this.label3); + this.Controls.Add(this.registeredClientCount); + this.Controls.Add(this.label1); + this.Controls.Add(this.ShutterCurrentBar); + this.DataBindings.Add(new System.Windows.Forms.Binding("Location", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "MainFormLocation", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow; + this.Location = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.MainFormLocation; + this.Name = "ServerStatusDisplay"; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + this.Text = "DIgital Domeworks ASCOM Server"; + this.WindowState = System.Windows.Forms.FormWindowState.Minimized; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.frmMain_FormClosing); + this.Load += new System.EventHandler(this.frmMain_Load); + this.LocationChanged += new System.EventHandler(this.frmMain_LocationChanged); + this.VisibleChanged += new System.EventHandler(this.ServerStatusDisplay_VisibleChanged); + this.annunciatorPanel1.ResumeLayout(false); + this.annunciatorPanel1.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label registeredClientCount; + private System.Windows.Forms.Label OnlineClients; + private System.Windows.Forms.Label label3; + private ASCOM.Controls.AnnunciatorPanel annunciatorPanel1; + private ASCOM.Controls.Annunciator RotationTitle; + private System.Windows.Forms.Button SetupCommand; + private ASCOM.Controls.Annunciator CounterClockwiseAnnunciator; + private ASCOM.Controls.Annunciator AzimuthMotorAnnunciator; + private ASCOM.Controls.Annunciator ClockwiseAnnunciator; + private ASCOM.Controls.Annunciator AzimuthPositionAnnunciator; + private ASCOM.Controls.Annunciator ShutterTitle; + private ASCOM.Controls.Annunciator ShutterOpeningAnnunciator; + private ASCOM.Controls.Annunciator ShutterMotorAnnunciator; + private ASCOM.Controls.Annunciator ShutterClosingAnnunciator; + private ASCOM.Controls.Annunciator ShutterCurrentAnnunciator; + private System.Windows.Forms.ProgressBar ShutterCurrentBar; + private System.Windows.Forms.Button OpenButton; + private System.Windows.Forms.Button CloseButton; + private ASCOM.Controls.Annunciator AtHomeAnnunciator; + private ASCOM.Controls.Annunciator UserPin1Annunciator; + private ASCOM.Controls.Annunciator UserPin2Annunciator; + private ASCOM.Controls.Annunciator UserPin3Annunciator; + private ASCOM.Controls.Annunciator UserPin4Annunciator; + private ASCOM.Controls.Annunciator ShutterOpenAnnunciator; + private ASCOM.Controls.Annunciator ShutterClosedAnnunciator; + private ASCOM.Controls.Annunciator ShutterIndeterminateAnnunciator; + private System.Windows.Forms.Label label2; + } +} + diff --git a/TA.DigitalDomeworks.Server/ServerStatusDisplay.cs b/TA.DigitalDomeworks.Server/ServerStatusDisplay.cs new file mode 100644 index 0000000..edf6ac7 --- /dev/null +++ b/TA.DigitalDomeworks.Server/ServerStatusDisplay.cs @@ -0,0 +1,299 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ServerStatusDisplay.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using System.Windows.Forms; +using ASCOM.Controls; +using JetBrains.Annotations; +using TA.DigitalDomeworks.Server.Properties; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.Server + { + public partial class ServerStatusDisplay : Form + { + [NotNull] private readonly List disposableSubscriptions = new List(); + [NotNull] private List clickCommands = new List(); + private IDisposable clientStatusSubscription; + + public ServerStatusDisplay() + { + InitializeComponent(); + } + + private void frmMain_Load(object sender, EventArgs e) + { + ConfigureAnnunciators(); + var clientStatusObservable = Observable.FromEventPattern, EventArgs>( + handler => SharedResources.ConnectionManager.ClientStatusChanged += handler, + handler => SharedResources.ConnectionManager.ClientStatusChanged -= handler); + clientStatusSubscription = clientStatusObservable + .ObserveOn(SynchronizationContext.Current) + .Subscribe(ObserveClientStatusChanged); + ObserveClientStatusChanged(null); // This sets the initial UI state before any notifications arrive + SetupCommand.AttachCommand(ExecuteSetupDialog, CanSetup); + } + + private void ConfigureAnnunciators() + { + var annunciators = new List + { + AzimuthMotorAnnunciator, ClockwiseAnnunciator, CounterClockwiseAnnunciator, + ShutterMotorAnnunciator, ShutterOpeningAnnunciator, ShutterClosingAnnunciator, + ShutterOpenAnnunciator, ShutterClosedAnnunciator, ShutterIndeterminateAnnunciator, + UserPin1Annunciator, UserPin2Annunciator, UserPin3Annunciator, UserPin4Annunciator, + AtHomeAnnunciator + }; + annunciators.ForEach(p => p.Mute = false); + annunciators.ForEach(p => p.Cadence = CadencePattern.SteadyOn); + AzimuthMotorAnnunciator.Cadence = CadencePattern.BlinkAlarm; + ShutterMotorAnnunciator.Cadence = CadencePattern.BlinkAlarm; + ShutterOpenAnnunciator.Cadence = CadencePattern.Wink; + AtHomeAnnunciator.Cadence = CadencePattern.Wink; + annunciators.ForEach(p => p.Mute = true); + } + + + private void ObserveClientStatusChanged(EventPattern eventPattern) + { + SetUiDeviceConnectedState(); + var clientStatus = SharedResources.ConnectionManager.Clients; + registeredClientCount.Text = clientStatus.Count().ToString(); + var onlineClientCount = clientStatus.Count(p => p.Online); + OnlineClients.Text = onlineClientCount.ToString(); + ConfigureUiPropertyNotifications(); + if (onlineClientCount == 1) + AttachCommands(); + if (onlineClientCount == 0) + DetachCommands(); + } + + private bool CanSetup() => + CanMoveShutterOrDome() || !SharedResources.ConnectionManager.MaybeControllerInstance.Any(); + + private bool CanMoveShutterOrDome() + { + var maybeController = SharedResources.ConnectionManager.MaybeControllerInstance; + if (!maybeController.Any()) return false; + var controller = maybeController.Single(); + return !controller.IsMoving; + } + + /* + * Experimental code! + * Exploring the use of the Command pattern to wire up controls to + * OnClick events, and have the controls enable/disable themselves using the + * command's CanExecuteChanged() method. + */ + private void AttachCommands() + { + var maybeController = SharedResources.ConnectionManager.MaybeControllerInstance; + if (!maybeController.Any()) return; + var controller = maybeController.Single(); + clickCommands = new List + { + OpenButton.AttachCommand(ExecuteOpenShutter, CanMoveShutterOrDome), + CloseButton.AttachCommand(ExecuteCloseShutter, CanMoveShutterOrDome) + }; + } + + private void DetachCommands() + { + clickCommands.ForEach(p => p.Dispose()); + clickCommands.Clear(); + } + + /// + /// Enables each device activity annunciator if there are any clients connected to that + /// device. + /// + private void SetUiDeviceConnectedState() + { + var clients = SharedResources.ConnectionManager.Clients; + if (clients.Count == 1) + ConfigureAnnunciators(); + } + + /// + /// Begins or terminates UI updates depending on the number of online clients. + /// + private void ConfigureUiPropertyNotifications() + { + var clientsOnline = SharedResources.ConnectionManager.OnlineClientCount; + if (clientsOnline > 0) + SubscribePropertyChangeNotifications(); + else + UnsubscribePropertyChangeNotifications(); + } + + /// + /// Stops observing the controller property change notifications. + /// + private void UnsubscribePropertyChangeNotifications() + { + disposableSubscriptions.ForEach(p => p.Dispose()); + disposableSubscriptions.Clear(); + } + + /// + /// Creates subscriptions that observe property change notifications and update the UI as changes occur. + /// + private void SubscribePropertyChangeNotifications() + { + if (!SharedResources.ConnectionManager.MaybeControllerInstance.Any()) + return; + var controller = SharedResources.ConnectionManager.MaybeControllerInstance.Single(); + /* ToDo: + * Add subscriptions to PropertyChanged notifications using this pattern: + * movingSubscription = controller + * .GetObservableValueFor(m => m.IsMoving) + * .ObserveOn(SynchronizationContext.Current) + * .Subscribe(SetMotorMovingState); + */ + disposableSubscriptions.Add( + controller + .GetObservableValueFor(p => p.AzimuthMotorActive) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(motorActive => AzimuthMotorAnnunciator.Mute = !motorActive) + ); + disposableSubscriptions.Add( + controller + .GetObservableValueFor(p => p.AzimuthDirection) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(SetRotationDirection) + ); + disposableSubscriptions.Add( + controller + .GetObservableValueFor(p => p.AzimuthDegrees) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(SetAzimuthPosition) + ); + disposableSubscriptions.Add( + controller.GetObservableValueFor(p => p.ShutterMotorActive) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(motorActive => ShutterMotorAnnunciator.Mute = !motorActive) + ); + disposableSubscriptions.Add( + controller.GetObservableValueFor(p => p.ShutterMovementDirection) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(SetShutterDirection) + ); + disposableSubscriptions.Add( + controller.GetObservableValueFor(p => p.ShutterMotorCurrent) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(SetShutterMotorCurrent) + ); + disposableSubscriptions.Add( + controller.GetObservableValueFor(p => p.ShutterPosition) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(SetShutterPosition) + ); + disposableSubscriptions.Add( + controller.GetObservableValueFor(p => p.AtHome) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(home => AtHomeAnnunciator.Mute = !home) + ); + disposableSubscriptions.Add( + controller.GetObservableValueFor(p => p.UserPins) + .ObserveOn(SynchronizationContext.Current) + .Subscribe(SetUserPins) + ); + disposableSubscriptions.Add( + controller.GetPropertyChangedEvents() + .ObserveOn(SynchronizationContext.Current) + .Subscribe(p => clickCommands.ForEach(q => q.CanExecuteChanged())) + ); + } + + private void SetUserPins(Octet pinState) + { + UserPin1Annunciator.Mute = !pinState[0]; + UserPin2Annunciator.Mute = !pinState[1]; + UserPin3Annunciator.Mute = !pinState[2]; + UserPin4Annunciator.Mute = !pinState[3]; + } + + private void SetShutterPosition(SensorState position) + { + var controller = SharedResources.ConnectionManager.MaybeControllerInstance.Single(); + var shutterMoving = controller?.ShutterMotorActive ?? false; + ShutterClosedAnnunciator.Mute = position != SensorState.Closed || shutterMoving; + ShutterIndeterminateAnnunciator.Mute = position != SensorState.Indeterminate || shutterMoving; + ShutterOpenAnnunciator.Mute = position != SensorState.Open || shutterMoving; + } + + private void SetAzimuthPosition(float position) + { + var format = AzimuthPositionAnnunciator.Tag.ToString(); + var formattedPosition = string.Format(format, (int) position); + AzimuthPositionAnnunciator.Text = formattedPosition; + } + + private void SetShutterMotorCurrent(int current) + { + var format = ShutterCurrentAnnunciator.Tag.ToString(); + var formattedValue = string.Format(format, current); + ShutterCurrentAnnunciator.Text = formattedValue; + // Auto-scale the progress bar + ShutterCurrentBar.Maximum = Math.Max(ShutterCurrentBar.Maximum, current); + ShutterCurrentBar.Value = current; + } + + private void SetRotationDirection(RotationDirection direction) + { + CounterClockwiseAnnunciator.Mute = direction != RotationDirection.CounterClockwise; + ClockwiseAnnunciator.Mute = direction != RotationDirection.Clockwise; + } + + private void SetShutterDirection(ShutterDirection direction) + { + ShutterOpeningAnnunciator.Mute = direction != ShutterDirection.Opening; + ShutterClosingAnnunciator.Mute = direction != ShutterDirection.Closing; + } + + + private void frmMain_FormClosing(object sender, FormClosingEventArgs e) + { + clientStatusSubscription?.Dispose(); + UnsubscribePropertyChangeNotifications(); + var clients = SharedResources.ConnectionManager.Clients; + foreach (var client in clients) SharedResources.ConnectionManager.GoOffline(client.ClientId); + Application.Exit(); + } + + private void ExecuteSetupDialog() + { + SharedResources.DoSetupDialog(default(Guid)); + } + + private void frmMain_LocationChanged(object sender, EventArgs e) + { + Settings.Default.Save(); + } + + private void ServerStatusDisplay_VisibleChanged(object sender, EventArgs e) + { + BringToFront(); + } + + private void ExecuteOpenShutter() + { + if (SharedResources.ConnectionManager.MaybeControllerInstance.Any()) + SharedResources.ConnectionManager.MaybeControllerInstance.Single().OpenShutter(); + } + + private void ExecuteCloseShutter() + { + if (SharedResources.ConnectionManager.MaybeControllerInstance.Any()) + SharedResources.ConnectionManager.MaybeControllerInstance.Single().CloseShutter(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/ServerStatusDisplay.resx b/TA.DigitalDomeworks.Server/ServerStatusDisplay.resx new file mode 100644 index 0000000..d58980a --- /dev/null +++ b/TA.DigitalDomeworks.Server/ServerStatusDisplay.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/TA.DigitalDomeworks.Server/Settings.cs b/TA.DigitalDomeworks.Server/Settings.cs new file mode 100644 index 0000000..fac61df --- /dev/null +++ b/TA.DigitalDomeworks.Server/Settings.cs @@ -0,0 +1,55 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: Settings.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System.ComponentModel; +using System.Configuration; +using ASCOM; +using NLog; +using SettingsProvider = ASCOM.SettingsProvider; + +// ReSharper disable once CheckNamespace +namespace TA.DigitalDomeworks.Server.Properties + { + // This class allows you to handle specific events on the settings class: + // The SettingChanging event is raised before a setting's value is changed. + // The PropertyChanged event is raised after a setting's value is changed. + // The SettingsLoaded event is raised after the setting values are loaded. + // The SettingsSaving event is raised before the setting values are saved. + [SettingsProvider(typeof(SettingsProvider))] + [DeviceId(SharedResources.DomeDriverId, DeviceName = SharedResources.DomeDriverName)] + public sealed partial class Settings + { + private readonly ILogger log = LogManager.GetCurrentClassLogger(); + + public Settings() + { + SettingChanging += SettingChangingEventHandler; + SettingsSaving += SettingsSavingEventHandler; + SettingsLoaded += SettingsLoadedEventHandler; + PropertyChanged += SettingChangedEventHandler; + } + + private void SettingChangedEventHandler(object sender, PropertyChangedEventArgs args) + { + log.Debug($"Setting changed: {args.PropertyName}"); + } + + private void SettingsLoadedEventHandler(object sender, SettingsLoadedEventArgs e) + { + log.Warn("Settings loaded"); + } + + private void SettingChangingEventHandler(object sender, SettingChangingEventArgs e) + { + log.Debug($"Setting changing {e.SettingName}[{e.SettingKey}] -> {e.NewValue}"); + } + + private void SettingsSavingEventHandler(object sender, CancelEventArgs e) + { + log.Warn($"Saving settings"); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/SetupDialogForm.cs b/TA.DigitalDomeworks.Server/SetupDialogForm.cs new file mode 100644 index 0000000..aaf65ed --- /dev/null +++ b/TA.DigitalDomeworks.Server/SetupDialogForm.cs @@ -0,0 +1,114 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: SetupDialogForm.cs Last modified: 2018-06-17@14:27 by Tim Long + +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace TA.DigitalDomeworks.Server + { + [ComVisible(false)] // Form not registered for COM! + public partial class SetupDialogForm : Form + { + public SetupDialogForm() + { + InitializeComponent(); + } + + private void cmdOK_Click(object sender, EventArgs e) // OK button event handler + { + communicationSettingsControl1.Save(); + } + + 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 + { + Process.Start("http://ascom-standards.org/"); + } + catch (Win32Exception noBrowser) + { + if (noBrowser.ErrorCode == -2147467259) + MessageBox.Show(noBrowser.Message); + } + catch (Exception other) + { + MessageBox.Show(other.Message); + } + } + + private void SetupDialogForm_Load(object sender, EventArgs e) + { + var onlineClients = SharedResources.ConnectionManager.OnlineClientCount; + if (onlineClients == 0) + { + communicationSettingsControl1.Enabled = true; + ConnectionErrorProvider.SetError(communicationSettingsControl1, string.Empty); + } + else + { + communicationSettingsControl1.Enabled = false; + ConnectionErrorProvider.SetError(communicationSettingsControl1, + "Connection settings cannot be changed while there are connected clients"); + } + SetControlAppearance(); + } + + private void AboutBox_Click(object sender, EventArgs e) + { + using (var aboutBox = new AboutBox()) + { + aboutBox.ShowDialog(); + } + } + + private void PresetHD6_Click(object sender, EventArgs e) + { + FullRotationTimeSeconds.Value = 60; + ShutterOpenCloseTimeSeconds.Value = 120; + } + + private void PresetHD10_Click(object sender, EventArgs e) + { + FullRotationTimeSeconds.Value = 90; + ShutterOpenCloseTimeSeconds.Value = 180; + } + + private void PresetHD15_Click(object sender, EventArgs e) + { + FullRotationTimeSeconds.Value = 120; + ShutterOpenCloseTimeSeconds.Value = 240; + } + + private void IgnoreShutterSensor_CheckedChanged(object sender, EventArgs e) + { + if (IgnoreShutterSensor.Checked) + { + var result = MessageBox.Show( + "This is a potentially unsafe setting.\nPlease be sure you understand the implications\nbefore enabling this!", + "Potentially unsafe configuration", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2); + if (result != DialogResult.OK) + IgnoreShutterSensor.Checked = false; + } + + SetControlAppearance(); + } + + private void SetControlAppearance() + { + IgnoreShutterSensor.ForeColor = IgnoreShutterSensor.Checked ? Color.DarkRed : DefaultForeColor; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/SetupDialogForm.designer.cs b/TA.DigitalDomeworks.Server/SetupDialogForm.designer.cs new file mode 100644 index 0000000..764ecce --- /dev/null +++ b/TA.DigitalDomeworks.Server/SetupDialogForm.designer.cs @@ -0,0 +1,371 @@ +namespace TA.DigitalDomeworks.Server +{ + 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.components = new System.ComponentModel.Container(); + System.Windows.Forms.ToolTip toolTip1; + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SetupDialogForm)); + this.communicationSettingsControl1 = new TA.DigitalDomeworks.Server.CommunicationSettingsControl(); + this.PerformShutterRecovery = new System.Windows.Forms.CheckBox(); + this.ShutterOpenCloseTimeSeconds = new System.Windows.Forms.NumericUpDown(); + this.FullRotationTimeSeconds = new System.Windows.Forms.NumericUpDown(); + this.PresetHD6 = new System.Windows.Forms.Button(); + this.PresetHD10 = new System.Windows.Forms.Button(); + this.PresetHD15 = new System.Windows.Forms.Button(); + this.IgnoreShutterSensor = new System.Windows.Forms.CheckBox(); + this.cmdOK = new System.Windows.Forms.Button(); + this.cmdCancel = new System.Windows.Forms.Button(); + this.picASCOM = new System.Windows.Forms.PictureBox(); + this.AboutBox = new System.Windows.Forms.Button(); + this.ConnectionErrorProvider = new System.Windows.Forms.ErrorProvider(this.components); + this.CommunicationsGroup = new System.Windows.Forms.GroupBox(); + this.StartupOptionsGroup = new System.Windows.Forms.GroupBox(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.label3 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.groupBox2 = new System.Windows.Forms.GroupBox(); + toolTip1 = new System.Windows.Forms.ToolTip(this.components); + ((System.ComponentModel.ISupportInitialize)(this.ShutterOpenCloseTimeSeconds)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.FullRotationTimeSeconds)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.picASCOM)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.ConnectionErrorProvider)).BeginInit(); + this.CommunicationsGroup.SuspendLayout(); + this.StartupOptionsGroup.SuspendLayout(); + this.groupBox1.SuspendLayout(); + this.groupBox2.SuspendLayout(); + this.SuspendLayout(); + // + // toolTip1 + // + toolTip1.AutoPopDelay = 30000; + toolTip1.InitialDelay = 200; + toolTip1.ReshowDelay = 100; + toolTip1.ToolTipIcon = System.Windows.Forms.ToolTipIcon.Info; + toolTip1.ToolTipTitle = "Settings Help"; + // + // communicationSettingsControl1 + // + this.communicationSettingsControl1.AutoSize = true; + this.communicationSettingsControl1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; + this.communicationSettingsControl1.Location = new System.Drawing.Point(6, 19); + this.communicationSettingsControl1.Name = "communicationSettingsControl1"; + this.communicationSettingsControl1.Size = new System.Drawing.Size(253, 36); + this.communicationSettingsControl1.TabIndex = 7; + toolTip1.SetToolTip(this.communicationSettingsControl1, resources.GetString("communicationSettingsControl1.ToolTip")); + // + // PerformShutterRecovery + // + this.PerformShutterRecovery.AutoSize = true; + this.PerformShutterRecovery.Checked = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.PerformShutterRecovery; + this.PerformShutterRecovery.CheckState = System.Windows.Forms.CheckState.Checked; + this.PerformShutterRecovery.DataBindings.Add(new System.Windows.Forms.Binding("Checked", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "PerformShutterRecovery", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.PerformShutterRecovery.Location = new System.Drawing.Point(7, 20); + this.PerformShutterRecovery.Name = "PerformShutterRecovery"; + this.PerformShutterRecovery.Size = new System.Drawing.Size(266, 17); + this.PerformShutterRecovery.TabIndex = 0; + this.PerformShutterRecovery.Text = "Close shutter if position is not known upon connect"; + toolTip1.SetToolTip(this.PerformShutterRecovery, resources.GetString("PerformShutterRecovery.ToolTip")); + this.PerformShutterRecovery.UseVisualStyleBackColor = true; + // + // ShutterOpenCloseTimeSeconds + // + this.ShutterOpenCloseTimeSeconds.DataBindings.Add(new System.Windows.Forms.Binding("Value", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "ShutterOpenCloseTimeSeconds", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.ShutterOpenCloseTimeSeconds.Location = new System.Drawing.Point(175, 45); + this.ShutterOpenCloseTimeSeconds.Maximum = new decimal(new int[] { + 600, + 0, + 0, + 0}); + this.ShutterOpenCloseTimeSeconds.Name = "ShutterOpenCloseTimeSeconds"; + this.ShutterOpenCloseTimeSeconds.Size = new System.Drawing.Size(80, 20); + this.ShutterOpenCloseTimeSeconds.TabIndex = 1; + this.ShutterOpenCloseTimeSeconds.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; + toolTip1.SetToolTip(this.ShutterOpenCloseTimeSeconds, resources.GetString("ShutterOpenCloseTimeSeconds.ToolTip")); + this.ShutterOpenCloseTimeSeconds.Value = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.ShutterOpenCloseTimeSeconds; + // + // FullRotationTimeSeconds + // + this.FullRotationTimeSeconds.DataBindings.Add(new System.Windows.Forms.Binding("Value", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "FullRotationTimeSeconds", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.FullRotationTimeSeconds.Location = new System.Drawing.Point(175, 19); + this.FullRotationTimeSeconds.Maximum = new decimal(new int[] { + 600, + 0, + 0, + 0}); + this.FullRotationTimeSeconds.Name = "FullRotationTimeSeconds"; + this.FullRotationTimeSeconds.Size = new System.Drawing.Size(80, 20); + this.FullRotationTimeSeconds.TabIndex = 1; + this.FullRotationTimeSeconds.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; + toolTip1.SetToolTip(this.FullRotationTimeSeconds, resources.GetString("FullRotationTimeSeconds.ToolTip")); + this.FullRotationTimeSeconds.Value = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.FullRotationTimeSeconds; + // + // PresetHD6 + // + this.PresetHD6.Location = new System.Drawing.Point(61, 71); + this.PresetHD6.Name = "PresetHD6"; + this.PresetHD6.Size = new System.Drawing.Size(51, 23); + this.PresetHD6.TabIndex = 2; + this.PresetHD6.Text = "HD-6"; + toolTip1.SetToolTip(this.PresetHD6, "Load default timeouts for a 6ft dome"); + this.PresetHD6.UseVisualStyleBackColor = true; + this.PresetHD6.Click += new System.EventHandler(this.PresetHD6_Click); + // + // PresetHD10 + // + this.PresetHD10.Location = new System.Drawing.Point(118, 71); + this.PresetHD10.Name = "PresetHD10"; + this.PresetHD10.Size = new System.Drawing.Size(51, 23); + this.PresetHD10.TabIndex = 2; + this.PresetHD10.Text = "HD-10"; + toolTip1.SetToolTip(this.PresetHD10, "Load default timeouts for a 10ft dome"); + this.PresetHD10.UseVisualStyleBackColor = true; + this.PresetHD10.Click += new System.EventHandler(this.PresetHD10_Click); + // + // PresetHD15 + // + this.PresetHD15.Location = new System.Drawing.Point(175, 71); + this.PresetHD15.Name = "PresetHD15"; + this.PresetHD15.Size = new System.Drawing.Size(51, 23); + this.PresetHD15.TabIndex = 2; + this.PresetHD15.Text = "HD-15"; + toolTip1.SetToolTip(this.PresetHD15, "Load default timeouts for a 15ft dome"); + this.PresetHD15.UseVisualStyleBackColor = true; + this.PresetHD15.Click += new System.EventHandler(this.PresetHD15_Click); + // + // IgnoreShutterSensor + // + this.IgnoreShutterSensor.AutoSize = true; + this.IgnoreShutterSensor.Checked = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.IgnoreHardwareShutterSensor; + this.IgnoreShutterSensor.DataBindings.Add(new System.Windows.Forms.Binding("Checked", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "IgnoreHardwareShutterSensor", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.IgnoreShutterSensor.Location = new System.Drawing.Point(7, 20); + this.IgnoreShutterSensor.Name = "IgnoreShutterSensor"; + this.IgnoreShutterSensor.Size = new System.Drawing.Size(222, 17); + this.IgnoreShutterSensor.TabIndex = 0; + this.IgnoreShutterSensor.Text = "Enable shutter position inference heuristic"; + toolTip1.SetToolTip(this.IgnoreShutterSensor, resources.GetString("IgnoreShutterSensor.ToolTip")); + this.IgnoreShutterSensor.UseVisualStyleBackColor = true; + this.IgnoreShutterSensor.CheckedChanged += new System.EventHandler(this.IgnoreShutterSensor_CheckedChanged); + // + // 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(327, 323); + 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(327, 353); + 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); + // + // 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::TA.DigitalDomeworks.Server.Properties.Resources.ASCOM; + this.picASCOM.Location = new System.Drawing.Point(340, 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); + // + // AboutBox + // + this.AboutBox.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.AboutBox.Location = new System.Drawing.Point(327, 295); + this.AboutBox.Name = "AboutBox"; + this.AboutBox.Size = new System.Drawing.Size(59, 23); + this.AboutBox.TabIndex = 8; + this.AboutBox.Text = "About..."; + this.AboutBox.UseVisualStyleBackColor = true; + this.AboutBox.Click += new System.EventHandler(this.AboutBox_Click); + // + // ConnectionErrorProvider + // + this.ConnectionErrorProvider.BlinkRate = 1000; + this.ConnectionErrorProvider.BlinkStyle = System.Windows.Forms.ErrorBlinkStyle.AlwaysBlink; + this.ConnectionErrorProvider.ContainerControl = this; + // + // CommunicationsGroup + // + this.CommunicationsGroup.Controls.Add(this.communicationSettingsControl1); + this.CommunicationsGroup.Location = new System.Drawing.Point(12, 12); + this.CommunicationsGroup.Name = "CommunicationsGroup"; + this.CommunicationsGroup.Size = new System.Drawing.Size(303, 74); + this.CommunicationsGroup.TabIndex = 9; + this.CommunicationsGroup.TabStop = false; + this.CommunicationsGroup.Text = "Communications"; + // + // StartupOptionsGroup + // + this.StartupOptionsGroup.Controls.Add(this.PerformShutterRecovery); + this.StartupOptionsGroup.Location = new System.Drawing.Point(12, 92); + this.StartupOptionsGroup.Name = "StartupOptionsGroup"; + this.StartupOptionsGroup.Size = new System.Drawing.Size(303, 70); + this.StartupOptionsGroup.TabIndex = 10; + this.StartupOptionsGroup.TabStop = false; + this.StartupOptionsGroup.Text = "Startup Options"; + // + // groupBox1 + // + this.groupBox1.Controls.Add(this.PresetHD15); + this.groupBox1.Controls.Add(this.PresetHD10); + this.groupBox1.Controls.Add(this.PresetHD6); + this.groupBox1.Controls.Add(this.FullRotationTimeSeconds); + this.groupBox1.Controls.Add(this.ShutterOpenCloseTimeSeconds); + this.groupBox1.Controls.Add(this.label3); + this.groupBox1.Controls.Add(this.label2); + this.groupBox1.Controls.Add(this.label1); + this.groupBox1.Location = new System.Drawing.Point(13, 169); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(302, 111); + this.groupBox1.TabIndex = 11; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "Timeouts"; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(2, 76); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(42, 13); + this.label3.TabIndex = 0; + this.label3.Text = "Presets"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(6, 47); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(147, 13); + this.label2.TabIndex = 0; + this.label2.Text = "Shutter open/close (seconds)"; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(6, 21); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(110, 13); + this.label1.TabIndex = 0; + this.label1.Text = "Full rotation (seconds)"; + // + // groupBox2 + // + this.groupBox2.Controls.Add(this.IgnoreShutterSensor); + this.groupBox2.Location = new System.Drawing.Point(13, 287); + this.groupBox2.Name = "groupBox2"; + this.groupBox2.Size = new System.Drawing.Size(302, 100); + this.groupBox2.TabIndex = 12; + this.groupBox2.TabStop = false; + this.groupBox2.Text = "Experimental"; + // + // SetupDialogForm + // + this.AcceptButton = this.cmdOK; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.cmdCancel; + this.ClientSize = new System.Drawing.Size(398, 390); + this.Controls.Add(this.groupBox2); + this.Controls.Add(this.groupBox1); + this.Controls.Add(this.StartupOptionsGroup); + this.Controls.Add(this.CommunicationsGroup); + this.Controls.Add(this.AboutBox); + this.Controls.Add(this.picASCOM); + this.Controls.Add(this.cmdCancel); + this.Controls.Add(this.cmdOK); + this.DataBindings.Add(new System.Windows.Forms.Binding("Location", global::TA.DigitalDomeworks.Server.Properties.Settings.Default, "SetupDialogLocation", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged)); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Location = global::TA.DigitalDomeworks.Server.Properties.Settings.Default.SetupDialogLocation; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(395, 213); + this.Name = "SetupDialogForm"; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "ASCOM Drivers for Digital Domeworks Setup"; + this.Load += new System.EventHandler(this.SetupDialogForm_Load); + ((System.ComponentModel.ISupportInitialize)(this.ShutterOpenCloseTimeSeconds)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.FullRotationTimeSeconds)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.picASCOM)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.ConnectionErrorProvider)).EndInit(); + this.CommunicationsGroup.ResumeLayout(false); + this.CommunicationsGroup.PerformLayout(); + this.StartupOptionsGroup.ResumeLayout(false); + this.StartupOptionsGroup.PerformLayout(); + this.groupBox1.ResumeLayout(false); + this.groupBox1.PerformLayout(); + this.groupBox2.ResumeLayout(false); + this.groupBox2.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button cmdOK; + private System.Windows.Forms.Button cmdCancel; + private System.Windows.Forms.PictureBox picASCOM; + private CommunicationSettingsControl communicationSettingsControl1; + private System.Windows.Forms.Button AboutBox; + private System.Windows.Forms.ErrorProvider ConnectionErrorProvider; + private System.Windows.Forms.GroupBox CommunicationsGroup; + private System.Windows.Forms.GroupBox StartupOptionsGroup; + private System.Windows.Forms.CheckBox PerformShutterRecovery; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.NumericUpDown ShutterOpenCloseTimeSeconds; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Button PresetHD15; + private System.Windows.Forms.Button PresetHD10; + private System.Windows.Forms.Button PresetHD6; + private System.Windows.Forms.NumericUpDown FullRotationTimeSeconds; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.GroupBox groupBox2; + private System.Windows.Forms.CheckBox IgnoreShutterSensor; + } +} \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/SetupDialogForm.resx b/TA.DigitalDomeworks.Server/SetupDialogForm.resx new file mode 100644 index 0000000..76ad82b --- /dev/null +++ b/TA.DigitalDomeworks.Server/SetupDialogForm.resx @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + False + + + 203, 17 + + + Set the communications parameters for your installation. +If the "Use Simulator" box is checked, then the driver uses +a built-in hardware simulator that can be used for testing +and experimentation (no serial port or dome hardware +required). Otherwise, select the COM port where your +Digital Domeworks hardware is connected. + + + Controls "shutter auto-recovery". When the Digital +Domeworks hardware is powered on, it has no way +of knowing whether the shutter is open or closed +until a successful open or close operation completes. +When this option is enabled, if the shutter position is +"indeterminate" upon connecting, then a close +instruction will be issued in an attempt to +unambiguously determine the shutter state. +This is highly recommended for remote controlled +installations. + + + Determines the maximum time that the +ASCOM driver will wait for certain synchronous +operations to complete. Set this to slightly more +than the worst case time (in seconds) +for the shutter to fully open or close. + + + + Determines the maximum time that the +ASCOM driver will wait for certain synchronous +operations to complete. Set this to slightly more +than the worst case time (in seconds) +for a full 360° rotation of your dome. + + + This is a potentially unsafe setting and should not be enabled +without careful consideration. + +When enabled, the driver ignores the shutter position reported +by Digital Domeworks and uses a heuristic to infer the shutter +position based on the shutter operation history. If the heuristic +has insufficient information, then it falls back to using the +position reported by Digital Domeworks. + +The heuristic takes into account the length of time the shutter +was detected as moving and how much current was drawn by +the shutter motor. + +This option is designed to work around a problem where some +Digital Domeworks installations give an unreliable indication of +the shutter position. If you get shutter errors in your application +even when the shutter appears to work correctly, you can try +enabling this option. The heuristic is not foolproof and this +option should be used with care. + +Default: disabled. + + + 17, 17 + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/SharedResources.cs b/TA.DigitalDomeworks.Server/SharedResources.cs new file mode 100644 index 0000000..2d6738c --- /dev/null +++ b/TA.DigitalDomeworks.Server/SharedResources.cs @@ -0,0 +1,70 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: SharedResources.cs Last modified: 2018-03-28@22:20 by Tim Long + +using System; +using System.Windows.Forms; +using NLog; +using TA.DigitalDomeworks.Server.Properties; + +namespace TA.DigitalDomeworks.Server + { + /// + /// The resources shared by all drivers and devices. + /// + public static class SharedResources + { + public const string DomeDriverId = "ASCOM.DigitalDomeworks2018.Dome"; + public const string SwitchDriverId = "ASCOM.DigitalDomeworks2018.Switch"; + public const string DomeDriverName = "Digital Domeworks 2018 Reboot"; + public const string SwitchDriverName = "Digital Domeworks 2018 Reboot"; + + private static readonly ILogger Log; + + static SharedResources() + { + Log = LogManager.GetCurrentClassLogger(); + ConnectionManager = CreateConnectionManager(); + } + + /// + /// Gets the connection manager. + /// + /// The connection manager. + public static ClientConnectionManager ConnectionManager { get; } + + + public static void DoSetupDialog(Guid clientId) + { + var oldConnectionString = Settings.Default.ConnectionString; + Log.Info($"SetupDialog requested by client {clientId}"); + using (var F = new SetupDialogForm()) + { + var result = F.ShowDialog(); + switch (result) + { + case DialogResult.OK: + Log.Info($"SetupDialog successful, saving settings"); + Settings.Default.Save(); + var newConnectionString = Settings.Default.ConnectionString; + if (oldConnectionString != newConnectionString) + Log.Warn( + $"Connection string has changed from {oldConnectionString} to {newConnectionString} - replacing the TansactionProcessorFactory"); + break; + default: + Log.Warn("SetupDialog cancelled or failed - reverting to previous settings"); + Settings.Default.Reload(); + break; + } + } + } + + private static ClientConnectionManager CreateConnectionManager() + { + Log.Info("Creating ClientConnectionManager"); + return new ClientConnectionManager(); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/TA.DigitalDomeworks.Server.csproj b/TA.DigitalDomeworks.Server/TA.DigitalDomeworks.Server.csproj new file mode 100644 index 0000000..75107ac --- /dev/null +++ b/TA.DigitalDomeworks.Server/TA.DigitalDomeworks.Server.csproj @@ -0,0 +1,322 @@ + + + + + Debug + AnyCPU + 8.0.50727 + 2.0 + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143} + WinExe + Properties + TA.DigitalDomeworks.Server + TA.DigitalDomeworks.Server + v4.6.2 + + + 2.0 + + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + + + false + + + LocalServer.snk + + + true + ..\BuildOutput\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + ..\BuildOutput\Release\ + TRACE + true + pdbonly + AnyCPU + prompt + MinimumRecommendedRules.ruleset + true + + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Astrometry.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Attributes.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Controls.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DeviceInterfaces.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DriverAccess.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Exceptions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Internal.Extensions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.SettingsProvider.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.Video.dll + True + + + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll + + + + ..\packages\Ninject.3.3.4\lib\net45\Ninject.dll + + + ..\packages\NLog.4.5.0\lib\net45\NLog.dll + + + ..\packages\PostSharp.Redist.5.0.48\lib\net45\PostSharp.dll + True + + + ..\packages\PostSharp.Patterns.Aggregation.Redist.5.0.48\lib\net45\PostSharp.Patterns.Aggregation.dll + + + ..\packages\PostSharp.Patterns.Common.Redist.5.0.48\lib\net46\PostSharp.Patterns.Common.dll + + + ..\packages\PostSharp.Patterns.Model.Redist.5.0.48\lib\net40\PostSharp.Patterns.Model.dll + + + ..\packages\PostSharp.Patterns.Threading.Redist.5.0.48\lib\net45\PostSharp.Patterns.Threading.dll + + + + + + + + ..\packages\System.Reactive.Core.3.1.1\lib\net46\System.Reactive.Core.dll + + + ..\packages\System.Reactive.Interfaces.3.1.1\lib\net45\System.Reactive.Interfaces.dll + + + ..\packages\System.Reactive.Linq.3.1.1\lib\net46\System.Reactive.Linq.dll + + + ..\packages\System.Reactive.PlatformServices.3.1.1\lib\net46\System.Reactive.PlatformServices.dll + + + ..\packages\System.Reactive.Windows.Threading.3.1.1\lib\net45\System.Reactive.Windows.Threading.dll + + + + + + + + + ..\packages\TA.Ascom.ReactiveCommunications.1.0.0\lib\net45\TA.Ascom.ReactiveCommunications.dll + + + + + + Form + + + AboutBox.cs + + + + + + + + UserControl + + + CommunicationSettingsControl.cs + + + + Form + + + ServerStatusDisplay.cs + + + + + + AboutBox.cs + + + CommunicationSettingsControl.cs + + + Designer + ServerStatusDisplay.cs + + + True + True + Settings.settings + + + + Resources.resx + True + True + + + + + Form + + + SetupDialogForm.cs + + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + SetupDialogForm.cs + + + + + + + + + NLog.config + True + + + NLog.config + True + + + Designer + + + + + + + + + + + Always + true + + + + PublicSettingsSingleFileGenerator + 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 + + + + + + + + {020924b9-23d5-4b92-b5b1-461423bbe23c} + TA.DigitalDomeworks.DeviceInterface + + + {86b17c99-41b6-4611-ad1d-26b7d6c70a22} + TA.DigitalDomeworks.HardwareSimulator + + + {adbb1165-e995-4c75-8de9-1dbffcf34d6f} + TA.DigitalDomeworks.SharedTypes + + + + + + + + + + + + + + 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/TA.DigitalDomeworks.Server/app.config b/TA.DigitalDomeworks.Server/app.config new file mode 100644 index 0000000..0d6e45e --- /dev/null +++ b/TA.DigitalDomeworks.Server/app.config @@ -0,0 +1,82 @@ + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + COM1 + + + COM1:9600 + + + 0, 0 + + + 0, 0 + + + True + + + 60 + + + 120 + + + False + + + 10 + + + + + + + 00:03:00 + + + 00:00:05 + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Server/packages.config b/TA.DigitalDomeworks.Server/packages.config new file mode 100644 index 0000000..9f584c8 --- /dev/null +++ b/TA.DigitalDomeworks.Server/packages.config @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Shared/GlobalAssemblyInfo.cs b/TA.DigitalDomeworks.Shared/GlobalAssemblyInfo.cs index d01032c..b10e8fe 100644 --- a/TA.DigitalDomeworks.Shared/GlobalAssemblyInfo.cs +++ b/TA.DigitalDomeworks.Shared/GlobalAssemblyInfo.cs @@ -1,4 +1,10 @@ -using System.Reflection; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: GlobalAssemblyInfo.cs Last modified: 2018-04-21@21:45 by Tim Long + +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -9,7 +15,8 @@ [assembly: AssemblyTrademark("Tigra Astronomy")] [assembly: AssemblyCulture("")] [assembly: ComVisible(false)] -[assembly: AssemblyVersion("0.0.*")] -[assembly: AssemblyFileVersion("0.0.0.0")] -[assembly: AssemblyInformationalVersion("0.0-Integration")] +[assembly: AssemblyVersion("7.0.*")] +[assembly: AssemblyFileVersion("7.0.0.8")] +[assembly: AssemblyInformationalVersion("7.0-Beta.8")] [assembly: InternalsVisibleTo("TA.DigitalDomeworks.Specifications")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/AsciiExtensions.cs b/TA.DigitalDomeworks.SharedTypes/AsciiExtensions.cs index d97cb28..06e1e77 100644 --- a/TA.DigitalDomeworks.SharedTypes/AsciiExtensions.cs +++ b/TA.DigitalDomeworks.SharedTypes/AsciiExtensions.cs @@ -1,11 +1,11 @@ -// This file is part of the TI.DigitalDomeWorks project +// This file is part of the TA.DigitalDomeworks project // -// Copyright © 2014 TiGra Astronomy, all rights reserved. +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: AsciiExtensions.cs Created: 2014-10-29@20:45 -// Last modified: 2014-11-12@05:56 by Tim +// File: AsciiExtensions.cs Last modified: 2018-03-30@01:48 by Tim Long using System; +using System.Diagnostics.Contracts; using System.Text; namespace TA.DigitalDomeworks.SharedTypes @@ -13,29 +13,33 @@ namespace TA.DigitalDomeworks.SharedTypes public static class AsciiExtensions { /// - /// Utility function. Expands non-printable ASCII characters into mnemonic human-readable form. + /// Utility function. Expands non-printable ASCII characters into mnemonic human-readable form. /// /// - /// Returns a new string with non-printing characters replaced by human-readable mnemonics. + /// Returns a new string with non-printing characters replaced by human-readable mnemonics. /// public static string ExpandAscii(this string text) { - var expanded = new StringBuilder(Math.Max(64, text.Length*3)); - foreach (char c in text) + Contract.Requires(text != null); + Contract.Ensures(Contract.Result() != null); + var expanded = new StringBuilder(); + foreach (var c in text) { - var b = (byte)c; - var strASCII = Enum.GetName(typeof(AsciiSymbols), b); - if (strASCII != null) - expanded.Append("<" + strASCII + ">"); + var b = (byte) c; + var strAscii = Enum.GetName(typeof(AsciiSymbols), b); + if (strAscii != null) + expanded.Append("<" + strAscii + ">"); else expanded.Append(c); } + return expanded.ToString(); } public static string ExpandAscii(this char c) { + Contract.Ensures(Contract.Result() != null); return c.ToString().ExpandAscii(); } } - } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/Constants.cs b/TA.DigitalDomeworks.SharedTypes/Constants.cs index b926aa9..e1d78fe 100644 --- a/TA.DigitalDomeworks.SharedTypes/Constants.cs +++ b/TA.DigitalDomeworks.SharedTypes/Constants.cs @@ -1,8 +1,8 @@ -// This file is part of the TI.DigitalDomeWorks project +// This file is part of the TA.DigitalDomeworks project // -// Copyright © 2015-2016 Tigra Networks., all rights reserved. +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: Constants.cs Last modified: 2016-09-13@00:03 by Tim Long +// File: Constants.cs Last modified: 2018-04-21@21:03 by Tim Long namespace TI.DigitalDomeWorks { @@ -42,6 +42,12 @@ public struct Constants "V4,437,427,2,419,0,1,0,1,422,433,7,128,255,255,255,255,255,255,255,68,3,0"; #region DDW Command Codes + /// + /// Command to stop all movement. The Digital Domeworks controller will actually accept + /// pretty much any spurious character for this purpose. + /// + public const string CmdEmergencyStop = "STOP\n"; + /// /// DDW command to cancel the 4-minute auto-park timeout. /// @@ -148,7 +154,8 @@ public struct Constants #endregion #region Custom Actions - internal const string ActionNameDsrSwingoutState = "DomeSupportRingSwingoutState"; + public const string ActionNameDsrSwingoutState = "DomeSupportRingSwingoutState"; + public const string ActionNameControllerStatus = "ControllerStatus"; #endregion Custom Actions } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/ControllerStatusFactory.cs b/TA.DigitalDomeworks.SharedTypes/ControllerStatusFactory.cs index f1a1e7d..e06381e 100644 --- a/TA.DigitalDomeworks.SharedTypes/ControllerStatusFactory.cs +++ b/TA.DigitalDomeworks.SharedTypes/ControllerStatusFactory.cs @@ -1,38 +1,46 @@ -using System; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ControllerStatusFactory.cs Last modified: 2018-03-30@01:42 by Tim Long + +using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using NLog; -using NodaTime; -namespace TA.DigitalDomeworks.SharedTypes { - public sealed class ControllerStatusFactory { - private readonly IClock timeSource; +namespace TA.DigitalDomeworks.SharedTypes + { + public sealed class ControllerStatusFactory + { private static readonly Logger log = LogManager.GetCurrentClassLogger(); private static readonly char[] fieldDelimiters = {','}; + private readonly IClock timeSource; public ControllerStatusFactory(IClock timeSource) { + Contract.Requires(timeSource != null); this.timeSource = timeSource; - // ToDo: use IClock from NodaTime } /// - /// Creates a object from the text of a status packet + /// Creates a object from the text of a status packet /// - public IControllerStatus FromStatusPacket(string packet) + public IHardwareStatus FromStatusPacket(string packet) { Contract.Requires(!string.IsNullOrEmpty(packet)); + Contract.Ensures(Contract.Result() != null); var elements = packet.Split(fieldDelimiters); switch (elements[0]) { case "V4": - return (IControllerStatus)ParseV4StatusElements(elements); + return ParseV4StatusElements(elements); default: throw new ApplicationException("Unsupported firmware version"); } } - private ControllerStatus ParseV4StatusElements(IReadOnlyList elements) + private HardwareStatus ParseV4StatusElements(IReadOnlyList elements) { log.Info("V4 status"); var elementsLength = elements.Count; @@ -49,9 +57,9 @@ private ControllerStatus ParseV4StatusElements(IReadOnlyList elements) try { - var status = new ControllerStatus + var status = new HardwareStatus { - TimeStamp = timeSource.GetCurrentInstant(), + TimeStamp = timeSource.GetCurrentTime(), FirmwareVersion = elements[0], DomeCircumference = Convert.ToInt16(elements[1]), HomePosition = Convert.ToInt16(elements[2]), diff --git a/TA.DigitalDomeworks.SharedTypes/DeviceControllerOptions.cs b/TA.DigitalDomeworks.SharedTypes/DeviceControllerOptions.cs new file mode 100644 index 0000000..afef9c5 --- /dev/null +++ b/TA.DigitalDomeworks.SharedTypes/DeviceControllerOptions.cs @@ -0,0 +1,27 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: DeviceControllerOptions.cs Last modified: 2018-06-16@16:50 by Tim Long + +using System; + +namespace TA.DigitalDomeworks.SharedTypes + { + public class DeviceControllerOptions + { + public bool PerformShutterRecovery { get; set; } = true; + + public TimeSpan MaximumShutterCloseTime { get; set; } + + public TimeSpan MaximumFullRotationTime { get; set; } + + public TimeSpan KeepAliveTimerInterval { get; set; } + + public int CurrentDrawDetectionThreshold { get; set; } + + public bool IgnoreHardwareShutterSensor { get; set; } + + public TimeSpan ShutterTickTimeout { get; set; } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/ControllerStatus.cs b/TA.DigitalDomeworks.SharedTypes/HardwareStatus.cs similarity index 94% rename from TA.DigitalDomeworks.SharedTypes/ControllerStatus.cs rename to TA.DigitalDomeworks.SharedTypes/HardwareStatus.cs index 1efcf63..3064633 100644 --- a/TA.DigitalDomeworks.SharedTypes/ControllerStatus.cs +++ b/TA.DigitalDomeworks.SharedTypes/HardwareStatus.cs @@ -1,19 +1,17 @@ -// This file is part of the TI.DigitalDomeWorks project +// This file is part of the TA.DigitalDomeworks project // -// Copyright © 2014 TiGra Astronomy, all rights reserved. +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: ControllerStatus.cs Created: 2014-10-05@00:56 -// Last modified: 2014-11-12@05:54 by Tim +// File: HardwareStatus.cs Last modified: 2018-03-28@17:43 by Tim Long using System; -using NodaTime; namespace TA.DigitalDomeworks.SharedTypes { /// /// An immutable class representing the state of the dome controller hardware at a point in time. /// - public sealed class ControllerStatus : IControllerStatus + public sealed class HardwareStatus : IHardwareStatus { /// /// Indicates when the dome is in the Home Position. @@ -132,7 +130,7 @@ public sealed class ControllerStatus : IControllerStatus /// /// A value recording the date and time that the status packet was createdS. /// - public Instant TimeStamp { get; set; } + public DateTime TimeStamp { get; set; } /// /// The state of all of the user output pins @@ -198,7 +196,7 @@ public override string ToString() { const string statusFormat = "{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22}"; - return String.Format( + return string.Format( statusFormat, FirmwareVersion, DomeCircumference, @@ -206,12 +204,12 @@ public override string ToString() Coast, CurrentAzimuth, Slaved ? 1 : 0, - (int)ShutterSensor, - (int)DsrSensor, + (int) ShutterSensor, + (int) DsrSensor, AtHome ? 0 : 1, HomeCounterClockwise, HomeClockwise, - (byte)UserPins, + (byte) UserPins, WeatherAge, WindDirection, WindSpeed, diff --git a/TA.DigitalDomeworks.SharedTypes/IClock.cs b/TA.DigitalDomeworks.SharedTypes/IClock.cs new file mode 100644 index 0000000..9427fd9 --- /dev/null +++ b/TA.DigitalDomeworks.SharedTypes/IClock.cs @@ -0,0 +1,23 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: IClock.cs Last modified: 2018-03-28@17:43 by Tim Long + +using System; + +namespace TA.DigitalDomeworks.SharedTypes + { + public interface IClock + { + DateTime GetCurrentTime(); + } + + public class SystemDateTimeUtcClock : IClock + { + public DateTime GetCurrentTime() + { + return DateTime.UtcNow; + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/IControllerStatus.cs b/TA.DigitalDomeworks.SharedTypes/IHardwareStatus.cs similarity index 94% rename from TA.DigitalDomeworks.SharedTypes/IControllerStatus.cs rename to TA.DigitalDomeworks.SharedTypes/IHardwareStatus.cs index 5ac5805..d4fc463 100644 --- a/TA.DigitalDomeworks.SharedTypes/IControllerStatus.cs +++ b/TA.DigitalDomeworks.SharedTypes/IHardwareStatus.cs @@ -1,7 +1,15 @@ -using NodaTime; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: IHardwareStatus.cs Last modified: 2018-03-28@17:43 by Tim Long -namespace TA.DigitalDomeworks.SharedTypes { - public interface IControllerStatus { +using System; + +namespace TA.DigitalDomeworks.SharedTypes + { + public interface IHardwareStatus + { /// /// Indicates when the dome is in the Home Position. /// Note that the home position covers a small range of encoder ticks and is not @@ -119,7 +127,7 @@ public interface IControllerStatus { /// /// A DateTime object recording the date and time that the status packet was parsed. /// - Instant TimeStamp { get; } + DateTime TimeStamp { get; } /// /// The state of all of the user output pins diff --git a/TA.DigitalDomeworks.SharedTypes/NotifyPropertyChangeReactiveExtensions.cs b/TA.DigitalDomeworks.SharedTypes/NotifyPropertyChangeReactiveExtensions.cs new file mode 100644 index 0000000..8bd14b0 --- /dev/null +++ b/TA.DigitalDomeworks.SharedTypes/NotifyPropertyChangeReactiveExtensions.cs @@ -0,0 +1,120 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: NotifyPropertyChangeReactiveExtensions.cs Last modified: 2018-03-30@01:42 by Tim Long + +using System; +using System.ComponentModel; +using System.Diagnostics.Contracts; +using System.Linq.Expressions; +using System.Reactive; +using System.Reactive.Linq; + +namespace TA.DigitalDomeworks.SharedTypes + { + public static class NotifyPropertyChangeReactiveExtensions + { + // Returns the values of property (an Expression) as they + // change, starting with the current value + /// + /// Gets an observable sequence that produces a new value whenever the value of a property (member expression) changes, + /// starting with the current value. + /// + /// The type of the t source. + /// The type of the t value. + /// The source. + /// The property. + /// IObservable<TValue>. + /// + /// property must directly access " + + /// "a property of the source + /// + public static IObservable GetObservableValueFor( + this TSource source, Expression> property) + where TSource : INotifyPropertyChanged + { + Contract.Requires(property != null); + var memberExpression = + property.Body as MemberExpression; + + if (memberExpression == null) + throw new ArgumentException( + "property must directly access " + + "a property of the source"); + + var propertyName = memberExpression.Member.Name; + + var accessor = property.Compile(); + + return source.GetPropertyChangedEvents() + .Where(x => x.EventArgs.PropertyName == propertyName) + .Select(x => accessor(source)) + .StartWith(accessor(source)); + } + + // This is a wrapper around FromEvent(PropertyChanged) + public static IObservable> GetPropertyChangedEvents( + this INotifyPropertyChanged source) + { + return Observable.FromEventPattern( + handler => handler.Invoke, + handler => source.PropertyChanged += handler, + handler => source.PropertyChanged -= handler + ); + } + + public static IDisposable + Subscribe( + this TSource source, + Expression> property, + Action observer) + where TSource : INotifyPropertyChanged + { + Contract.Requires(property != null); + return source + .GetObservableValueFor(property) + .Subscribe(observer); + } + + public static IDisposable + Subscribe( + this TSource source, + Expression> property, + Action observer, + Action onCompleted) + where TSource : INotifyPropertyChanged + { + return source + .GetObservableValueFor(property) + .Subscribe(observer, onCompleted); + } + + public static IDisposable + Subscribe( + this TSource source, + Expression> property, + Action observer, + Action onException) + where TSource : INotifyPropertyChanged + { + return source + .GetObservableValueFor(property) + .Subscribe(observer, onException); + } + + public static IDisposable + Subscribe( + this TSource source, + Expression> property, + Action observer, + Action onException, + Action onCompleted) + where TSource : INotifyPropertyChanged + { + return source + .GetObservableValueFor(property) + .Subscribe(observer, onException, onCompleted); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.HardwareSimulator/RotationDirection.cs b/TA.DigitalDomeworks.SharedTypes/RotationDirection.cs similarity index 93% rename from TA.DigitalDomeworks.HardwareSimulator/RotationDirection.cs rename to TA.DigitalDomeworks.SharedTypes/RotationDirection.cs index 9d9fc42..596cfde 100644 --- a/TA.DigitalDomeworks.HardwareSimulator/RotationDirection.cs +++ b/TA.DigitalDomeworks.SharedTypes/RotationDirection.cs @@ -5,7 +5,7 @@ // File: RotationDirection.cs Created: 2014-10-05@00:56 // Last modified: 2014-11-12@05:55 by Tim -namespace TA.DigitalDomeworks.HardwareSimulator +namespace TA.DigitalDomeworks.SharedTypes { /// /// Rotation direction diff --git a/TA.DigitalDomeworks.SharedTypes/ShutterDirection.cs b/TA.DigitalDomeworks.SharedTypes/ShutterDirection.cs new file mode 100644 index 0000000..bb9a4f8 --- /dev/null +++ b/TA.DigitalDomeworks.SharedTypes/ShutterDirection.cs @@ -0,0 +1,29 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ShutterDirection.cs Last modified: 2018-03-19@20:47 by Tim Long + +namespace TA.DigitalDomeworks.SharedTypes + { + /// + /// Shutter movement directions. + /// Note: ordinal values are important and are used for parsing + /// received data within DeviceController. + /// + public enum ShutterDirection + { + /// + /// The shutter is not moving, or the direction is unknown + /// + None = 0, + /// + /// The shutter is closing. + /// + Closing = 1, + /// + /// The shutter is opening. + /// + Opening = 2 + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj b/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj index d140d49..4ebd8c4 100644 --- a/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj +++ b/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj @@ -1,6 +1,45 @@  + + + True + False + True + False + False + True + True + True + True + True + False + True + True + True + True + True + True + False + False + True + True + False + True + False + False + False + False + True + Full + 0 + True + True + True + True + Build + True + Debug AnyCPU @@ -11,13 +50,15 @@ TA.DigitalDomeworks.SharedTypes v4.6.2 512 + + true full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG prompt 4 @@ -31,35 +72,68 @@ - ..\packages\NLog.4.4.13\lib\net45\NLog.dll - - - ..\packages\NodaTime.2.2.4\lib\net45\NodaTime.dll + ..\packages\NLog.4.5.0\lib\net45\NLog.dll + + + + ..\packages\System.Reactive.Core.3.1.1\lib\net46\System.Reactive.Core.dll + + + ..\packages\System.Reactive.Interfaces.3.1.1\lib\net45\System.Reactive.Interfaces.dll + + + ..\packages\System.Reactive.Linq.3.1.1\lib\net46\System.Reactive.Linq.dll + + + ..\packages\System.Reactive.PlatformServices.3.1.1\lib\net46\System.Reactive.PlatformServices.dll + + + ..\packages\System.Reactive.Windows.Threading.3.1.1\lib\net45\System.Reactive.Windows.Threading.dll + + + + + + - + + - + + + + + + + + + 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/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj.DotSettings b/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj.DotSettings new file mode 100644 index 0000000..96331d1 --- /dev/null +++ b/TA.DigitalDomeworks.SharedTypes/TA.DigitalDomeworks.SharedTypes.csproj.DotSettings @@ -0,0 +1,2 @@ + + CSharp72 \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/app.config b/TA.DigitalDomeworks.SharedTypes/app.config new file mode 100644 index 0000000..cacd4cd --- /dev/null +++ b/TA.DigitalDomeworks.SharedTypes/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.SharedTypes/packages.config b/TA.DigitalDomeworks.SharedTypes/packages.config index 3d059dc..87de31e 100644 --- a/TA.DigitalDomeworks.SharedTypes/packages.config +++ b/TA.DigitalDomeworks.SharedTypes/packages.config @@ -1,5 +1,11 @@  - - + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Builders/DeviceControllerContextBuilder.cs b/TA.DigitalDomeworks.Specifications/Builders/DeviceControllerContextBuilder.cs index 879464e..f04281c 100644 --- a/TA.DigitalDomeworks.Specifications/Builders/DeviceControllerContextBuilder.cs +++ b/TA.DigitalDomeworks.Specifications/Builders/DeviceControllerContextBuilder.cs @@ -2,19 +2,19 @@ // // Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: DeviceControllerContextBuilder.cs Last modified: 2018-03-08@19:17 by Tim Long +// File: DeviceControllerContextBuilder.cs Last modified: 2018-06-16@16:53 by Tim Long using System; +using System.Collections.Generic; using System.ComponentModel; using System.Text; -using NodaTime; -using NodaTime.Testing; using TA.Ascom.ReactiveCommunications; using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.DeviceInterface.StateMachine; +using TA.DigitalDomeworks.HardwareSimulator; using TA.DigitalDomeworks.SharedTypes; using TA.DigitalDomeworks.Specifications.Contexts; using TA.DigitalDomeworks.Specifications.Fakes; -using TA.DigitalDomeworks.Specifications.Helpers; namespace TA.DigitalDomeworks.Specifications.Builders { @@ -27,21 +27,31 @@ public DeviceControllerContextBuilder() p => p.StartsWith("Fake", StringComparison.InvariantCultureIgnoreCase), connection => new FakeEndpoint(), endpoint => new FakeCommunicationChannel(fakeResponseBuilder.ToString()) - ); + ); channelFactory.RegisterChannelType( SimulatorEndpoint.IsConnectionStringValid, SimulatorEndpoint.FromConnectionString, endpoint => new SimulatorCommunicationsChannel(endpoint as SimulatorEndpoint) - ); + ); } bool channelShouldBeOpen; readonly StringBuilder fakeResponseBuilder = new StringBuilder(); - readonly IClock timeSource = new FakeClock(Instant.MinValue); - bool useFakeChannel; + readonly IClock timeSource = new FakeClock(DateTime.MinValue.ToUniversalTime()); readonly ChannelFactory channelFactory; string connectionString = "Fake"; + readonly DeviceControllerOptions controllerOptions = new DeviceControllerOptions + { + KeepAliveTimerInterval = TimeSpan.FromMinutes(3), + MaximumFullRotationTime = TimeSpan.FromMinutes(1), + MaximumShutterCloseTime = TimeSpan.FromMinutes(1), + PerformShutterRecovery = true, + CurrentDrawDetectionThreshold = 10, + IgnoreHardwareShutterSensor = false, + ShutterTickTimeout = TimeSpan.FromSeconds(5) + }; PropertyChangedEventHandler propertyChangedAction; + List> propertyChangeObservers = new List>(); public DeviceControllerContext Build() { @@ -53,21 +63,23 @@ public DeviceControllerContext Build() // Build the ControllerStatusFactory var statusFactory = new ControllerStatusFactory(timeSource); + var controllerActions = new RxControllerActions(channel); + var controllerStateMachine = new ControllerStateMachine(controllerActions, controllerOptions, timeSource); + // Build the device controller - var controller = new DeviceController(channel, statusFactory); + var controller = new DeviceController(channel, statusFactory, controllerStateMachine, controllerOptions); // Assemble the device controller test context var context = new DeviceControllerContext { Channel = channel, - Controller = controller + Controller = controller, + StateMachine = controllerStateMachine, + Actions = controllerActions }; // Wire up any Property Changed notifications - if (propertyChangedAction != null) - { - controller.PropertyChanged += propertyChangedAction; - } + if (propertyChangedAction != null) controller.PropertyChanged += propertyChangedAction; return context; } diff --git a/TA.DigitalDomeworks.Specifications/Builders/HardwareSimulationBuilder.cs b/TA.DigitalDomeworks.Specifications/Builders/HardwareSimulationBuilder.cs index 9531a92..3cd7e4d 100644 --- a/TA.DigitalDomeworks.Specifications/Builders/HardwareSimulationBuilder.cs +++ b/TA.DigitalDomeworks.Specifications/Builders/HardwareSimulationBuilder.cs @@ -1,22 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: HardwareSimulationBuilder.cs Last modified: 2018-03-28@18:17 by Tim Long + +using System; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; -using TA.Ascom.ReactiveCommunications; using TA.DigitalDomeworks.HardwareSimulator; +using TA.DigitalDomeworks.Specifications.Fakes; namespace TA.DigitalDomeworks.Specifications.Builders -{ - internal class HardwareSimulationBuilder { - private IObservable inputSequence = Observable.Empty(); - - public SimulatorStateMachine Build() + internal class HardwareSimulationBuilder { - var machine = new SimulatorStateMachine(realTime: false); - return machine; + IObservable inputSequence = Observable.Empty(); + + public SimulatorStateMachine Build() + { + var machine = new SimulatorStateMachine(realTime: false, timeSource: new FakeClock(DateTime.UtcNow)); + return machine; + } } - } -} + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Contexts/DeviceControllerContext.cs b/TA.DigitalDomeworks.Specifications/Contexts/DeviceControllerContext.cs index 66ab2c2..9b0e13a 100644 --- a/TA.DigitalDomeworks.Specifications/Contexts/DeviceControllerContext.cs +++ b/TA.DigitalDomeworks.Specifications/Contexts/DeviceControllerContext.cs @@ -1,5 +1,6 @@ using TA.Ascom.ReactiveCommunications; using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.DeviceInterface.StateMachine; using TA.DigitalDomeworks.Specifications.Fakes; namespace TA.DigitalDomeworks.Specifications.Contexts { @@ -8,5 +9,9 @@ class DeviceControllerContext public DeviceController Controller { get; set; } public ICommunicationChannel Channel { get; set; } + + public ControllerStateMachine StateMachine { get; set; } + + public RxControllerActions Actions { get; set; } } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Contexts/LogSetup.cs b/TA.DigitalDomeworks.Specifications/Contexts/LogSetup.cs index c6b2f54..2bf9ab3 100644 --- a/TA.DigitalDomeworks.Specifications/Contexts/LogSetup.cs +++ b/TA.DigitalDomeworks.Specifications/Contexts/LogSetup.cs @@ -22,7 +22,10 @@ public void OnAssemblyStart() var configuration = new LoggingConfiguration(); var unitTestRunnerTarget = new TraceTarget(); configuration.AddTarget("Unit test runner", unitTestRunnerTarget); - var logEverything = new LoggingRule("*", LogLevel.Debug, unitTestRunnerTarget); + unitTestRunnerTarget.Layout = + "${time}|${pad:padding=-5:inner=${uppercase:${level}}}|${pad:padding=-16:inner=${callsite:className=true:fileName=false:includeSourcePath=false:methodName=false:includeNamespace=false}}|${message}"; + unitTestRunnerTarget.RawWrite = true; + var logEverything = new LoggingRule("*", LogLevel.Trace, unitTestRunnerTarget); configuration.LoggingRules.Add(logEverything); LogManager.Configuration = configuration; log = LogManager.GetCurrentClassLogger(); diff --git a/TA.DigitalDomeworks.Specifications/Contexts/with_device_builder_context.cs b/TA.DigitalDomeworks.Specifications/Contexts/with_device_builder_context.cs index 99d1732..8880623 100644 --- a/TA.DigitalDomeworks.Specifications/Contexts/with_device_builder_context.cs +++ b/TA.DigitalDomeworks.Specifications/Contexts/with_device_builder_context.cs @@ -1,13 +1,19 @@ -using Machine.Specifications; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: with_device_builder_context.cs Last modified: 2018-03-19@17:38 by Tim Long + +using Machine.Specifications; using TA.Ascom.ReactiveCommunications; using TA.DigitalDomeworks.DeviceInterface; using TA.DigitalDomeworks.Specifications.Builders; -using TA.DigitalDomeworks.Specifications.DeviceInterface; using TA.DigitalDomeworks.Specifications.Fakes; namespace TA.DigitalDomeworks.Specifications.Contexts { - class with_device_controller_context + #region Context base classes + internal class with_device_controller_context { Establish context = () => DeviceControllerContextBuilder = new DeviceControllerContextBuilder(); Cleanup after = () => @@ -15,15 +21,17 @@ class with_device_controller_context DeviceControllerContextBuilder = null; Context = null; }; + protected static DeviceControllerContext Context; + + protected static DeviceControllerContextBuilder DeviceControllerContextBuilder; #region Convenience properties public static DeviceController Controller => Context.Controller; public static ICommunicationChannel Channel => Context.Channel; + public static FakeCommunicationChannel FakeChannel => Context.Channel as FakeCommunicationChannel; #endregion Convenience properties - - protected static DeviceControllerContextBuilder DeviceControllerContextBuilder; - protected static DeviceControllerContext Context; } + #endregion } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_directionless_rotating_dome.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_directionless_rotating_dome.cs new file mode 100644 index 0000000..df5a4b9 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_directionless_rotating_dome.cs @@ -0,0 +1,18 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: a_directionless_rotating_dome.cs Last modified: 2018-03-14@00:27 by Tim Long + +using Machine.Specifications; + +namespace TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours + { + [Behaviors] + internal class a_directionless_rotating_dome : device_controller_behaviour + { + It should_be_rotating = () => Controller.AzimuthMotorActive.ShouldBeTrue(); + It should_have_a_stationary_shutter = () => Controller.ShutterMotorActive.ShouldBeFalse(); + It should_indicate_that_something_is_moving = () => Controller.IsMoving.ShouldBeTrue(); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_dome_with_a_moving_shutter.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_dome_with_a_moving_shutter.cs new file mode 100644 index 0000000..519af96 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_dome_with_a_moving_shutter.cs @@ -0,0 +1,21 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: a_dome_with_a_moving_shutter.cs Last modified: 2018-03-14@00:17 by Tim Long + +using Machine.Specifications; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours + { + [Behaviors] + internal class a_dome_with_a_moving_shutter : device_controller_behaviour + { + It should_not_have_rotation_direction = () => Controller.AzimuthDirection.ShouldEqual(RotationDirection.None); + It should_not_indicate_that_the_azimuth_motor_is_active = + () => Controller.AzimuthMotorActive.ShouldBeFalse(); + It should_indicate_that_the_shutter_motor_is_active = () => Controller.ShutterMotorActive.ShouldBeTrue(); + It should_indicate_that_something_is_moving = () => Controller.IsMoving.ShouldBeTrue(); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_rotating_dome.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_rotating_dome.cs new file mode 100644 index 0000000..f2b8bce --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_rotating_dome.cs @@ -0,0 +1,20 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: a_rotating_dome.cs Last modified: 2018-03-14@00:27 by Tim Long + +using Machine.Specifications; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours + { + [Behaviors] + internal class a_rotating_dome : device_controller_behaviour + { + It should_be_rotating = () => Controller.AzimuthMotorActive.ShouldBeTrue(); + It should_have_a_stationary_shutter = () => Controller.ShutterMotorActive.ShouldBeFalse(); + It should_indicate_that_something_is_moving = () => Controller.IsMoving.ShouldBeTrue(); + It should_have_a_rotation_direction = () => Controller.AzimuthDirection.ShouldNotEqual(RotationDirection.None); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_stopped_dome.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_stopped_dome.cs new file mode 100644 index 0000000..49074ee --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/a_stopped_dome.cs @@ -0,0 +1,19 @@ +using Machine.Specifications; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.SharedTypes; +using TA.DigitalDomeworks.Specifications.Contexts; + +namespace TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours + { + [Behaviors] + internal class a_stopped_dome : device_controller_behaviour + { + It should_not_be_rotating = () => Controller.AzimuthMotorActive.ShouldBeFalse(); + It should_should_not_be_moving_at_all = () => Controller.IsMoving.ShouldBeFalse(); + It should_not_have_a_rotation_direction = + () => Controller.AzimuthDirection.ShouldEqual(RotationDirection.None); + It should_draw_no_shutter_current = () => Controller.ShutterMotorCurrent.ShouldEqual(0); + It should_not_have_a_shutter_direction = () => Controller.ShutterMovementDirection.ShouldEqual(ShutterDirection.None); + It should_have_a_stationary_shutter = () => Controller.ShutterMotorActive.ShouldBeFalse(); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/device_controller_behaviour.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/device_controller_behaviour.cs new file mode 100644 index 0000000..f04b2b2 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/Behaviours/device_controller_behaviour.cs @@ -0,0 +1,22 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: device_controller_behaviour.cs Last modified: 2018-03-14@00:06 by Tim Long + +using JetBrains.Annotations; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.Specifications.Contexts; + +#pragma warning disable 0649 // Context never assigned + +namespace TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours + { + [UsedImplicitly] + internal class device_controller_behaviour + { + protected static DeviceControllerContext Context; + + protected static DeviceController Controller => Context.Controller; + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/ConnectionSpecs.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/ConnectionSpecs.cs index 3e0dabe..22012bd 100644 --- a/TA.DigitalDomeworks.Specifications/DeviceInterface/ConnectionSpecs.cs +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/ConnectionSpecs.cs @@ -1,34 +1,22 @@ -using System; -using System.ComponentModel; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ConnectionSpecs.cs Last modified: 2018-03-28@15:44 by Tim Long + +using System; using System.Linq; using Machine.Specifications; using TA.DigitalDomeworks.DeviceInterface; -using TA.DigitalDomeworks.SharedTypes; +using TA.DigitalDomeworks.HardwareSimulator; using TA.DigitalDomeworks.Specifications.Contexts; -using TA.DigitalDomeworks.Specifications.Helpers; +using TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours; using TI.DigitalDomeWorks; +#pragma warning disable 0169 + namespace TA.DigitalDomeworks.Specifications.DeviceInterface { - [Subject(typeof(DeviceController), "connection status")] - class when_the_channel_is_open : with_device_controller_context - { - Establish context = () => Context = DeviceControllerContextBuilder - .WithOpenConnection("Fake") - .WithFakeResponse("Yoohoo2U2") - .Build(); - It should_be_online = () => Controller.IsOnline.ShouldBeTrue(); - } - - [Subject(typeof(DeviceController), "connection status")] - class when_the_channel_is_closed : with_device_controller_context - { - Establish context = () => Context = DeviceControllerContextBuilder - .WithClosedConnection("Fake") - .Build(); - It should_be_offline = () => Controller.IsOnline.ShouldBeFalse(); - } - /* * Given a new DeviceController * When Open() is called @@ -40,26 +28,20 @@ class when_the_channel_is_closed : with_device_controller_context */ [Subject(typeof(DeviceController), "tasks on connect")] - internal class when_opening_the_communications_channel : with_device_controller_context + internal class when_opening_the_controller : with_device_controller_context { Establish context = () => Context = DeviceControllerContextBuilder .WithClosedConnection("Simulator:Fast") - .OnPropertyChanged(DetectStatusChanged) .Build(); - static void DetectStatusChanged(object sender, PropertyChangedEventArgs e) - { - statusChanged = e.PropertyName == nameof(Controller.CurrentStatus); - } + Because of = () => exception = Catch.Exception(() => Controller.Open()); + It should_send_a_status_request = () => SimulatorChannel.SendLog.First().ShouldEqual("GINF"); + It should_perform_shutter_recovery = + () => SimulatorChannel.SendLog.Skip(1).First().ShouldEqual(Constants.CmdClose); + It should_connect_successfully = () => exception.ShouldBeNull(); + static Exception exception; + Behaves_like stopped_dome; - Because of = () => Exception = Catch.Exception(() => Controller.Open()); - It should_send_a_status_request = () => - (Channel as SimulatorCommunicationsChannel).SendLog.Single().ShouldEqual("GINF"); - It should_connect_successfully = () => Exception.ShouldBeNull(); - It should_update_internal_state_to_reflect_the_received_status_response = () => - statusChanged.ShouldBeTrue(); - static IControllerStatus status; - static Exception Exception; - static bool statusChanged; + static SimulatorCommunicationsChannel SimulatorChannel => (SimulatorCommunicationsChannel) Channel; } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStateMachineSpecs.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStateMachineSpecs.cs new file mode 100644 index 0000000..c7b7234 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStateMachineSpecs.cs @@ -0,0 +1,227 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ControllerStateMachineSpecs.cs Last modified: 2018-06-16@16:54 by Tim Long + +using System; +using FakeItEasy; +using JetBrains.Annotations; +using Machine.Specifications; +using TA.DigitalDomeworks.DeviceInterface.StateMachine; +using TA.DigitalDomeworks.SharedTypes; +using TA.DigitalDomeworks.Specifications.Fakes; +using TA.DigitalDomeworks.Specifications.Helpers; +using TI.DigitalDomeWorks; + +namespace TA.DigitalDomeworks.Specifications + { + #region Context base classes + internal class with_default_controller_state_machine + { + Establish context = () => + { + FakeControllerActions = A.Fake(); + var options = new DeviceControllerOptions + { + KeepAliveTimerInterval = TimeSpan.FromMinutes(3), + MaximumFullRotationTime = TimeSpan.FromMinutes(1), + MaximumShutterCloseTime = TimeSpan.FromMinutes(1), + PerformShutterRecovery = false, + IgnoreHardwareShutterSensor = false, + CurrentDrawDetectionThreshold = 10 + }; + Machine = new ControllerStateMachine(FakeControllerActions, options, new SystemDateTimeUtcClock()); + }; + Cleanup after = () => + { + StatusRequested = false; + Machine = null; + }; + protected static Exception Exception; + protected static IControllerActions FakeControllerActions; + protected static ControllerStateMachine Machine; + protected static bool StatusRequested; + + static void SimulateRequestStatus() + { + StatusRequested = true; + } + } + + internal class with_state_machine_that_infers_shutter_position + { + Establish context = () => + { + FakeControllerActions = A.Fake(); + Clock = new FakeClock(new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var options = new DeviceControllerOptions + { + KeepAliveTimerInterval = TimeSpan.FromMinutes(3), + MaximumFullRotationTime = TimeSpan.FromMinutes(1), + MaximumShutterCloseTime = TimeSpan.FromMinutes(1), + PerformShutterRecovery = false, + IgnoreHardwareShutterSensor = true, + CurrentDrawDetectionThreshold = 10, + ShutterTickTimeout = TimeSpan.MaxValue + }; + Machine = new ControllerStateMachine(FakeControllerActions, options, Clock); + }; + Cleanup after = () => + { + StatusRequested = false; + Machine = null; + }; + protected static Exception Exception; + protected static IControllerActions FakeControllerActions; + protected static ControllerStateMachine Machine; + protected static bool StatusRequested; + protected static FakeClock Clock; + + static void SimulateRequestStatus() + { + StatusRequested = true; + } + } + + internal class with_controller_state_machine_in_ready_state : with_default_controller_state_machine + { + Establish context = () => Machine.Initialize(new Ready(Machine)); + } + + internal class with_controller_state_machine_in_rotating_state : with_default_controller_state_machine + { + Establish context = () => Machine.Initialize(new Rotating(Machine)); + + static void SimulateRequestStatus() { } + } + #endregion + + [Subject(typeof(ControllerStateMachine), "construction")] + internal class when_the_state_machine_is_constructed : with_default_controller_state_machine + { + It should_start_in_the_uninitialized_state = () => Machine.CurrentState.Name.ShouldEqual(nameof(Uninitialized)); + } + + [Subject(typeof(ControllerStateMachine), "initialization")] + internal class when_the_user_fails_to_initialize_the_state_machine : with_default_controller_state_machine + { + Because of = () => Exception = Catch.Exception(() => Machine.AzimuthEncoderTickReceived(0)); + It should_throw = () => Exception.ShouldBeOfExactType(); + } + + [Subject(typeof(ControllerStateMachine), "startup")] + internal class when_the_state_machine_starts : with_default_controller_state_machine + { + Because of = () => + { + Machine.Initialize(new RequestStatus(Machine)); + var factory = new ControllerStatusFactory(new SystemDateTimeUtcClock()); + var newStatus = factory.FromStatusPacket(Constants.StrSimulatedStatusResponse); + Machine.HardwareStatusReceived(newStatus); + }; + It should_request_the_hardware_status = () => + A.CallTo(() => FakeControllerActions.RequestHardwareStatus()).MustHaveHappened(); + It should_finish_in_the_ready_state = () => Machine.CurrentState.Name.ShouldEqual(nameof(Ready)); + } + + [Subject(typeof(ControllerStateMachine), "local operations")] + internal class when_idle_and_an_azimuth_encoder_tick_is_received : with_controller_state_machine_in_ready_state + { + Because of = () => Machine.AzimuthEncoderTickReceived(100); + It should_transition_to_rotating_state = () => Machine.CurrentState.Name.ShouldEqual(nameof(Rotating)); + It should_update_the_azimuth_property = () => Machine.AzimuthEncoderPosition.ShouldEqual(100); + } + + [Subject(typeof(ControllerStateMachine), "local operations")] + internal class + when_idle_and_a_shutter_current_measurement_is_received : with_controller_state_machine_in_ready_state + { + Because of = () => Machine.ShutterMotorCurrentReceived(15); + It should_transition_to_shutter_moving_state = + () => Machine.CurrentState.Name.ShouldEqual(nameof(ShutterMoving)); + It should_update_the_shutter_current_property = () => Machine.ShutterMotorCurrent.ShouldEqual(15); + It should_not_set_a_shutter_direction = + () => Machine.ShutterMovementDirection.ShouldEqual(ShutterDirection.None); + Behaves_like the_shutter_is_moving; + } + + [Subject(typeof(ControllerStateMachine), "inferred shutter position")] + internal class when_the_shutter_moves_for_a_sufficiently_long_time : with_state_machine_that_infers_shutter_position + { + Because of = () => + { + var minimumRequiredMoveTime = Machine.Options.MaximumShutterCloseTime.TotalSeconds / 2; + Machine.Initialize(new Ready(Machine)); + var factory = new ControllerStatusFactory(Clock); + var newStatus = factory.FromStatusPacket(Constants.StrSimulatedStatusResponse); + Machine.ShutterDirectionReceived(ShutterDirection.Closing); + Clock.AdvanceBy(TimeSpan.FromSeconds(minimumRequiredMoveTime)); + Machine.ShutterMotorCurrentReceived(Machine.Options.CurrentDrawDetectionThreshold); + var indeterminateShutter = + factory.FromStatusPacket(TestData.FromEmbeddedResource("StatusWithIndeterminateShutter.txt")); + Machine.HardwareStatusReceived(indeterminateShutter); + }; + It should_finish_with_shutter_closed = () => Machine.ShutterPosition.ShouldEqual(SensorState.Closed); + } + + [Subject(typeof(ControllerStateMachine), "inferred shutter position")] + internal class when_the_shutter_moves_for_a_sufficiently_long_time_but_current_draw_is_too_low : with_state_machine_that_infers_shutter_position + { + Because of = () => + { + var minimumRequiredMoveTime = Machine.Options.MaximumShutterCloseTime.TotalSeconds / 2; + Machine.Initialize(new Ready(Machine)); + var factory = new ControllerStatusFactory(Clock); + var newStatus = factory.FromStatusPacket(Constants.StrSimulatedStatusResponse); + Machine.ShutterDirectionReceived(ShutterDirection.Closing); + Clock.AdvanceBy(TimeSpan.FromSeconds(minimumRequiredMoveTime)); + Machine.ShutterMotorCurrentReceived(Machine.Options.CurrentDrawDetectionThreshold - 1); + var indeterminateShutter = + factory.FromStatusPacket(TestData.FromEmbeddedResource("StatusWithIndeterminateShutter.txt")); + Machine.HardwareStatusReceived(indeterminateShutter); + }; + It should_finish_with_shutter_indeterminate = () => Machine.ShutterPosition.ShouldEqual(SensorState.Indeterminate); + } + + [Subject(typeof(ControllerStateMachine), "inferred shutter position")] + internal class when_the_shutter_move_is_insufficient : with_state_machine_that_infers_shutter_position + { + Because of = () => + { + var minimumRequiredMoveTime = Machine.Options.MaximumShutterCloseTime.TotalSeconds / 2; + Machine.Initialize(new Ready(Machine)); + var factory = new ControllerStatusFactory(Clock); + var newStatus = factory.FromStatusPacket(Constants.StrSimulatedStatusResponse); + Machine.ShutterDirectionReceived(ShutterDirection.Closing); + Clock.AdvanceBy(TimeSpan.FromSeconds(minimumRequiredMoveTime)-TimeSpan.FromTicks(1)); + Machine.ShutterMotorCurrentReceived(Machine.Options.CurrentDrawDetectionThreshold); + var indeterminateShutter = + factory.FromStatusPacket(TestData.FromEmbeddedResource("StatusWithIndeterminateShutter.txt")); + Machine.HardwareStatusReceived(indeterminateShutter); + }; + It should_finish_with_shutter_indeterminate = () => Machine.ShutterPosition.ShouldEqual(SensorState.Indeterminate); + } + + [Behaviors] + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + internal class ShutterMoving + { + [UsedImplicitly] protected static ControllerStateMachine Machine; + It should_indicate_shutter_motor_active = () => Machine.ShutterMotorActive.ShouldBeTrue(); + It should_not_indicate_azimuth_movement = () => Machine.AzimuthMotorActive.ShouldBeFalse(); + It should_not_indicate_rotation_direction = () => Machine.AzimuthDirection.ShouldEqual(RotationDirection.None); + } + + [Behaviors] + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] + internal class AzimuthRotation + { + [UsedImplicitly] protected static ControllerStateMachine Machine; + It should_not_indicate_shutter_motor_active = () => Machine.ShutterMotorActive.ShouldBeFalse(); + It should_not_indicate_shutter_direction = + () => Machine.ShutterMovementDirection.ShouldEqual(ShutterDirection.None); + It should_not_indicate_any_shutter_motor_current = () => Machine.ShutterMotorCurrent.ShouldEqual(0); + It should_indicate_azimuth_movement = () => Machine.AzimuthMotorActive.ShouldBeTrue(); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStatusSpecs.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStatusSpecs.cs index 817b6e5..30e8734 100644 --- a/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStatusSpecs.cs +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/ControllerStatusSpecs.cs @@ -1,17 +1,26 @@ -using System; +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ControllerStatusSpecs.cs Last modified: 2018-03-28@18:17 by Tim Long + +using System; using Machine.Specifications; -using NodaTime; -using NodaTime.Testing; using TA.DigitalDomeworks.DeviceInterface; using TA.DigitalDomeworks.SharedTypes; +using TA.DigitalDomeworks.Specifications.Contexts; +using TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours; using TA.DigitalDomeworks.Specifications.Fakes; +#pragma warning disable 0169 // Field not used, triggers on Behaves_like<> + namespace TA.DigitalDomeworks.Specifications.DeviceInterface { - [Subject(typeof(ControllerStatus), "creation")] - class when_creating_a_status + [Subject(typeof(HardwareStatus), "creation")] + internal class when_creating_a_status { - Establish context = () => factory=new ControllerStatusFactory(new FakeClock(Instant.MinValue)); + Establish context = () => + factory = new ControllerStatusFactory(new FakeClock(DateTime.MinValue.ToUniversalTime())); Because of = () => actual = factory.FromStatusPacket(RealWorldStatusPacket); It should_be_v4 = () => actual.FirmwareVersion.ShouldEqual("V4"); It should_have_circumference = () => actual.DomeCircumference.ShouldEqual(704); @@ -37,9 +46,26 @@ class when_creating_a_status It should_have_dead_zone = () => actual.DeadZone.ShouldEqual(5); It should_have_offset = () => actual.Offset.ShouldEqual(5); //ToDo: fill in the other fields - static IControllerStatus actual; + static IHardwareStatus actual; static ControllerStatusFactory factory; // This status packet was captured from real hardware. const string RealWorldStatusPacket = "V4,704,293,1,289,0,0,1,0,287,299,0,0,0,0,112,50,0,0,0,180,5,5"; } + + [Subject(typeof(DeviceController), "property updates")] + internal class when_a_status_packet_is_received : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Simulator:Fast") + .Build(); + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Context.Actions.RequestHardwareStatus(); + Context.StateMachine.WaitForReady(TimeSpan.FromSeconds(5)); + }; + static IHardwareStatus receivedStatus; + Behaves_like stopped_dome; + const string RealWorldStatusPacket = "V4,704,293,1,289,0,0,1,0,287,299,0,0,0,0,112,50,0,0,0,180,5,5\n"; + } } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/DeviceInterface/MovementUpdateSpecs.cs b/TA.DigitalDomeworks.Specifications/DeviceInterface/MovementUpdateSpecs.cs new file mode 100644 index 0000000..1a916e0 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/DeviceInterface/MovementUpdateSpecs.cs @@ -0,0 +1,126 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: MovementUpdateSpecs.cs Last modified: 2018-03-14@00:31 by Tim Long + +using Machine.Specifications; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.SharedTypes; +using TA.DigitalDomeworks.Specifications.Contexts; +using TA.DigitalDomeworks.Specifications.DeviceInterface.Behaviours; + +#pragma warning disable 0169 // Field not used, triggers on Behaves_like<> + +namespace TA.DigitalDomeworks.Specifications.DeviceInterface + { + [Subject(typeof(DeviceController), "Encoder Ticks")] + internal class when_an_encoder_tick_is_received : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Fake") + .WithFakeResponse("P99\n") + .Build(); + + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Channel.Send(string.Empty); + }; + It should_update_the_position_property = () => Controller.AzimuthEncoderPosition.ShouldEqual(99); + Behaves_like _; + } + + [Subject(typeof(DeviceController), "Direction")] + internal class when_the_dome_begins_to_rotate_counterclockwise : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Fake") + .WithFakeResponse("L") + .Build(); + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Channel.Send(string.Empty); + }; + It should_be_rotating_counter_clockwise = + () => Controller.AzimuthDirection.ShouldEqual(RotationDirection.CounterClockwise); + Behaves_like _; + } + + [Subject(typeof(DeviceController), "Direction")] + internal class when_the_dome_begins_to_rotate_clockwise : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Fake") + .WithFakeResponse("R") + .Build(); + + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Channel.Send(string.Empty); + }; + It should_be_rotating_clockwise = () => Controller.AzimuthDirection.ShouldEqual(RotationDirection.Clockwise); + Behaves_like _; + } + + [Subject(typeof(DeviceController), "Shutter Current")] + internal class when_the_device_sends_a_shutter_current_reading : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Fake") + .WithFakeResponse("Z15\n") + .Build(); + + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Channel.Send(string.Empty); + }; + It should_update_the_shutter_current_property = () => Controller.ShutterMotorCurrent.ShouldEqual(15); + Behaves_like _; + } + + [Subject(typeof(DeviceController), "Shutter Direction")] + internal class when_the_shutter_begins_to_close : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Fake") + .WithFakeResponse("C") + .Build(); + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Channel.Send(string.Empty); + }; + It should_be_closing = () => Controller.ShutterMovementDirection.ShouldEqual(ShutterDirection.Closing); + Behaves_like _; + } + + [Subject(typeof(DeviceController), "Shutter Direction")] + internal class when_the_shutter_begins_to_open : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithClosedConnection("Fake") + .WithFakeResponse("O") + .Build(); + Because of = () => + { + Controller.Open(performOnConnectActions: false); + Channel.Send(string.Empty); + }; + It should_be_opening = () => Controller.ShutterMovementDirection.ShouldEqual(ShutterDirection.Opening); + Behaves_like _; + } + + [Subject(typeof(DeviceController), "emergency stop")] + internal class when_the_client_requests_an_emergency_stop : with_device_controller_context + { + Establish context = () => Context = DeviceControllerContextBuilder + .WithOpenConnection("Fake") + .Build(); + Because of = () => Controller.RequestEmergencyStop(); + It should_send_the_emergency_stop_command_three_times = () => FakeChannel.SendLog.ShouldEqual("STOP\nSTOP\nSTOP\n"); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Fakes/FakeClock.cs b/TA.DigitalDomeworks.Specifications/Fakes/FakeClock.cs new file mode 100644 index 0000000..8c23583 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/Fakes/FakeClock.cs @@ -0,0 +1,44 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: FakeClock.cs Last modified: 2018-03-28@18:42 by Tim Long + +using System; +using System.Diagnostics.Contracts; +using TA.DigitalDomeworks.SharedTypes; + +namespace TA.DigitalDomeworks.Specifications.Fakes + { + internal class FakeClock : IClock + { + DateTime currentTime; + + public FakeClock(DateTime initialTime) + { + currentTime = initialTime; + } + + [Pure] + public DateTime GetCurrentTime() => currentTime; + + public void AdvanceBy(TimeSpan amount) + { + Contract.Requires(amount > TimeSpan.Zero); + currentTime += amount; + } + + public void AdvanceTo(DateTime time) + { + Contract.Requires(time > GetCurrentTime()); + Contract.Requires(time.Kind == DateTimeKind.Utc); + currentTime = time; + } + + [ContractInvariantMethod] + void ObjectInvariant() + { + Contract.Invariant(currentTime.Kind == DateTimeKind.Utc); + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Fakes/FakeCommunicationChannel.cs b/TA.DigitalDomeworks.Specifications/Fakes/FakeCommunicationChannel.cs index f98f43e..c5c04a3 100644 --- a/TA.DigitalDomeworks.Specifications/Fakes/FakeCommunicationChannel.cs +++ b/TA.DigitalDomeworks.Specifications/Fakes/FakeCommunicationChannel.cs @@ -1,6 +1,10 @@ using System; +using System.Diagnostics.Contracts; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Text; +using Machine.Specifications.Model; +using NLog.Fluent; using TA.Ascom.ReactiveCommunications; namespace TA.DigitalDomeworks.Specifications.Fakes @@ -13,6 +17,7 @@ namespace TA.DigitalDomeworks.Specifications.Fakes public class FakeCommunicationChannel : ICommunicationChannel { readonly IObservable receivedCharacters; + readonly Subject receiveChannelSubject = new Subject(); readonly StringBuilder sendLog; /// @@ -22,10 +27,10 @@ public class FakeCommunicationChannel : ICommunicationChannel /// Implementation of the injected dependency. public FakeCommunicationChannel(string fakeResponse) { + Contract.Requires(fakeResponse != null); Endpoint = new InvalidEndpoint(); Response = fakeResponse; receivedCharacters = fakeResponse.ToCharArray().ToObservable(); - ObservableReceivedCharacters = receivedCharacters.Concat(Observable.Never()); sendLog = new StringBuilder(); IsOpen = false; } @@ -67,10 +72,15 @@ public void Close() public void Send(string txData) { + Log.Info().Message($"Send: {txData}").Property(nameof(txData), txData).Write(); sendLog.Append(txData); + foreach (char c in Response) + { + receiveChannelSubject.OnNext(c); + } } - public IObservable ObservableReceivedCharacters { get; } + public IObservable ObservableReceivedCharacters => receiveChannelSubject.AsObservable(); public bool IsOpen { get; set; } diff --git a/TA.DigitalDomeworks.Specifications/Fakes/TransactionExtensions.cs b/TA.DigitalDomeworks.Specifications/Fakes/TransactionExtensions.cs index 0de6c54..92a10ec 100644 --- a/TA.DigitalDomeworks.Specifications/Fakes/TransactionExtensions.cs +++ b/TA.DigitalDomeworks.Specifications/Fakes/TransactionExtensions.cs @@ -1,9 +1,8 @@ -// This file is part of the AWR Drive System ASCOM Driver project +// This file is part of the TA.DigitalDomeworks project // -// Copyright © 2007-2017 Tigra Astronomy, all rights reserved. +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. // -// File: TransactionExtensions.cs Created: 2017-03-21@01:29 -// Last modified: 2017-03-21@01:33 by Tim Long +// File: TransactionExtensions.cs Last modified: 2018-03-29@21:27 by Tim Long using System; using System.Reflection; @@ -15,7 +14,7 @@ namespace TA.DigitalDomeworks.Specifications.Fakes /// /// Extension methods for manipulating non-public members of transaction classes. /// - static class TransactionExtensions + internal static class TransactionExtensions { /// /// Sets the protected response property. @@ -44,11 +43,11 @@ public static void SimulateCompletionWithResponse(this DeviceTransaction transac var makeHotMethod = transactionType.GetMethod("MakeHot", BindingFlags.Instance | BindingFlags.NonPublic); makeHotMethod.Invoke(transaction, BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, - new object[] {}, null); + new object[] { }, null); var onCompletedMethod = transactionType.GetMethod("OnCompleted", BindingFlags.Instance | BindingFlags.NonPublic); onCompletedMethod.Invoke(transaction, BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, - new object[] {}, null); + new object[] { }, null); } /// diff --git a/TA.DigitalDomeworks.Specifications/Helpers/ObservableTestExtensions.cs b/TA.DigitalDomeworks.Specifications/Helpers/ObservableTestExtensions.cs new file mode 100644 index 0000000..99096b4 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/Helpers/ObservableTestExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace TA.DigitalDomeworks.Specifications.Helpers +{ + internal static class ObservableTestExtensions + { + + public static void SubscribeAndWaitForCompletion(this IObservable sequence, Action observer) + { + var sequenceComplete = new ManualResetEvent(false); + var subscription = sequence.Subscribe( + onNext: observer, + onCompleted: () => sequenceComplete.Set() + ); + sequenceComplete.WaitOne(); + subscription.Dispose(); + sequenceComplete.Dispose(); + } + } +} diff --git a/TA.DigitalDomeworks.Specifications/Helpers/SimulatorCommunicationsChannel.cs b/TA.DigitalDomeworks.Specifications/Helpers/SimulatorCommunicationsChannel.cs deleted file mode 100644 index ae8fe79..0000000 --- a/TA.DigitalDomeworks.Specifications/Helpers/SimulatorCommunicationsChannel.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using TA.Ascom.ReactiveCommunications; -using TA.DigitalDomeworks.HardwareSimulator; - -namespace TA.DigitalDomeworks.Specifications.Helpers - { - internal class SimulatorCommunicationsChannel : ICommunicationChannel - { - readonly SimulatorStateMachine simulator; - - public SimulatorCommunicationsChannel(SimulatorEndpoint endpoint) - { - Endpoint = endpoint; - simulator = new SimulatorStateMachine(endpoint.Realtime); - } - - public void Dispose() - { - simulator?.InputObserver.OnCompleted(); - } - - public void Open() - { - IsOpen = true; - } - - public void Close() - { - IsOpen = false; - } - - public void Send(string txData) - { - SendLog.Add(txData); - foreach (var c in txData) simulator.InputObserver.OnNext(c); - } - - public List SendLog { get; } = new List(); - - public IObservable ObservableReceivedCharacters => simulator.ObservableResponses; - - public bool IsOpen { get; private set; } - - public DeviceEndpoint Endpoint { get; } - } - } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Helpers/SimulatorEndpoint.cs b/TA.DigitalDomeworks.Specifications/Helpers/SimulatorEndpoint.cs deleted file mode 100644 index c50f74e..0000000 --- a/TA.DigitalDomeworks.Specifications/Helpers/SimulatorEndpoint.cs +++ /dev/null @@ -1,52 +0,0 @@ -// This file is part of the TA.DigitalDomeworks project -// -// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. -// -// File: SimulatorEndpoint.cs Last modified: 2018-03-08@19:15 by Tim Long - -using System; -using System.Text.RegularExpressions; -using TA.Ascom.ReactiveCommunications; - -namespace TA.DigitalDomeworks.Specifications.Helpers - { - /// - /// Endpoint representing the hardware simulator. - /// Connection string format: Simulator:Realtime or Simulator:Fast - /// - internal class SimulatorEndpoint : DeviceEndpoint - { - const string realtime = "Realtime"; - const string fast = "Fast"; - string connectionString; - const string connectionPattern = @"^Simulator(:(?(Realtime)|(Fast)))?$"; - static readonly Regex connectionRegex = new Regex(connectionPattern, Options); - - public static SimulatorEndpoint FromConnectionString(string connection) - { - if (!IsConnectionStringValid(connection)) - throw new ArgumentException($"Invalid connection string: {connection}"); - var matches = connectionRegex.Match(connection); - var speed = CaptureGroupOrDefault(matches, "Speed", "Fast"); - var timing = speed.Equals(realtime, StringComparison.InvariantCultureIgnoreCase); - return new SimulatorEndpoint(connection) {Realtime = timing}; - } - - public static bool IsConnectionStringValid(string connection) - { - return connectionRegex.IsMatch(connection); - } - - SimulatorEndpoint(string connectionString) - { - this.connectionString = connectionString; - } - - public bool Realtime { get; set; } - - public override string ToString() - { - return $"Simulator:{(Realtime ? realtime : fast)}"; - } - } - } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Helpers/TestData.cs b/TA.DigitalDomeworks.Specifications/Helpers/TestData.cs new file mode 100644 index 0000000..5b486dd --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/Helpers/TestData.cs @@ -0,0 +1,27 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: TestData.cs Last modified: 2018-06-16@16:20 by Tim Long + +using System.IO; +using System.Reflection; + +namespace TA.DigitalDomeworks.Specifications.Helpers + { + internal static class TestData + { + internal static string FromEmbeddedResource(string resourceName) + { + var asm = Assembly.GetExecutingAssembly(); + var asmName = asm.GetName().Name; + var resourceRoot = $"{asmName}.TestData"; + var resource = $"{resourceRoot}.{resourceName}"; + using (var stream = asm.GetManifestResourceStream(resource)) + { + var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + } + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/ObservableExtensionSpecs.cs b/TA.DigitalDomeworks.Specifications/ObservableExtensionSpecs.cs new file mode 100644 index 0000000..09e0132 --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/ObservableExtensionSpecs.cs @@ -0,0 +1,45 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: ObservableExtensionSpecs.cs Last modified: 2018-03-13@19:21 by Tim Long + +using System; +using System.Collections.Generic; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Threading; +using Machine.Specifications; +using Microsoft.Reactive.Testing; +using TA.Ascom.ReactiveCommunications.Diagnostics; +using TA.DigitalDomeworks.DeviceInterface; +using TA.DigitalDomeworks.Specifications.Helpers; +using ObservableExtensions = TA.DigitalDomeworks.DeviceInterface.ObservableExtensions; + +namespace TA.DigitalDomeworks.Specifications + { + [Subject(typeof(ObservableExtensions), "Encoder Ticks")] + internal class when_an_encoder_tick_is_received + { + Establish context = () => source = "P99\nP100\nP101\n".ToObservable(); + Because of = () => source.AzimuthEncoderTicks().SubscribeAndWaitForCompletion(tick => tickHistory.Add(tick)); + It should_receive_the_encoder_ticks = () => tickHistory.ShouldEqual(expectedTicks); + static List expectedTicks = new List {99, 100, 101}; + static IObservable source; + static List tickHistory = new List(); + } + + [Subject(typeof(ObservableExtensions), "Shutter Current Readings")] + internal class when_a_shutter_current_reading_is_received + { + Establish context = () => source = "Z8\nZ10\nZ11\n".ToObservable(); + Because of = () => source + .ShutterCurrentReadings().Trace("Unbelievable") + .SubscribeAndWaitForCompletion(item => elementHistory.Add(item)); + It should_receive_the_current_readings = () => elementHistory.ShouldEqual(expectedElements); + static List elementHistory = new List(); + static List expectedElements = new List {8, 10, 11}; + static IObservable source; + + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/Simulator/SimulatorEndpointSpecs.cs b/TA.DigitalDomeworks.Specifications/Simulator/SimulatorEndpointSpecs.cs new file mode 100644 index 0000000..15d2cab --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/Simulator/SimulatorEndpointSpecs.cs @@ -0,0 +1,29 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: SimulatorEndpointSpecs.cs Last modified: 2018-03-28@18:30 by Tim Long + +using System; +using Machine.Specifications; +using TA.DigitalDomeworks.HardwareSimulator; + +namespace TA.DigitalDomeworks.Specifications + { + [Subject(typeof(SimulatorEndpoint), "code contracts")] + internal class when_violating_a_code_contract + { + Because of = () => Exception = Catch.Exception(() => SimulatorEndpoint.IsConnectionStringValid(null)); + It should_throw = () => Exception.ShouldNotBeNull(); + static Exception Exception; + } + + [Subject(typeof(SimulatorEndpoint), "connection string")] + internal class when_validating_a_valid_connection_string + { + It should_succeed_for_realtime = + () => SimulatorEndpoint.IsConnectionStringValid("Simulator:Realtime").ShouldBeTrue(); + It should_succeed_for_fast = () => SimulatorEndpoint.IsConnectionStringValid("Simulator:Fast").ShouldBeTrue(); + It should_succeed_for_default = () => SimulatorEndpoint.IsConnectionStringValid("Simulator").ShouldBeTrue(); + } + } \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/TA.DigitalDomeworks.Specifications.csproj b/TA.DigitalDomeworks.Specifications/TA.DigitalDomeworks.Specifications.csproj index c3e269a..8df2849 100644 --- a/TA.DigitalDomeworks.Specifications/TA.DigitalDomeworks.Specifications.csproj +++ b/TA.DigitalDomeworks.Specifications/TA.DigitalDomeworks.Specifications.csproj @@ -1,6 +1,45 @@  + + + True + False + True + False + False + True + True + True + True + True + True + True + True + True + False + True + False + False + False + True + True + True + True + False + False + False + False + True + Full + 0 + True + True + True + True + DoNotBuild + True + Debug AnyCPU @@ -11,6 +50,8 @@ TA.DigitalDomeworks.Specifications v4.6.2 512 + + true @@ -30,6 +71,9 @@ 4 + + ..\packages\FakeItEasy.4.5.1\lib\net45\FakeItEasy.dll + ..\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll @@ -39,17 +83,16 @@ ..\packages\Machine.Specifications.Should.0.11.0\lib\net45\Machine.Specifications.Should.dll - - ..\packages\NLog.4.4.13\lib\net45\NLog.dll - - - ..\packages\NodaTime.2.2.4\lib\net45\NodaTime.dll + + ..\packages\Microsoft.Reactive.Testing.3.1.1\lib\net46\Microsoft.Reactive.Testing.dll - - ..\packages\NodaTime.Testing.2.2.4\lib\net45\NodaTime.Testing.dll + + ..\packages\NLog.4.5.0\lib\net45\NLog.dll + + ..\packages\System.Reactive.Core.3.1.1\lib\net46\System.Reactive.Core.dll @@ -66,6 +109,12 @@ ..\packages\System.Reactive.Windows.Threading.3.1.1\lib\net45\System.Reactive.Windows.Threading.dll + + + + + ..\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll + @@ -74,8 +123,8 @@ - - ..\packages\TA.Ascom.ReactiveCommunications.0.5.1\lib\net45\TA.Ascom.ReactiveCommunications.dll + + ..\packages\TA.Ascom.ReactiveCommunications.1.0.0\lib\net45\TA.Ascom.ReactiveCommunications.dll @@ -85,17 +134,27 @@ + + + + + + + + - - + + + + @@ -113,8 +172,24 @@ + + Designer + - + + + + + + + + + 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/TA.DigitalDomeworks.Specifications/TestData/StatusWithIndeterminateShutter.txt b/TA.DigitalDomeworks.Specifications/TestData/StatusWithIndeterminateShutter.txt new file mode 100644 index 0000000..d38c3bc --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/TestData/StatusWithIndeterminateShutter.txt @@ -0,0 +1 @@ +V4,407,376,2,376,0,0,1,0,371,382,0,128,255,255,255,255,255,255,255,999,3,0 \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/nlog.dll.nlog b/TA.DigitalDomeworks.Specifications/nlog.dll.nlog new file mode 100644 index 0000000..fb66edc --- /dev/null +++ b/TA.DigitalDomeworks.Specifications/nlog.dll.nlog @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.Specifications/packages.config b/TA.DigitalDomeworks.Specifications/packages.config index 16084ae..c6b3912 100644 --- a/TA.DigitalDomeworks.Specifications/packages.config +++ b/TA.DigitalDomeworks.Specifications/packages.config @@ -1,16 +1,19 @@  + + + - - - + + - + + \ No newline at end of file diff --git a/TA.DigitalDomeworks.sln b/TA.DigitalDomeworks.sln index ff4ccb1..7eb981d 100644 --- a/TA.DigitalDomeworks.sln +++ b/TA.DigitalDomeworks.sln @@ -13,34 +13,103 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.DigitalDomeworks.SharedT EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.DigitalDomeworks.DeviceInterface", "TA.DigitalDomeworks.DeviceInterface\TA.DigitalDomeworks.DeviceInterface.csproj", "{020924B9-23D5-4B92-B5B1-461423BBE23C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.DigitalDomeworks.AscomDome", "TA.DigitalDomeworks.AscomDome\TA.DigitalDomeworks.AscomDome.csproj", "{0B22461B-2D19-4342-A87D-4E90EFD3E2A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.DigitalDomeworks.Server", "TA.DigitalDomeworks.Server\TA.DigitalDomeworks.Server.csproj", "{3689A2CB-94C5-4012-A5CF-7E7D1DD27143}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.PostSharp.Aspects", "TA.PostSharp.Aspects\TA.PostSharp.Aspects.csproj", "{9CDCF319-DADC-41EB-B787-DE3862017E95}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TA.DigitalDomeworks.AscomSwitch", "TA.DigitalDomeworks.AscomSwitch\TA.DigitalDomeworks.AscomSwitch.csproj", "{F95208C4-450F-4B51-810A-C805A8EFBB7A}" +EndProject +Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "TA.DigitalDomeworks.Installer", "TA.DigitalDomeworks.Installer\TA.DigitalDomeworks.Installer.wixproj", "{62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution TA.DigitalDomeworks.Shared\TA.DigitalDomeworks.Shared.projitems*{020924b9-23d5-4b92-b5b1-461423bbe23c}*SharedItemsImports = 4 + TA.DigitalDomeworks.Shared\TA.DigitalDomeworks.Shared.projitems*{3689a2cb-94c5-4012-a5cf-7e7d1dd27143}*SharedItemsImports = 4 TA.DigitalDomeworks.Shared\TA.DigitalDomeworks.Shared.projitems*{7397ed53-0fac-4466-bd0f-d37d71208d00}*SharedItemsImports = 13 TA.DigitalDomeworks.Shared\TA.DigitalDomeworks.Shared.projitems*{86b17c99-41b6-4611-ad1d-26b7d6c70a22}*SharedItemsImports = 4 TA.DigitalDomeworks.Shared\TA.DigitalDomeworks.Shared.projitems*{adbb1165-e995-4c75-8de9-1dbffcf34d6f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {91D73239-42FD-4616-A8B0-687289AA3AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91D73239-42FD-4616-A8B0-687289AA3AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91D73239-42FD-4616-A8B0-687289AA3AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91D73239-42FD-4616-A8B0-687289AA3AC5}.Release|Any CPU.Build.0 = Release|Any CPU - {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Debug|Any CPU.Build.0 = Debug|Any CPU - {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Release|Any CPU.ActiveCfg = Release|Any CPU - {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Release|Any CPU.Build.0 = Release|Any CPU - {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Release|Any CPU.Build.0 = Release|Any CPU - {020924B9-23D5-4B92-B5B1-461423BBE23C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {020924B9-23D5-4B92-B5B1-461423BBE23C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {020924B9-23D5-4B92-B5B1-461423BBE23C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {020924B9-23D5-4B92-B5B1-461423BBE23C}.Release|Any CPU.Build.0 = Release|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Debug|x64.Build.0 = Debug|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Debug|x86.Build.0 = Debug|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Release|x64.ActiveCfg = Release|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Release|x64.Build.0 = Release|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Release|x86.ActiveCfg = Release|Any CPU + {91D73239-42FD-4616-A8B0-687289AA3AC5}.Release|x86.Build.0 = Release|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Debug|x64.ActiveCfg = Debug|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Debug|x64.Build.0 = Debug|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Debug|x86.ActiveCfg = Debug|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Debug|x86.Build.0 = Debug|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Release|x64.ActiveCfg = Release|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Release|x64.Build.0 = Release|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Release|x86.ActiveCfg = Release|Any CPU + {86B17C99-41B6-4611-AD1D-26B7D6C70A22}.Release|x86.Build.0 = Release|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Debug|x64.ActiveCfg = Debug|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Debug|x64.Build.0 = Debug|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Debug|x86.ActiveCfg = Debug|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Debug|x86.Build.0 = Debug|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Release|x64.ActiveCfg = Release|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Release|x64.Build.0 = Release|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Release|x86.ActiveCfg = Release|Any CPU + {ADBB1165-E995-4C75-8DE9-1DBFFCF34D6F}.Release|x86.Build.0 = Release|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Debug|x64.ActiveCfg = Debug|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Debug|x64.Build.0 = Debug|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Debug|x86.ActiveCfg = Debug|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Debug|x86.Build.0 = Debug|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Release|x64.ActiveCfg = Release|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Release|x64.Build.0 = Release|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Release|x86.ActiveCfg = Release|Any CPU + {020924B9-23D5-4B92-B5B1-461423BBE23C}.Release|x86.Build.0 = Release|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Debug|x64.Build.0 = Debug|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Debug|x86.Build.0 = Debug|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Release|x64.ActiveCfg = Release|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Release|x64.Build.0 = Release|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Release|x86.ActiveCfg = Release|Any CPU + {0B22461B-2D19-4342-A87D-4E90EFD3E2A9}.Release|x86.Build.0 = Release|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Debug|x64.ActiveCfg = Debug|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Debug|x64.Build.0 = Debug|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Debug|x86.ActiveCfg = Debug|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Debug|x86.Build.0 = Debug|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Release|x64.ActiveCfg = Release|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Release|x64.Build.0 = Release|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Release|x86.ActiveCfg = Release|Any CPU + {3689A2CB-94C5-4012-A5CF-7E7D1DD27143}.Release|x86.Build.0 = Release|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Debug|x64.ActiveCfg = Debug|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Debug|x64.Build.0 = Debug|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Debug|x86.ActiveCfg = Debug|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Debug|x86.Build.0 = Debug|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Release|x64.ActiveCfg = Release|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Release|x64.Build.0 = Release|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Release|x86.ActiveCfg = Release|Any CPU + {9CDCF319-DADC-41EB-B787-DE3862017E95}.Release|x86.Build.0 = Release|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Debug|x64.ActiveCfg = Debug|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Debug|x64.Build.0 = Debug|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Debug|x86.ActiveCfg = Debug|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Debug|x86.Build.0 = Debug|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Release|x64.ActiveCfg = Release|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Release|x64.Build.0 = Release|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Release|x86.ActiveCfg = Release|Any CPU + {F95208C4-450F-4B51-810A-C805A8EFBB7A}.Release|x86.Build.0 = Release|Any CPU + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Debug|x64.ActiveCfg = Debug|x64 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Debug|x64.Build.0 = Debug|x64 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Debug|x86.ActiveCfg = Debug|x86 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Debug|x86.Build.0 = Debug|x86 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Release|x64.ActiveCfg = Release|x64 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Release|x64.Build.0 = Release|x64 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Release|x86.ActiveCfg = Release|x86 + {62BC6E28-4239-47F9-B0BC-69CCDCEA6F75}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TA.DigitalDomeworks.sln.DotSettings b/TA.DigitalDomeworks.sln.DotSettings index 5130604..d61a9d5 100644 --- a/TA.DigitalDomeworks.sln.DotSettings +++ b/TA.DigitalDomeworks.sln.DotSettings @@ -1,7 +1,14 @@  + 0b22461b-2d19-4342-a87d-4e90efd3e2a9;f95208c4-450f-4b51-810a-c805a8efbb7a + 4 C:\Users\Tim\source\repos\TA.DigitalDomeworks\TA.DigitalDomeworks.Specifications\TA.DigitalDomeworks.Specifications.csproj.DotSettings ..\TA.DigitalDomeworks.Specifications\TA.DigitalDomeworks.Specifications.csproj.DotSettings True + True + C:\Users\Tim\OneDrive\Software Engineering\ReSharper\Tigra Type Member Layout.DotSettings + ..\..\..\..\OneDrive\Software Engineering\ReSharper\Tigra Type Member Layout.DotSettings True False - 1 \ No newline at end of file + 1 + True + 2 \ No newline at end of file diff --git a/TA.DigitalDomeworks.v3.ncrunchsolution b/TA.DigitalDomeworks.v3.ncrunchsolution index 03e2406..79eb427 100644 --- a/TA.DigitalDomeworks.v3.ncrunchsolution +++ b/TA.DigitalDomeworks.v3.ncrunchsolution @@ -1,6 +1,6 @@  - False + True True x86 diff --git a/TA.PostSharp.Aspects/IAscomDriver.cs b/TA.PostSharp.Aspects/IAscomDriver.cs new file mode 100644 index 0000000..5c78a9b --- /dev/null +++ b/TA.PostSharp.Aspects/IAscomDriver.cs @@ -0,0 +1,13 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: IAscomDriver.cs Last modified: 2018-03-29@02:55 by Tim Long + +namespace TA.PostSharp.Aspects + { + public interface IAscomDriver + { + bool Connected { get; } + } + } \ No newline at end of file diff --git a/TA.PostSharp.Aspects/MustBeConnectedAttribute.cs b/TA.PostSharp.Aspects/MustBeConnectedAttribute.cs new file mode 100644 index 0000000..540c3a5 --- /dev/null +++ b/TA.PostSharp.Aspects/MustBeConnectedAttribute.cs @@ -0,0 +1,84 @@ +// This file is part of the TA.DigitalDomeworks project +// +// Copyright © 2016-2018 Tigra Astronomy, all rights reserved. +// +// File: MustBeConnectedAttribute.cs Last modified: 2018-03-29@02:55 by Tim Long + +using System; +using System.Reflection; +using ASCOM; +using PostSharp.Aspects; +using PostSharp.Aspects.Dependencies; +using PostSharp.Extensibility; + +namespace TA.PostSharp.Aspects + { + /// + /// MustBeConnected aspect. Verifies that the controlled device is connected and if not, + /// throws a + /// . + /// + [Serializable] + [ProvideAspectRole("ASCOM")] + public sealed class MustBeConnectedAttribute : OnMethodBoundaryAspect + { + private static int nesting; + + /// + /// Initializes a new instance of the class. + /// + public MustBeConnectedAttribute() + { + SemanticallyAdvisedMethodKinds = SemanticallyAdvisedMethodKinds.None; + } + + public override bool CompileTimeValidate(MethodBase method) + { + var targetType = method.DeclaringType; + if (!typeof(IAscomDriver).IsAssignableFrom(targetType)) + throw new InvalidAnnotationException( + "This aspect can only be applied to members of types that implement IAscomDriver"); + return base.CompileTimeValidate(method); + } + + /// + /// Verifies that the device is connected. Throws + /// if not. + /// + /// + /// Event arguments specifying which method is being executed, which are its arguments, + /// and how should the execution continue after the execution of + /// + /// + /// + /// Thrown if the device is not connected. + /// + public override void OnEntry(MethodExecutionArgs args) + { + base.OnEntry(args); + var instance = args.Instance as IAscomDriver; + if (nesting++ > 0) return; // Optimization - no need to check in nested calls. + if (!instance.Connected) + { + var name = args.Method.Name; + var message = $"{name} requires that Connected is true but it was false"; + throw new NotConnectedException(message); + } + } + + /// + /// Method executed after the body of methods to which this aspect is applied, + /// even when the method exists with an exception (this method is invoked from + /// the finally block). + /// + /// + /// Event arguments specifying which method + /// is being executed and which are its arguments. + /// + public override void OnExit(MethodExecutionArgs args) + { + base.OnExit(args); + nesting--; + } + } + } \ No newline at end of file diff --git a/TA.PostSharp.Aspects/NLogTraceWithArgumentsAttribute.cs b/TA.PostSharp.Aspects/NLogTraceWithArgumentsAttribute.cs new file mode 100644 index 0000000..592941a --- /dev/null +++ b/TA.PostSharp.Aspects/NLogTraceWithArgumentsAttribute.cs @@ -0,0 +1,162 @@ +// This file is part of the TI.DigitalDomeWorks project +// +// Copyright © 2015-2016 Tigra Networks., all rights reserved. +// +// File: NLogTraceWithArgumentsAttribute.cs Last modified: 2016-09-13@00:23 by Tim Long + +using System; +using System.Reflection; +using System.Text; +using System.Threading; +using NLog; +using PostSharp.Aspects; +using PostSharp.Aspects.Dependencies; + +namespace TA.PostSharp.Aspects + { + /// + /// Class NLogTraceWithArgumentsAttribute. This class cannot be inherited. Traces member entry and exit, with + /// argument values and return values. + /// + [Serializable] + [AttributeUsage( + AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Struct)] + [ProvideAspectRole("Trace")] + [AspectRoleDependency(AspectDependencyAction.Order, AspectDependencyPosition.Before, "ASCOM")] + public sealed class NLogTraceWithArgumentsAttribute : OnMethodBoundaryAspect + { + private static readonly Type MyType = typeof(NLogTraceWithArgumentsAttribute); + [NonSerialized] private static int indent; + private readonly int logAtLevelOrdinal; + [NonSerialized] private string enteringMessage; + [NonSerialized] private string exitingMessage; + [NonSerialized] private Logger log; + [NonSerialized] private string loggerName; + + /// + /// Initializes a new instance of the class. + /// + /// The log at level. + public NLogTraceWithArgumentsAttribute(int logAtLevel = 0) + { + logAtLevelOrdinal = logAtLevel; + } + + private LogLevel LogAtLevel + { + get + { + if (logAtLevelOrdinal == LogLevel.Trace.Ordinal) + return LogLevel.Trace; + if (logAtLevelOrdinal == LogLevel.Debug.Ordinal) + return LogLevel.Debug; + if (logAtLevelOrdinal == LogLevel.Info.Ordinal) + return LogLevel.Info; + if (logAtLevelOrdinal == LogLevel.Warn.Ordinal) + return LogLevel.Warn; + if (logAtLevelOrdinal == LogLevel.Error.Ordinal) + return LogLevel.Error; + if (logAtLevelOrdinal == LogLevel.Fatal.Ordinal) + return LogLevel.Fatal; + if (logAtLevelOrdinal == LogLevel.Off.Ordinal) + return LogLevel.Off; + throw new ArgumentException("Invalid trace level specified"); + } + } + + /// + /// Initializes the current aspect. Invoked only once at runtime from the static constructor of type declaring + /// the target method. + /// + /// Method to which the current aspect is applied. + public override void RuntimeInitialize(MethodBase method) + { + if (method.DeclaringType != null) loggerName = method.DeclaringType.FullName; + var methodName = method.Name; + enteringMessage = "Enter " + methodName + '('; + exitingMessage = "Exit " + methodName + "()"; + log = LogManager.GetLogger(loggerName); + } + + /// + /// Method executed before the body of methods to which this aspect is applied. + /// + /// + /// Event arguments specifying which method + /// is being executed, which are its arguments, and how should the execution continue + /// after the execution of + /// . + /// + public override void OnEntry(MethodExecutionArgs args) + { + base.OnEntry(args); + var useIndent = Interlocked.Increment(ref indent); + LogMethodEntryWithParameters(args, useIndent); + } + + private void LogMethodEntryWithParameters(MethodExecutionArgs args, int indent = 0) + { + var builder = new StringBuilder(); + if (indent < 0) indent = 0; + builder.Append(' ', indent); + builder.Append(enteringMessage); + foreach (var argument in args.Arguments) + { + if (argument == null) + builder.Append("null"); + else + { + builder.Append(argument.GetType().Name); + builder.Append('='); + builder.Append(argument); + } + builder.Append(", "); + } + if (args.Arguments.Count > 0) + builder.Length -= 2; // Remove the last comma + builder.Append(')'); + LogWithUnwoundStack(builder.ToString()); + } + + /// + /// Method executed after the body of methods to which this aspect is applied, + /// even when the method exists with an exception (this method is invoked from + /// the finally block). + /// + /// + /// Event arguments specifying which method + /// is being executed and which are its arguments. + /// + public override void OnExit(MethodExecutionArgs args) + { + base.OnExit(args); + LogMethodExit(args, indent); + Interlocked.Decrement(ref indent); + } + + /// + /// Sends output to NLog while correctly preserving the call site of the original logging event. + /// + /// The verbatim message to be logged. + private void LogWithUnwoundStack(string message) + { + var logEvent = new LogEventInfo(LogAtLevel, loggerName, message); + log.Log(MyType, logEvent); + } + + private void LogMethodExit(MethodExecutionArgs args, int indent = 0) + { + var builder = new StringBuilder(); + if (indent < 0) indent = 0; + builder.Append(' ', indent); + builder.Append(exitingMessage); + if (args.ReturnValue != null) + { + builder.Append(" == "); + builder.Append(args.ReturnValue); + } + LogWithUnwoundStack(builder.ToString()); + } + } + } \ No newline at end of file diff --git a/TA.PostSharp.Aspects/Properties/AssemblyInfo.cs b/TA.PostSharp.Aspects/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..83b19ff --- /dev/null +++ b/TA.PostSharp.Aspects/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("TA.PostSharp.Aspects")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TA.PostSharp.Aspects")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[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("9cdcf319-dadc-41eb-b787-de3862017e95")] + +// 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/TA.PostSharp.Aspects/TA.PostSharp.Aspects.csproj b/TA.PostSharp.Aspects/TA.PostSharp.Aspects.csproj new file mode 100644 index 0000000..ddc36ec --- /dev/null +++ b/TA.PostSharp.Aspects/TA.PostSharp.Aspects.csproj @@ -0,0 +1,122 @@ + + + + + + Debug + AnyCPU + {9CDCF319-DADC-41EB-B787-DE3862017E95} + Library + Properties + TA.PostSharp.Aspects + TA.PostSharp.Aspects + v4.6.2 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Astrometry.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Attributes.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Controls.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DeviceInterfaces.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.DriverAccess.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Exceptions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Internal.Extensions.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.SettingsProvider.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.dll + True + + + ..\packages\ASCOM.Platform.6.3.2\lib\net45\ASCOM.Utilities.Video.dll + True + + + ..\packages\NLog.4.5.0\lib\net45\NLog.dll + + + ..\packages\PostSharp.Redist.5.0.48\lib\net45\PostSharp.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/TA.PostSharp.Aspects/TA.PostSharp.Aspects.v3.ncrunchproject b/TA.PostSharp.Aspects/TA.PostSharp.Aspects.v3.ncrunchproject new file mode 100644 index 0000000..6800b4a --- /dev/null +++ b/TA.PostSharp.Aspects/TA.PostSharp.Aspects.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/TA.PostSharp.Aspects/packages.config b/TA.PostSharp.Aspects/packages.config new file mode 100644 index 0000000..a622d69 --- /dev/null +++ b/TA.PostSharp.Aspects/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file