From 9b439f1a754cf90a9cffc92cac518816a4a26345 Mon Sep 17 00:00:00 2001 From: Julien Lecomte Date: Wed, 18 Sep 2024 15:40:44 -0700 Subject: [PATCH] Added serial port locking in focuser driver for multi-threaded applications + cleanup and minor fixes --- ASCOM_Driver/FilterWheelProxyDriver.cs | 28 ++--- .../FilterWheelSetupDialogForm.designer.cs | 2 +- ASCOM_Driver/FocuserDriver.cs | 105 +++++++++++------- .../FocuserSetupDialogForm.designer.cs | 2 +- ASCOM_Driver/Properties/AssemblyInfo.cs | 8 +- Focuser_App/MainForm.Designer.cs | 2 +- Installer/Inno Setup Script.iss | 2 +- README.md | 10 +- 8 files changed, 89 insertions(+), 70 deletions(-) diff --git a/ASCOM_Driver/FilterWheelProxyDriver.cs b/ASCOM_Driver/FilterWheelProxyDriver.cs index 17a64b9..cbce86f 100644 --- a/ASCOM_Driver/FilterWheelProxyDriver.cs +++ b/ASCOM_Driver/FilterWheelProxyDriver.cs @@ -43,7 +43,7 @@ public class FilterWheelProxy : IFilterWheelV2 /// /// Driver description that displays in the ASCOM Chooser. /// - private static readonly string deviceName = "DarkSkyGeek’s Filter Wheel Proxy For OAG Focuser"; + private static readonly string deviceName = "DarkSkyGeek's Filter Wheel Proxy For OAG Focuser"; // Constants used for Profile persistence internal static int MAX_FILTER_COUNT = 8; @@ -87,7 +87,7 @@ public FilterWheelProxy() { tl = new TraceLogger("", "DarkSkyGeek"); - tl.LogMessage("FilterWheel", "Starting initialization"); + LogMessage("FilterWheel", "Starting initialization"); ReadProfile(); @@ -105,7 +105,7 @@ public FilterWheelProxy() connectedState = false; - tl.LogMessage("FilterWheel", "Completed initialization"); + LogMessage("FilterWheel", "Completed initialization"); } // @@ -142,7 +142,7 @@ public ArrayList SupportedActions { get { - tl.LogMessage("SupportedActions Get", "Returning empty arraylist"); + LogMessage("SupportedActions Get", "Returning empty arraylist"); return new ArrayList(); } } @@ -187,7 +187,7 @@ public bool Connected } set { - tl.LogMessage("Connected", "Set {0}", value); + LogMessage("Connected", "Set {0}", value); if (value == IsConnected) return; @@ -236,7 +236,7 @@ public string Description { get { - tl.LogMessage("Description Get", deviceName); + LogMessage("Description Get", deviceName); return deviceName; } } @@ -247,7 +247,7 @@ public string DriverInfo { Version version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; string driverInfo = deviceName + " ASCOM Driver Version " + String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version.Major, version.Minor); - tl.LogMessage("DriverInfo Get", driverInfo); + LogMessage("DriverInfo Get", driverInfo); return driverInfo; } } @@ -258,7 +258,7 @@ public string DriverVersion { Version version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; string driverVersion = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version.Major, version.Minor); - tl.LogMessage("DriverVersion Get", driverVersion); + LogMessage("DriverVersion Get", driverVersion); return driverVersion; } } @@ -276,7 +276,7 @@ public string Name { get { - tl.LogMessage("Name Get", deviceName); + LogMessage("Name Get", deviceName); return deviceName; } } @@ -290,7 +290,7 @@ public int[] FocusOffsets get { var profile = GetSelectedProfile(); - tl.LogMessage("FocusOffsets Get", "[ " + String.Join(", ", profile.filterOffsets) + " ]"); + LogMessage("FocusOffsets Get", "[ " + String.Join(", ", profile.filterOffsets) + " ]"); return profile.filterOffsets.ToArray(); } } @@ -300,7 +300,7 @@ public string[] Names get { var profile = GetSelectedProfile(); - tl.LogMessage("Names Get", "[ " + String.Join(", ", profile.filterNames) + " ]"); + LogMessage("Names Get", "[ " + String.Join(", ", profile.filterNames) + " ]"); return profile.filterNames.ToArray(); } } @@ -326,7 +326,7 @@ public short Position } var profile = GetSelectedProfile(); - tl.LogMessage("FilterWheel", $"Using profile {profile.name}"); + LogMessage("FilterWheel", $"Using profile {profile.name}"); short oldPosition = filterWheel.Position; short newPosition = value; @@ -335,7 +335,7 @@ public short Position int oldFilterOffset = profile.filterOffsets[oldPosition]; int newFilterOffset = profile.filterOffsets[newPosition]; int delta = (int) ((newFilterOffset - oldFilterOffset) * profile.stepRatio); - tl.LogMessage("FilterWheel", $"oldFilterOffset = {oldFilterOffset}, newFilterOffset = {newFilterOffset}, delta = {delta}"); + LogMessage("FilterWheel", $"oldFilterOffset = {oldFilterOffset}, newFilterOffset = {newFilterOffset}, delta = {delta}"); if (delta > 0) { // If we're moving OUT, we overshoot to deal with backlash... @@ -481,7 +481,7 @@ internal void ReadProfile() } catch (Exception e) { - tl.LogMessage("FilterWheel", "ReadProfile: Exception handled: " + e.Message); + LogMessage("FilterWheel", "ReadProfile: Exception handled: " + e.Message); } } } diff --git a/ASCOM_Driver/FilterWheelSetupDialogForm.designer.cs b/ASCOM_Driver/FilterWheelSetupDialogForm.designer.cs index 235f3e1..e7f5fe9 100644 --- a/ASCOM_Driver/FilterWheelSetupDialogForm.designer.cs +++ b/ASCOM_Driver/FilterWheelSetupDialogForm.designer.cs @@ -411,7 +411,7 @@ private void InitializeComponent() this.Name = "FilterWheelSetupDialogForm"; this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "DarkSkyGeek’s Filter Wheel Proxy For OAG Focuser"; + this.Text = "DarkSkyGeek's Filter Wheel Proxy For OAG Focuser"; this.Load += new System.EventHandler(this.FilterWheelSetupDialogForm_Load); this.groupBox1.ResumeLayout(false); this.groupBox1.PerformLayout(); diff --git a/ASCOM_Driver/FocuserDriver.cs b/ASCOM_Driver/FocuserDriver.cs index 8e4bccd..4e0672e 100644 --- a/ASCOM_Driver/FocuserDriver.cs +++ b/ASCOM_Driver/FocuserDriver.cs @@ -41,7 +41,7 @@ public class Focuser : IFocuserV3 /// /// Driver description that displays in the ASCOM Chooser. /// - private static readonly string deviceName = "DarkSkyGeek’s OAG Focuser"; + private static readonly string deviceName = "DarkSkyGeek's OAG Focuser"; // Constants used for Profile persistence internal static string autoDetectComPortProfileName = "Auto-Detect COM Port"; @@ -83,6 +83,11 @@ public class Focuser : IFocuserV3 /// private bool connectedState; + /// + // Object used to synchronize the serial communication with the device in a multi-threaded environment. + /// + private readonly object lockObject = new object(); + /// // The object used to communicate with the device using serial port communication. /// @@ -126,9 +131,9 @@ public Focuser() { tl = new TraceLogger("", "DarkSkyGeek"); ReadProfile(); - tl.LogMessage("Focuser", "Starting initialization"); + LogMessage("Focuser", "Starting initialization"); connectedState = false; - tl.LogMessage("Focuser", "Completed initialization"); + LogMessage("Focuser", "Completed initialization"); } // @@ -164,7 +169,7 @@ public ArrayList SupportedActions { get { - tl.LogMessage("SupportedActions Get", "Returning [\"SetZeroPosition\"]"); + LogMessage("SupportedActions Get", "Returning [\"SetZeroPosition\"]"); return new ArrayList() { "SetZeroPosition" @@ -180,7 +185,7 @@ public string Action(string actionName, string actionParameters) string response = SendCommandToDevice("SetZeroPosition", COMMAND_FOCUSER_SETZEROPOSITION, RESULT_FOCUSER_SETZEROPOSITION); if (response != OK) { - tl.LogMessage("SetZeroPosition", "Device responded with an error"); + LogMessage("SetZeroPosition", "Device responded with an error"); throw new ASCOM.DriverException("Device responded with an error"); } return string.Empty; @@ -224,7 +229,7 @@ public bool Connected } set { - tl.LogMessage("Connected", "Set {0}", value); + LogMessage("Connected", "Set {0}", value); if (value == IsConnected) return; @@ -315,7 +320,7 @@ public string Description { get { - tl.LogMessage("Description Get", deviceName); + LogMessage("Description Get", deviceName); return deviceName; } } @@ -326,7 +331,7 @@ public string DriverInfo { Version version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; string driverInfo = deviceName + " ASCOM Driver Version " + String.Format(CultureInfo.InvariantCulture, "{0}.{1}", version.Major, version.Minor); - tl.LogMessage("DriverInfo Get", driverInfo); + LogMessage("DriverInfo Get", driverInfo); return driverInfo; } } @@ -337,7 +342,7 @@ public string DriverVersion { Version version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; string driverVersion = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", version.Major, version.Minor); - tl.LogMessage("DriverVersion Get", driverVersion); + LogMessage("DriverVersion Get", driverVersion); return driverVersion; } } @@ -355,7 +360,7 @@ public string Name { get { - tl.LogMessage("Name Get", deviceName); + LogMessage("Name Get", deviceName); return deviceName; } } @@ -369,7 +374,7 @@ public bool Absolute { get { - tl.LogMessage("Absolute Get", true.ToString()); + LogMessage("Absolute Get", true.ToString()); return true; } } @@ -389,7 +394,7 @@ public bool IsMoving string response = SendCommandToDevice("IsMoving", COMMAND_FOCUSER_ISMOVING, RESULT_FOCUSER_ISMOVING); if (response != TRUE && response != FALSE) { - tl.LogMessage("IsMoving", "Invalid response from device: " + response); + LogMessage("IsMoving", "Invalid response from device: " + response); throw new DriverException("Invalid response from device: " + response); } return (response == TRUE); @@ -401,12 +406,12 @@ public bool Link { get { - tl.LogMessage("Link Get", this.Connected.ToString()); + LogMessage("Link Get", this.Connected.ToString()); return this.Connected; } set { - tl.LogMessage("Link Set", value.ToString()); + LogMessage("Link Set", value.ToString()); this.Connected = value; } } @@ -416,7 +421,7 @@ public int MaxIncrement { get { - tl.LogMessage("MaxIncrement Get", maxPosition.ToString()); + LogMessage("MaxIncrement Get", maxPosition.ToString()); return maxPosition; } } @@ -426,7 +431,7 @@ public int MaxStep { get { - tl.LogMessage("MaxStep Get", maxPosition.ToString()); + LogMessage("MaxStep Get", maxPosition.ToString()); return maxPosition; } } @@ -444,7 +449,7 @@ public void Move(int Position) string response = SendCommandToDevice("Move", COMMAND_FOCUSER_MOVE + Position.ToString(), RESULT_FOCUSER_MOVE); if (response != OK) { - tl.LogMessage("Move", "Device responded with an error"); + LogMessage("Move", "Device responded with an error"); throw new DriverException("Device responded with an error"); } } @@ -461,7 +466,7 @@ public int Position } catch (FormatException) { - tl.LogMessage("Position", "Invalid position value received from device: " + response); + LogMessage("Position", "Invalid position value received from device: " + response); throw new DriverException("Invalid position value received from device: " + response); } if (reverseRotation) @@ -476,7 +481,7 @@ public double StepSize { get { - tl.LogMessage("StepSize Get", "Not implemented"); + LogMessage("StepSize Get", "Not implemented"); throw new PropertyNotImplementedException("StepSize", false); } } @@ -485,12 +490,12 @@ public bool TempComp { get { - tl.LogMessage("TempComp Get", false.ToString()); + LogMessage("TempComp Get", false.ToString()); return false; } set { - tl.LogMessage("TempComp Set", "Not implemented"); + LogMessage("TempComp Set", "Not implemented"); throw new PropertyNotImplementedException("TempComp", false); } } @@ -499,7 +504,7 @@ public bool TempCompAvailable { get { - tl.LogMessage("TempCompAvailable Get", false.ToString()); + LogMessage("TempCompAvailable Get", false.ToString()); return false; } } @@ -508,7 +513,7 @@ public double Temperature { get { - tl.LogMessage("Temperature Get", "Not implemented"); + LogMessage("Temperature Get", "Not implemented"); throw new PropertyNotImplementedException("Temperature", false); } } @@ -693,16 +698,21 @@ internal Serial ConnectToDevice(string comPortName) for (int retries = 3; retries >= 0; retries--) { string response = ""; - try - { - serial.Transmit(COMMAND_PING + SEPARATOR); - response = serial.ReceiveTerminated(SEPARATOR).Trim(); - } - catch (Exception) + + lock (lockObject) { - // PortInUse or Timeout exceptions may happen here! - // We ignore them. + try + { + serial.Transmit(COMMAND_PING + SEPARATOR); + response = serial.ReceiveTerminated(SEPARATOR).Trim(); + } + catch (Exception) + { + // PortInUse or Timeout exceptions may happen here! + // We ignore them. + } } + if (response == RESULT_PING + DEVICE_GUID) { // Restore default timeout value... @@ -726,25 +736,34 @@ internal Serial ConnectToDevice(string comPortName) internal string SendCommandToDevice(string identifier, string command, string resultPrefix) { CheckConnected(identifier); - tl.LogMessage(identifier, "Sending command " + command + " to device..."); - objSerial.Transmit(command + SEPARATOR); - tl.LogMessage(identifier, "Waiting for response from device..."); + string response; - try - { - response = objSerial.ReceiveTerminated(SEPARATOR).Trim(); - } - catch (Exception e) + + lock (lockObject) { - tl.LogMessage(identifier, "Exception: " + e.Message); - throw e; + LogMessage(identifier, "Sending command " + command + " to device..."); + objSerial.Transmit(command + SEPARATOR); + LogMessage(identifier, "Waiting for response from device..."); + + try + { + response = objSerial.ReceiveTerminated(SEPARATOR).Trim(); + } + catch (Exception e) + { + LogMessage(identifier, "Exception: " + e.Message); + throw e; + } } - tl.LogMessage(identifier, "Response from device: " + response); + + LogMessage(identifier, "Response from device: " + response); + if (!response.StartsWith(resultPrefix)) { - tl.LogMessage(identifier, "Invalid response from device: " + response); + LogMessage(identifier, "Invalid response from device: " + response); throw new DriverException("Invalid response from device: " + response); } + string arg = response.Substring(resultPrefix.Length); return arg; } diff --git a/ASCOM_Driver/FocuserSetupDialogForm.designer.cs b/ASCOM_Driver/FocuserSetupDialogForm.designer.cs index e93600e..cf0f5bf 100644 --- a/ASCOM_Driver/FocuserSetupDialogForm.designer.cs +++ b/ASCOM_Driver/FocuserSetupDialogForm.designer.cs @@ -188,7 +188,7 @@ private void InitializeComponent() this.Name = "FocuserSetupDialogForm"; this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; - this.Text = "DarkSkyGeek’s OAG Focuser"; + this.Text = "DarkSkyGeek's OAG Focuser"; this.Load += new System.EventHandler(this.FocuserSetupDialogForm_Load); ((System.ComponentModel.ISupportInitialize)(this.DSGLogo)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).EndInit(); diff --git a/ASCOM_Driver/Properties/AssemblyInfo.cs b/ASCOM_Driver/Properties/AssemblyInfo.cs index 34676e4..916609a 100644 --- a/ASCOM_Driver/Properties/AssemblyInfo.cs +++ b/ASCOM_Driver/Properties/AssemblyInfo.cs @@ -8,10 +8,10 @@ using System.Runtime.InteropServices; [assembly: AssemblyTitle("ASCOM.DarkSkyGeek.FilterWheel")] -[assembly: AssemblyDescription("DarkSkyGeek’s OAG Focuser")] +[assembly: AssemblyDescription("DarkSkyGeek's OAG Focuser")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("DarkSkyGeek’s OAG Focuser")] +[assembly: AssemblyProduct("DarkSkyGeek's OAG Focuser")] [assembly: AssemblyCopyright("Copyright © 2022 - Present, Julien Lecomte - All Rights Reserved")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -34,5 +34,5 @@ // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: // -[assembly: AssemblyVersion("1.3.1.0")] -[assembly: AssemblyFileVersion("1.3.1.0")] +[assembly: AssemblyVersion("1.3.2.0")] +[assembly: AssemblyFileVersion("1.3.2.0")] diff --git a/Focuser_App/MainForm.Designer.cs b/Focuser_App/MainForm.Designer.cs index 35cd063..929a169 100644 --- a/Focuser_App/MainForm.Designer.cs +++ b/Focuser_App/MainForm.Designer.cs @@ -274,7 +274,7 @@ private void InitializeComponent() this.Controls.Add(this.lblCurPos); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.Name = "MainForm"; - this.Text = "DarkSkyGeek’s OAG Focuser"; + this.Text = "DarkSkyGeek's OAG Focuser"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); ((System.ComponentModel.ISupportInitialize)(this.picIsMoving)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.errorProvider)).EndInit(); diff --git a/Installer/Inno Setup Script.iss b/Installer/Inno Setup Script.iss index 978e2f2..c81d4bf 100644 --- a/Installer/Inno Setup Script.iss +++ b/Installer/Inno Setup Script.iss @@ -1,6 +1,6 @@ #define MyAppPublisher "Dark Sky Geek" #define MyAppName "OAG Focuser ASCOM Driver" -#define MyAppVersion "1.3.1" +#define MyAppVersion "1.3.2" #define MyAppURL "https://github.com/jlecomte/ascom-oag-focuser" [Setup] diff --git a/README.md b/README.md index 477c763..5ef201d 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Open the `Focuser_App\ASCOM.DarkSkyGeek.FocuserApp.sln` solution in Microsoft Vi ![Standalone Focuser Control Application](images/Standalone-Focuser-App.png) -This application allows you to connect to and control DarkSkyGeek’s OAG focuser, and in particular, it enables you to test various backlash compensation values as well as set the zero position. If you use a SCOPS OAG, I can only assume that it came with its own standalone application with similar functionality... +This application allows you to connect to and control DarkSkyGeek's OAG focuser, and in particular, it enables you to test various backlash compensation values as well as set the zero position. If you use a SCOPS OAG, I can only assume that it came with its own standalone application with similar functionality... ## Arduino Firmware @@ -201,7 +201,7 @@ Before you can use this device, you have to calibrate it. Here is the procedure: 1. **Filter Offsets Measurement** - Measuring filter offsets is something you have probably already done because it allows you to run an autofocus routine using your luminance (L) filter, which can be done with very short exposures, thereby saving a lot of time (it is also more accurate). Enter the filter offsets in your imaging application, e.g., N.I.N.A., as well as in the settings dialog of the `DarkSkyGeek’s Filter Wheel Proxy For OAG Focuser` ASCOM device (see screenshot above). Remember that it is critical that your imaging application and the driver have the same filter offset values! + Measuring filter offsets is something you have probably already done because it allows you to run an autofocus routine using your luminance (L) filter, which can be done with very short exposures, thereby saving a lot of time (it is also more accurate). Enter the filter offsets in your imaging application, e.g., N.I.N.A., as well as in the settings dialog of the `DarkSkyGeek's Filter Wheel Proxy For OAG Focuser` ASCOM device (see screenshot above). Remember that it is critical that your imaging application and the driver have the same filter offset values! **Note:** If you measure your filter offsets with a reducer, they will be different. Don't worry about that. Do the entire calibration procedure with your standard astrophotography setup. Once properly set up, the OAG focuser configuration will not need to change whether or not you add a reducer to your imaging train. @@ -209,7 +209,7 @@ Before you can use this device, you have to calibrate it. Here is the procedure: There are many sources of backlash in this system. The stepper motor itself, due to its internal gearbox, already has some amount of backlash. The 3D printed gear and pinion also have some backlash. And finally, the helical focuser has some backlash as well. All of those sources combine. Thankfully, compensating for backlash is easy and supported by the software in this repository. The trick is to first measure the amount of backlash in your system. That is done using a dial gauge. Please refer to the video below for more details. - Using the standalone focuser control application, setting a backlash compensation of 0, move in one direction by a large amount. Then, repetitively move in small increments in the opposite direction until the dial indicator starts moving. In my setup, I have a total of about 60 steps of backlash, so I set the backlash compensation amount to 150 (the software uses the so-called "overshoot" backlash compensation method) and it works absolutely flawlessly! Enter the value of the backlash, in number of steps (150 in my case), in the settings dialog of the `DarkSkyGeek’s Filter Wheel Proxy For OAG Focuser` ASCOM device (see screenshot above) + Using the standalone focuser control application, setting a backlash compensation of 0, move in one direction by a large amount. Then, repetitively move in small increments in the opposite direction until the dial indicator starts moving. In my setup, I have a total of about 60 steps of backlash, so I set the backlash compensation amount to 150 (the software uses the so-called "overshoot" backlash compensation method) and it works absolutely flawlessly! Enter the value of the backlash, in number of steps (150 in my case), in the settings dialog of the `DarkSkyGeek's Filter Wheel Proxy For OAG Focuser` ASCOM device (see screenshot above) 3. **Zero Position** @@ -227,13 +227,13 @@ Before you can use this device, you have to calibrate it. Here is the procedure: * Run another autofocus routine on the second N.I.N.A. instance (the one that controls the OAG focuser and the guide camera) * Subtract the new position of the OAG focuser with the initial position, and divide that number by the number of steps you moved the main telescope focuser (300 in our example). That will give you the steps ratio. Note that this will be a negative number. In my case, the steps ratio is -2.64. * You can run the autofocus routine several times, and average the positions obtained to increase accuracy. - * Enter the steps ratio in the settings dialog of the `DarkSkyGeek’s Filter Wheel Proxy For OAG Focuser` ASCOM device (see screenshot above) + * Enter the steps ratio in the settings dialog of the `DarkSkyGeek's Filter Wheel Proxy For OAG Focuser` ASCOM device (see screenshot above) **Important note:** Getting a clean AF curve from the guide camera / OAG focuser can be challenging because few stars will be detected. To make this task easier, point your telescope towards a star cluster, and dial in your star detection options. 1. **Test** - Once you have calibrated the OAG Focuser, you can close the second instance of N.I.N.A. and open PHD2. Use the first instance of N.I.N.A. as you normally would, although instead of connecting your filter wheel directly, you will connect to the `DarkSkyGeek’s Filter Wheel Proxy For OAG Focuser` device. Looking at the PHD2 live view, change the filter in the filter wheel and watch the PHD2 live view becoming blurry and then sharp again, automatically! Isn't technology beautiful?! + Once you have calibrated the OAG Focuser, you can close the second instance of N.I.N.A. and open PHD2. Use the first instance of N.I.N.A. as you normally would, although instead of connecting your filter wheel directly, you will connect to the `DarkSkyGeek's Filter Wheel Proxy For OAG Focuser` device. Looking at the PHD2 live view, change the filter in the filter wheel and watch the PHD2 live view becoming blurry and then sharp again, automatically! Isn't technology beautiful?! This procedure is explained in great detail in the following video: