Files
MeadeGeneric/Meade.net/LocalServer.cs
ColinD 58ba1e6ee3 Merged in develop (pull request #6)
Release 0.5.0.0

* Removed unwanted files

* Adding git ignore file

* Started working on getting the basic communications with the scope working.
    Working on routines to get and set the date and time in the handbox.

* Switched the serial port over to using the .net frameworks serial port.
    Extracted the serial port into it's own class and created a simpler command processing mechanism.

* Implemented AbortSlew and did some code tidy up.

* Forced all code to 64-bit only, will make this 32/64 bit before release.
    Fixed issue in the SerialProcessor when setting the time, makes sure that there's no chance of thread stealing when pulling out the junk messages
    Added ConformanceResult.txt to show the progress of the driver development

* Added code for the site latitude
    and started work on the longitude.

* Added unit tests for reading and writing the utcDate.
    Fixed a couple of defects in the code that was setting the utcDate.

* Corrected Longitude value range

* Added support for UTC offset.

* Pulse guiding support added

* Added SiteLatitude unit tests

* Added unit tests for SiteLongitude

* Added unit tests for the new Pulse guide implementation.

* Added support for AlignmentMode

* Added support for AtPark and Park

* Added support for parking the scope
    Added support for reading the scope Azimuth

* Added support for reading Declination

* Added 5 second timeout for the serial port.
    Fixed problem with the GW command not working on the Autostar 497.

* Fixed broken unit test

* Added support for altitude

* Tidying up resharper warinings

* Implemented RightAscension
    TargetRightAscension
    TargetDec
    SlewToCoord
    and SlewToCoordAsync

* Sorted out the target RA and Dec exceptions to be compliant with ascom.

* Implemented SlewToAltAz and SlewToAltAzAsync

* Implemented SyncToTarget functionality

* Implemented slew to target

* Added support for MoveAxis

* Added support for tracking rate

* Added IFocuserV3 to the driver and made sure that it's registered for ASCOM as a focuser as well.

* Fixed issue with Target RA and Dec loosing precision

* Telescope driver now passes the Ascom conformance.

* Upgraded driver version to 0.1

* Added explicit locks around all sequences of commands.

* Basic implementation of the IFocusserV3

* Focuser passes validation, this makes the code V0.2

* Added support for CommandBlind and CommandString
    Modified the tracking rates to be setable.  However, the get is now simulated.

* Merged in feature/LocalServer (pull request #5)

    Feature/LocalServer

    * Major refactor.  Switching over to a local server hub style driver allowing multiple programs to control the telescope at one time without the need for the POTH Hub

    * Unified the setup dialog

    * Implemented shared serial port, Both Telescope and Driver can connect at the same time.

    * Ported the focuser implementation from the non server based version.

    * Ported the telescope driver code.

    * Fixed problem with # not being stripped from the returned string ends. Fixed issue with RA being returned as degress rather than hours.

    * Telescope passes validation

    * Added a lock around the focuser move.

    * Reimplemented CommandBlind and CommandString

    * Corrected version information

    * Removed the Altitude support as there's a bug in the Autostar and Audiostar firmware

* Added comments for all meade commands.
    Fixed the Site Lat and Long setters

* Re instated the Altitude value and ran conformance for both the telescope and focuser.

* Redesigned the Altitude and Azimuth readings to use the Right Ascension and Declination co-ordinates and perform the transformation using the date and site details from the scope.   This will correct the problem of the Altitude reading from the handset being incorrect.

* Added code to make sure that the scope returns values in high precision mode.

* Fixed problem where SlewToAltAz didn't work correctly.  Now uses the RA/Dec slew for everything, and converts the values as needed.

* Upgraded the error handling to ensure that all serial commands are executed after checking that there is a connection open.

* Added some code to the focuser connect to make it consistent with the telescope connect in that it will now test that the connection is to an Autostar.

    Upgraded the move code to make it less unreliable.

* Added localisation support

* First draft of the installer

* Sorted out the registry settings needed to get the driver working properly on install.

* Modified the solution to be able to create as 32-bit install that works on 64bit windows 10.

* Added a call to activate when the setup dialog is shown.
2019-06-04 19:06:57 +00:00

643 lines
25 KiB
C#

//
// ASCOM.Meade.net Local COM Server
//
// This is the core of a managed COM Local Server, capable of serving
// multiple instances of multiple interfaces, within a single
// executable. This implementes the equivalent functionality of VB6
// which has been extensively used in ASCOM for drivers that provide
// multiple interfaces to multiple clients (e.g. Meade Telescope
// and Focuser) as well as hubs (e.g., POTH).
//
// Written by: Robert B. Denny (Version 1.0.1, 29-May-2007)
// Modified by Chris Rowland and Peter Simpson to allow use with multiple devices of the same type March 2011
//
//
using System;
using System.IO;
using System.Windows.Forms;
using System.Drawing;
using System.Collections;
using System.Runtime.InteropServices;
using System.Reflection;
using ASCOM.Utilities;
using Microsoft.Win32;
using System.Text;
using System.Threading;
using System.Security.Principal;
using System.Diagnostics;
using ASCOM;
namespace ASCOM.Meade.net
{
public static class Server
{
#region Access to kernel32.dll, user32.dll, and ole32.dll functions
[Flags]
enum CLSCTX : uint
{
CLSCTX_INPROC_SERVER = 0x1,
CLSCTX_INPROC_HANDLER = 0x2,
CLSCTX_LOCAL_SERVER = 0x4,
CLSCTX_INPROC_SERVER16 = 0x8,
CLSCTX_REMOTE_SERVER = 0x10,
CLSCTX_INPROC_HANDLER16 = 0x20,
CLSCTX_RESERVED1 = 0x40,
CLSCTX_RESERVED2 = 0x80,
CLSCTX_RESERVED3 = 0x100,
CLSCTX_RESERVED4 = 0x200,
CLSCTX_NO_CODE_DOWNLOAD = 0x400,
CLSCTX_RESERVED5 = 0x800,
CLSCTX_NO_CUSTOM_MARSHAL = 0x1000,
CLSCTX_ENABLE_CODE_DOWNLOAD = 0x2000,
CLSCTX_NO_FAILURE_LOG = 0x4000,
CLSCTX_DISABLE_AAA = 0x8000,
CLSCTX_ENABLE_AAA = 0x10000,
CLSCTX_FROM_DEFAULT_CONTEXT = 0x20000,
CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER,
CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER,
CLSCTX_ALL = CLSCTX_SERVER | CLSCTX_INPROC_HANDLER
}
[Flags]
enum COINIT : uint
{
/// Initializes the thread for multi-threaded object concurrency.
COINIT_MULTITHREADED = 0x0,
/// Initializes the thread for apartment-threaded object concurrency.
COINIT_APARTMENTTHREADED = 0x2,
/// Disables DDE for Ole1 support.
COINIT_DISABLE_OLE1DDE = 0x4,
/// Trades memory for speed.
COINIT_SPEED_OVER_MEMORY = 0x8
}
[Flags]
enum REGCLS : uint
{
REGCLS_SINGLEUSE = 0,
REGCLS_MULTIPLEUSE = 1,
REGCLS_MULTI_SEPARATE = 2,
REGCLS_SUSPENDED = 4,
REGCLS_SURROGATE = 8
}
// CoInitializeEx() can be used to set the apartment model
// of individual threads.
[DllImport("ole32.dll")]
static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
// CoUninitialize() is used to uninitialize a COM thread.
[DllImport("ole32.dll")]
static extern void CoUninitialize();
// PostThreadMessage() allows us to post a Windows Message to
// a specific thread (identified by its thread id).
// We will need this API to post a WM_QUIT message to the main
// thread in order to terminate this application.
[DllImport("user32.dll")]
static extern bool PostThreadMessage(uint idThread, uint Msg, UIntPtr wParam,
IntPtr lParam);
// GetCurrentThreadId() allows us to obtain the thread id of the
// calling thread. This allows us to post the WM_QUIT message to
// the main thread.
[DllImport("kernel32.dll")]
static extern uint GetCurrentThreadId();
#endregion
#region Private Data
private static int objsInUse; // Keeps a count on the total number of objects alive.
private static int serverLocks; // Keeps a lock count on this application.
private static frmMain s_MainForm = null; // Reference to our main form
private static ArrayList s_ComObjectAssys; // Dynamically loaded assemblies containing served COM objects
private static ArrayList s_ComObjectTypes; // Served COM object types
private static ArrayList s_ClassFactories; // Served COM object class factories
private static string s_appId = "{4e68ec46-5ffc-49e7-b298-38a548df0bfd}"; // Our AppId
private static readonly Object lockObject = new object();
#endregion
// This property returns the main thread's id.
public static uint MainThreadId { get; private set; } // Stores the main thread's thread id.
// Used to tell if started by COM or manually
public static bool StartedByCOM { get; private set; } // True if server started by COM (-embedding)
#region Server Lock, Object Counting, and AutoQuit on COM startup
// Returns the total number of objects alive currently.
public static int ObjectsCount
{
get
{
lock (lockObject)
{
return objsInUse;
}
}
}
// This method performs a thread-safe incrementation of the objects count.
public static int CountObject()
{
// Increment the global count of objects.
return Interlocked.Increment(ref objsInUse);
}
// This method performs a thread-safe decrementation the objects count.
public static int UncountObject()
{
// Decrement the global count of objects.
return Interlocked.Decrement(ref objsInUse);
}
// Returns the current server lock count.
public static int ServerLockCount
{
get
{
lock (lockObject)
{
return serverLocks;
}
}
}
// This method performs a thread-safe incrementation the
// server lock count.
public static int CountLock()
{
// Increment the global lock count of this server.
return Interlocked.Increment(ref serverLocks);
}
// This method performs a thread-safe decrementation the
// server lock count.
public static int UncountLock()
{
// Decrement the global lock count of this server.
return Interlocked.Decrement(ref serverLocks);
}
// AttemptToTerminateServer() will check to see if the objects count and the server
// lock count have both dropped to zero.
//
// If so, and if we were started by COM, we post a WM_QUIT message to the main thread's
// message loop. This will cause the message loop to exit and hence the termination
// of this application. If hand-started, then just trace that it WOULD exit now.
//
public static void ExitIf()
{
lock (lockObject)
{
if ((ObjectsCount <= 0) && (ServerLockCount <= 0))
{
if (StartedByCOM)
{
UIntPtr wParam = new UIntPtr(0);
IntPtr lParam = new IntPtr(0);
PostThreadMessage(MainThreadId, 0x0012, wParam, lParam);
}
}
}
}
#endregion
// -----------------
// PRIVATE FUNCTIONS
// -----------------
#region Dynamic Driver Assembly Loader
//
// Load the assemblies that contain the classes that we will serve
// via COM. These will be located in the same folder as
// our executable.
//
private static bool LoadComObjectAssemblies()
{
s_ComObjectAssys = new ArrayList();
s_ComObjectTypes = new ArrayList();
// put everything into one folder, the same as the server.
string assyPath = Assembly.GetEntryAssembly().Location;
assyPath = Path.GetDirectoryName(assyPath);
DirectoryInfo d = new DirectoryInfo(assyPath);
foreach (FileInfo fi in d.GetFiles("*.dll"))
{
string aPath = fi.FullName;
//
// First try to load the assembly and get the types for
// the class and the class factory. If this doesn't work ????
//
try
{
Assembly so = Assembly.LoadFrom(aPath);
//PWGS Get the types in the assembly
Type[] types = so.GetTypes();
foreach (Type type in types)
{
// PWGS Now checks the type rather than the assembly
// Check to see if the type has the ServedClassName attribute, only use it if it does.
MemberInfo info = type;
object[] attrbutes = info.GetCustomAttributes(typeof(ServedClassNameAttribute), false);
if (attrbutes.Length > 0)
{
//MessageBox.Show("Adding Type: " + type.Name + " " + type.FullName);
s_ComObjectTypes.Add(type); //PWGS - much simpler
s_ComObjectAssys.Add(so);
}
}
}
catch (BadImageFormatException)
{
// Probably an attempt to load a Win32 DLL (i.e. not a .net assembly)
// Just swallow the exception and continue to the next item.
continue;
}
catch (Exception e)
{
MessageBox.Show("Failed to load served COM class assembly " + fi.Name + " - " + e.Message,
"Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Stop);
return false;
}
}
return true;
}
#endregion
#region COM Registration and Unregistration
//
// Test if running elevated
//
private static bool IsAdministrator
{
get
{
WindowsIdentity i = WindowsIdentity.GetCurrent();
WindowsPrincipal p = new WindowsPrincipal(i);
return p.IsInRole(WindowsBuiltInRole.Administrator);
}
}
//
// Elevate by re-running ourselves with elevation dialog
//
private static void ElevateSelf(string arg)
{
ProcessStartInfo si = new ProcessStartInfo();
si.Arguments = arg;
si.WorkingDirectory = Environment.CurrentDirectory;
si.FileName = Application.ExecutablePath;
si.Verb = "runas";
try { Process.Start(si); }
catch (System.ComponentModel.Win32Exception)
{
MessageBox.Show("The Meade.net was not " + (arg == "/register" ? "registered" : "unregistered") +
" because you did not allow it.", "Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString(), "Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Stop);
}
return;
}
//
// Do everything to register this for COM. Never use REGASM on
// this exe assembly! It would create InProcServer32 entries
// which would prevent proper activation!
//
// Using the list of COM object types generated during dynamic
// assembly loading, it registers each one for COM as served by our
// exe/local server, as well as registering it for ASCOM. It also
// adds DCOM info for the local server itself, so it can be activated
// via an outboiud connection from TheSky.
//
private static void RegisterObjects()
{
if (!IsAdministrator)
{
ElevateSelf("/register");
return;
}
//
// If reached here, we're running elevated
//
Assembly assy = Assembly.GetExecutingAssembly();
Attribute attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyTitleAttribute));
string assyTitle = ((AssemblyTitleAttribute)attr).Title;
attr = Attribute.GetCustomAttribute(assy, typeof(AssemblyDescriptionAttribute));
string assyDescription = ((AssemblyDescriptionAttribute)attr).Description;
//
// Local server's DCOM/AppID information
//
try
{
//
// HKCR\APPID\appid
//
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey("APPID\\" + s_appId))
{
key.SetValue(null, assyDescription);
key.SetValue("AppID", s_appId);
key.SetValue("AuthenticationLevel", 1, RegistryValueKind.DWord);
}
//
// HKCR\APPID\exename.ext
//
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(string.Format("APPID\\{0}",
Application.ExecutablePath.Substring(Application.ExecutablePath.LastIndexOf('\\') + 1))))
{
key.SetValue("AppID", s_appId);
}
}
catch (Exception ex)
{
MessageBox.Show("Error while registering the server:\n" + ex.ToString(),
"Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Stop);
return;
}
finally
{
}
//
// For each of the driver assemblies
//
foreach (Type type in s_ComObjectTypes)
{
bool bFail = false;
try
{
//
// HKCR\CLSID\clsid
//
string clsid = Marshal.GenerateGuidForType(type).ToString("B");
string progid = Marshal.GenerateProgIdForType(type);
//PWGS Generate device type from the Class name
string deviceType = type.Name;
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(string.Format("CLSID\\{0}", clsid)))
{
key.SetValue(null, progid); // Could be assyTitle/Desc??, but .NET components show ProgId here
key.SetValue("AppId", s_appId);
using (RegistryKey key2 = key.CreateSubKey("Implemented Categories"))
{
key2.CreateSubKey("{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}");
}
using (RegistryKey key2 = key.CreateSubKey("ProgId"))
{
key2.SetValue(null, progid);
}
key.CreateSubKey("Programmable");
using (RegistryKey key2 = key.CreateSubKey("LocalServer32"))
{
key2.SetValue(null, Application.ExecutablePath);
}
}
//
// HKCR\CLSID\progid
//
using (RegistryKey key = Registry.ClassesRoot.CreateSubKey(progid))
{
key.SetValue(null, assyTitle);
using (RegistryKey key2 = key.CreateSubKey("CLSID"))
{
key2.SetValue(null, clsid);
}
}
//
// ASCOM
//
assy = type.Assembly;
// Pull the display name from the ServedClassName attribute.
attr = Attribute.GetCustomAttribute(type, typeof(ServedClassNameAttribute)); //PWGS Changed to search type for attribute rather than assembly
string chooserName = ((ServedClassNameAttribute)attr).DisplayName ?? "MultiServer";
using (var P = new ASCOM.Utilities.Profile())
{
P.DeviceType = deviceType;
P.Register(progid, chooserName);
}
}
catch (Exception ex)
{
MessageBox.Show("Error while registering the server:\n" + ex.ToString(),
"Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Stop);
bFail = true;
}
finally
{
}
if (bFail) break;
}
}
//
// Remove all traces of this from the registry.
//
// **TODO** If the above does AppID/DCOM stuff, this would have
// to remove that stuff too.
//
private static void UnregisterObjects()
{
if (!IsAdministrator)
{
ElevateSelf("/unregister");
return;
}
//
// Local server's DCOM/AppID information
//
Registry.ClassesRoot.DeleteSubKey(string.Format("APPID\\{0}", s_appId), false);
Registry.ClassesRoot.DeleteSubKey(string.Format("APPID\\{0}",
Application.ExecutablePath.Substring(Application.ExecutablePath.LastIndexOf('\\') + 1)), false);
//
// For each of the driver assemblies
//
foreach (Type type in s_ComObjectTypes)
{
string clsid = Marshal.GenerateGuidForType(type).ToString("B");
string progid = Marshal.GenerateProgIdForType(type);
string deviceType = type.Name;
//
// Best efforts
//
//
// HKCR\progid
//
Registry.ClassesRoot.DeleteSubKey(String.Format("{0}\\CLSID", progid), false);
Registry.ClassesRoot.DeleteSubKey(progid, false);
//
// HKCR\CLSID\clsid
//
Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Implemented Categories\\{{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}}", clsid), false);
Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Implemented Categories", clsid), false);
Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\ProgId", clsid), false);
Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\LocalServer32", clsid), false);
Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}\\Programmable", clsid), false);
Registry.ClassesRoot.DeleteSubKey(String.Format("CLSID\\{0}", clsid), false);
try
{
//
// ASCOM
//
using (var P = new ASCOM.Utilities.Profile())
{
P.DeviceType = deviceType;
P.Unregister(progid);
}
}
catch (Exception) { }
}
}
#endregion
#region Class Factory Support
//
// On startup, we register the class factories of the COM objects
// that we serve. This requires the class facgtory name to be
// equal to the served class name + "ClassFactory".
//
private static bool RegisterClassFactories()
{
s_ClassFactories = new ArrayList();
foreach (Type type in s_ComObjectTypes)
{
ClassFactory factory = new ClassFactory(type); // Use default context & flags
s_ClassFactories.Add(factory);
if (!factory.RegisterClassObject())
{
MessageBox.Show("Failed to register class factory for " + type.Name,
"Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Stop);
return false;
}
}
ClassFactory.ResumeClassObjects(); // Served objects now go live
return true;
}
private static void RevokeClassFactories()
{
ClassFactory.SuspendClassObjects(); // Prevent race conditions
foreach (ClassFactory factory in s_ClassFactories)
factory.RevokeClassObject();
}
#endregion
#region Command Line Arguments
//
// ProcessArguments() will process the command-line arguments
// If the return value is true, we carry on and start this application.
// If the return value is false, we terminate this application immediately.
//
private static bool ProcessArguments(string[] args)
{
bool bRet = true;
//
//**TODO** -Embedding is "ActiveX start". Prohibit non_AX starting?
//
if (args.Length > 0)
{
switch (args[0].ToLower())
{
case "-embedding":
StartedByCOM = true; // Indicate COM started us
break;
case "-register":
case @"/register":
case "-regserver": // Emulate VB6
case @"/regserver":
RegisterObjects(); // Register each served object
bRet = false;
break;
case "-unregister":
case @"/unregister":
case "-unregserver": // Emulate VB6
case @"/unregserver":
UnregisterObjects(); //Unregister each served object
bRet = false;
break;
default:
MessageBox.Show("Unknown argument: " + args[0] + "\nValid are : -register, -unregister and -embedding",
"Meade.net", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
break;
}
}
else
StartedByCOM = false;
return bRet;
}
#endregion
#region SERVER ENTRY POINT (main)
//
// ==================
// SERVER ENTRY POINT
// ==================
//
[STAThread]
static void Main(string[] args)
{
if (!LoadComObjectAssemblies()) return; // Load served COM class assemblies, get types
if (!ProcessArguments(args)) return; // Register/Unregister
// Initialize critical member variables.
objsInUse = 0;
serverLocks = 0;
MainThreadId = GetCurrentThreadId();
Thread.CurrentThread.Name = "Main Thread";
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
s_MainForm = new frmMain();
if (StartedByCOM) s_MainForm.WindowState = FormWindowState.Minimized;
// Register the class factories of the served objects
RegisterClassFactories();
// Start up the garbage collection thread.
GarbageCollection GarbageCollector = new GarbageCollection(1000);
Thread GCThread = new Thread(new ThreadStart(GarbageCollector.GCWatch));
GCThread.Name = "Garbage Collection Thread";
GCThread.Start();
//
// Start the message loop. This serializes incoming calls to our
// served COM objects, making this act like the VB6 equivalent!
//
try
{
Application.Run(s_MainForm);
}
finally
{
// Revoke the class factories immediately.
// Don't wait until the thread has stopped before
// we perform revocation!!!
RevokeClassFactories();
// Now stop the Garbage Collector thread.
GarbageCollector.StopThread();
GarbageCollector.WaitForThreadToStop();
}
}
#endregion
}
}