#define Switch using System; using System.Runtime.InteropServices; using ASCOM.DeviceInterface; using System.Collections; using System.Collections.Generic; using System.Reflection; namespace ASCOM.LynxAstro.DewController { // // Your driver's DeviceID is ASCOM.LynxAstro.DewController.Switch // // The Guid attribute sets the CLSID for ASCOM.LynxAstro.DewController.Switch // The ClassInterface/None attribute prevents an empty interface called // _LynxAstro.DewController from being created and used as the [default] interface // // TODO Replace the not implemented exceptions with code to implement the function or // throw the appropriate ASCOM exception. // /// /// ASCOM Switch Driver for LynxAstro.DewController. /// [Guid("3a29744a-f33f-4843-a7e0-6938d9bd50ba")] [ProgId("ASCOM.LynxAstro.DewController.Switch")] [ServedClassName("LynxAstro.DewController")] [ClassInterface(ClassInterfaceType.None)] public class Switch : AscomDriverBase, ISwitchV2 { /// /// ASCOM DeviceID (COM ProgID) for this driver. /// The DeviceID is used by ASCOM applications to load the driver at runtime. /// internal static string DriverId = Marshal.GenerateProgIdForType(MethodBase.GetCurrentMethod().DeclaringType ?? throw new System.InvalidOperationException()); /// /// Initializes a new instance of the class. /// Must be public for COM registration. /// /// public Switch() { Initialise(nameof(Switch)); } public Switch(ISharedResourcesWrapper sharedResourcesWrapper) : base (sharedResourcesWrapper) { Initialise(nameof(Switch)); } // // PUBLIC COM INTERFACE ISwitchV2 IMPLEMENTATION // #region Common properties and methods. /// /// Displays the Setup Dialog form. /// If the user clicks the OK button to dismiss the form, then /// the new settings are saved, otherwise the old values are reloaded. /// THIS IS THE ONLY PLACE WHERE SHOWING USER INTERFACE IS ALLOWED! /// public void SetupDialog() { LogMessage("SetupDialog", "Opening setup dialog"); SharedResourcesWrapper.SetupDialog(); ReadProfile(); LogMessage("SetupDialog", "complete"); } public ArrayList SupportedActions { get { LogMessage("SupportedActions Get", "Returning empty arraylist"); return new ArrayList(); } } public string Action(string actionName, string actionParameters) { LogMessage("", "Action {0}, parameters {1} not implemented", actionName, actionParameters); throw new ASCOM.ActionNotImplementedException("Action " + actionName + " is not implemented by this driver"); } public void CommandBlind(string command, bool raw) { CheckConnected("CommandBlind"); SharedResourcesWrapper.SendBlind(command); } public bool CommandBool(string command, bool raw) { CheckConnected("CommandBool"); throw new ASCOM.MethodNotImplementedException("CommandBool"); } public string CommandString(string command, bool raw) { CheckConnected("CommandString"); return SharedResourcesWrapper.SendString(command); } public void Dispose() { // Clean up the trace logger and util objects Tl.Enabled = false; Tl.Dispose(); Tl = null; } private string DecodeResult(string pattern, string encodedString) { var decodedString = encodedString.Substring(pattern.Length).TrimEnd('#'); return decodedString; } public bool Connected { get { LogMessage("Connected", "Get {0}", IsConnected); return IsConnected; } set { LogMessage("Connected", "Set {0}", value); if (value == IsConnected) return; if (value) { try { ReadProfile(); LogMessage("Connected Set", "Connecting to port {0}", ComPort); var connectionInfo = SharedResourcesWrapper.Connect("Serial", DriverId, Tl); try { LogMessage("Connected Set", $"Connected to port {ComPort}. Version:{SharedResourcesWrapper.FirmwareVersion}"); IsConnected = true; LogMessage("Connected Set", $"Connected OK."); var maxSwitch = MaxSwitch; } catch (Exception) { SharedResourcesWrapper.Disconnect("Serial", DriverId); throw; } } catch (Exception ex) { LogMessage("Connected Set", "Error connecting to port {0} - {1}", ComPort, ex.Message); } } else { LogMessage("Connected Set", "Disconnecting from port {0}", ComPort); SharedResourcesWrapper.Disconnect("Serial", DriverId); IsConnected = false; } } } public short InterfaceVersion { // set by the driver wizard get { LogMessage("InterfaceVersion Get", "2"); return Convert.ToInt16("2"); } } public string Name { get { string name = "LynxAstro.DewController"; LogMessage("Name Get", name); return name; } } #endregion #region ISwitchV2 Implementation private short numSwitch = 0; private List switchNames = new List(); /// /// The number of switches managed by this driver /// public short MaxSwitch { get { CheckConnected("MaxSwitch Get"); var result = SharedResourcesWrapper.SendString(":GD#"); var decoded = DecodeResult(":GD", result); this.numSwitch = short.Parse(decoded); //Command: :GD# //Purpose: Get the device type, i.e.the number of channels the dew controller has. //Response: :GDX# where X is either 1 or 4 depending on the number of channels this device has LogMessage("MaxSwitch Get", numSwitch.ToString()); return this.numSwitch; } } /// /// Return the name of switch n /// /// The switch number to return /// /// The name of the switch /// public string GetSwitchName(short id) { Validate("GetSwitchName", id); ReadProfile(); var switchName = switchNames.Count > id ? switchNames[id] : string.Empty; LogMessage("GetSwitchName", $"{id}: {switchName}"); return switchName; } /// /// Sets a switch name to a specified value /// /// The number of the switch whose name is to be set /// The name of the switch public void SetSwitchName(short id, string name) { Validate("SetSwitchName", id); ReadProfile(); while (id > switchNames.Count) switchNames.Add(string.Empty); LogMessage("SetSwitchName", $"{id}: \"{switchNames[id]}\" to \"{name}\""); switchNames[id] = name; WriteSwitchNames(); } /// /// Gets the switch description. /// /// The id. /// public string GetSwitchDescription(short id) { Validate("GetSwitchDescription", id); var channel= $"Channel {ChannelToLetter(id)}"; LogMessage("GetSwitchDescription", channel); return channel; } /// /// Reports if the specified switch can be written to. /// This is false if the switch cannot be written to, for example a limit switch or a sensor. /// The default is true. /// /// The number of the switch whose write state is to be returned /// true if the switch can be written to, otherwise false. /// /// If the method is not implemented /// If id is outside the range 0 to MaxSwitch - 1 public bool CanWrite(short id) { Validate("CanWrite", id); // default behavour is to report true LogMessage("CanWrite", $"CanWrite({id}) - default true"); return true; // implementation should report the correct behaviour //tl.LogMessage("CanWrite", string.Format("CanWrite({0}) - not implemented", id)); //throw new MethodNotImplementedException("CanWrite"); } #region boolean switch members /// /// Return the state of switch n /// a multi-value switch must throw a not implemented exception /// /// The switch number to return /// /// True or false /// public bool GetSwitch(short id) { var value = GetSwitchValue(id); var switchIsOn = value > 0; LogMessage("GetSwitch", $"{id}: {switchIsOn} ({value})"); return switchIsOn; } /// /// Sets a switch to the specified state /// If the switch cannot be set then throws a MethodNotImplementedException. /// A multi-value switch must throw a not implemented exception /// setting it to false will set it to its minimum value. /// /// /// public void SetSwitch(short id, bool state) { Validate("SetSwitch", id); if (!CanWrite(id)) { var str = string.Format("SetSwitch({0}) - Cannot Write", id); Tl.LogMessage("SetSwitch", str); throw new MethodNotImplementedException(str); } var switchIsOn = state ? 1023 : 0; LogMessage("SetSwitch", $"{id}: {state} ({switchIsOn})"); SetSwitchValue(id, switchIsOn); } #endregion #region analogue members /// /// returns the maximum value for this switch /// boolean switches must return 1.0 /// /// /// public double MaxSwitchValue(short id) { Validate("MaxSwitchValue", id); // boolean switch implementation: return 1023; // or //tl.LogMessage("MaxSwitchValue", string.Format("MaxSwitchValue({0}) - not implemented", id)); //throw new MethodNotImplementedException("MaxSwitchValue"); } /// /// returns the minimum value for this switch /// boolean switches must return 0.0 /// /// /// public double MinSwitchValue(short id) { Validate("MinSwitchValue", id); // boolean switch implementation: return 0; // or //tl.LogMessage("MinSwitchValue", string.Format("MinSwitchValue({0}) - not implemented", id)); //throw new MethodNotImplementedException("MinSwitchValue"); } /// /// returns the step size that this switch supports. This gives the difference between /// successive values of the switch. /// The number of values is ((MaxSwitchValue - MinSwitchValue) / SwitchStep) + 1 /// boolean switches must return 1.0, giving two states. /// /// /// public double SwitchStep(short id) { Validate("SwitchStep", id); // boolean switch implementation: return 1; // or //tl.LogMessage("SwitchStep", string.Format("SwitchStep({0}) - not implemented", id)); //throw new MethodNotImplementedException("SwitchStep"); } /// /// returns the analogue switch value for switch id /// boolean switches must throw a not implemented exception /// /// /// public double GetSwitchValue(short id) { Validate("GetSwitchValue", id); Tl.LogMessage("GetSwitchValue", string.Format("GetSwitchValue({0}) - not implemented", id)); var channel = ChannelToLetter(id); var encoded = SharedResourcesWrapper.SendString($":GC{channel}#"); var decoded = DecodeResult(":GC", encoded); LogMessage("GetSwitchValue", $"{id}: {channel} - {decoded}"); return double.Parse(decoded.TrimStart(channel)); //Command: :GCX# where X is the channel A, B, C or D to retrieve. //Purpose: Get the current power setting for a specific channel. //Response: :GCXVVVV# where X is the channel A, B, C or D returned and VVVV is the power level between 0 - 1023. //Possible Errors //:ER5# = Channel out of range, e.g. channel B on a 1 channel device. } private char ChannelToLetter(short id) { UInt16 ord = (UInt16) 'A'; ord += (ushort)id; return (char)ord; } /// /// set the analogue value for this switch. /// If the switch cannot be set then throws a MethodNotImplementedException. /// If the value is not between the maximum and minimum then throws an InvalidValueException /// boolean switches must throw a not implemented exception. /// /// /// public void SetSwitchValue(short id, double value) { Validate("SetSwitchValue", id, value); if (!CanWrite(id)) { Tl.LogMessage("SetSwitchValue", string.Format("SetSwitchValue({0}) - Cannot write", id)); throw new ASCOM.MethodNotImplementedException(string.Format("SetSwitchValue({0}) - Cannot write", id)); } //Tl.LogMessage("SetSwitchValue", string.Format("SetSwitchValue({0}) = {1} - not implemented", id, value)); //throw new MethodNotImplementedException("SetSwitchValue"); //Command: :SCXVVVV# where X is the channel A, B, C or D to set and VVVV is the power level //between 0 - 1023.The power level must be 4 digits long so pad with leading zeros if necessary. //Purpose: Set the current power setting for a specific channel. //Response: :SC1# indicates success. Run a GA or GC command to verify. //Possible Errors //:ER4# = Not enough data received - make sure you zero pad the power level. //:ER5# = Channel or power level out of range, e.g. channel B on a 1 channel device or power above 1023. var channel = ChannelToLetter(id); LogMessage("SetSwitchValue", $"{id}: {channel} - {value}"); var encoded = SharedResourcesWrapper.SendString($":SC{channel}{value:0000}#"); var decoded = DecodeResult(":SC", encoded); if (decoded != "1") { throw new InvalidValueException($"Unable to set switch {{id}} to value {value}"); } //return double.Parse(decoded.TrimStart(channel)); } #endregion #endregion #region private methods /// /// Checks that the switch id is in range and throws an InvalidValueException if it isn't /// /// The message. /// The id. private void Validate(string message, short id) { if (id < 0 || id >= numSwitch) { Tl.LogMessage(message, string.Format("Switch {0} not available, range is 0 to {1}", id, numSwitch - 1)); throw new InvalidValueException(message, id.ToString(), string.Format("0 to {0}", numSwitch - 1)); } } /// /// Checks that the switch id and value are in range and throws an /// InvalidValueException if they are not. /// /// The message. /// The id. /// The value. private void Validate(string message, short id, double value) { Validate(message, id); var min = MinSwitchValue(id); var max = MaxSwitchValue(id); if (value < min || value > max) { Tl.LogMessage(message, string.Format("Value {1} for Switch {0} is out of the allowed range {2} to {3}", id, value, min, max)); throw new InvalidValueException(message, value.ToString(), string.Format("Switch({0}) range {1} to {2}", id, min, max)); } } /// /// Checks that the number of states for the switch is correct and throws a methodNotImplemented exception if not. /// Boolean switches must have 2 states and multi-value switches more than 2. /// /// /// /// //private void Validate(string message, short id, bool expectBoolean) //{ // Validate(message, id); // var ns = (int)(((MaxSwitchValue(id) - MinSwitchValue(id)) / SwitchStep(id)) + 1); // if ((expectBoolean && ns != 2) || (!expectBoolean && ns <= 2)) // { // tl.LogMessage(message, string.Format("Switch {0} has the wriong number of states", id, ns)); // throw new MethodNotImplementedException(string.Format("{0}({1})", message, id)); // } //} #endregion #region Private properties and methods // here are some useful properties and methods that can be used as required // to help with driver development #region ASCOM Registration // Register or unregister driver for ASCOM. This is harmless if already // registered or unregistered. // /// /// Register or unregister the driver with the ASCOM Platform. /// This is harmless if the driver is already registered/unregistered. /// /// If true, registers the driver, otherwise unregisters it. private static void RegUnregASCOM(bool bRegister) { using (var P = new ASCOM.Utilities.Profile()) { P.DeviceType = "Switch"; if (bRegister) { P.Register(DriverId, DriverDescription); } else { P.Unregister(DriverId); } } } /// /// This function registers the driver with the ASCOM Chooser and /// is called automatically whenever this class is registered for COM Interop. /// /// Type of the class being registered, not used. /// /// This method typically runs in two distinct situations: /// /// /// In Visual Studio, when the project is successfully built. /// For this to work correctly, the option Register for COM Interop /// must be enabled in the project settings. /// /// During setup, when the installer registers the assembly for COM Interop. /// /// This technique should mean that it is never necessary to manually register a driver with ASCOM. /// [ComRegisterFunction] public static void RegisterASCOM(Type t) { RegUnregASCOM(true); } /// /// This function unregisters the driver from the ASCOM Chooser and /// is called automatically whenever this class is unregistered from COM Interop. /// /// Type of the class being registered, not used. /// /// This method typically runs in two distinct situations: /// /// /// In Visual Studio, when the project is cleaned or prior to rebuilding. /// For this to work correctly, the option Register for COM Interop /// must be enabled in the project settings. /// /// During uninstall, when the installer unregisters the assembly from COM Interop. /// /// This technique should mean that it is never necessary to manually unregister a driver from ASCOM. /// [ComUnregisterFunction] public static void UnregisterASCOM(Type t) { RegUnregASCOM(false); } #endregion /// /// Use this function to throw an exception if we aren't connected to the hardware /// /// private void CheckConnected(string message) { if (!IsConnected) { throw new ASCOM.NotConnectedException(message); } } protected void WriteSwitchNames() { var profileProperties = SharedResourcesWrapper.ReadProfile(); profileProperties.SwitchNames.Clear(); profileProperties.SwitchNames.AddRange(switchNames); SharedResourcesWrapper.WriteProfile(profileProperties); } #endregion } }