// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.IO;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Net;
using System.Runtime.InteropServices;
using System.Xml.Serialization;
using System.Text;
namespace OpenTap
{
internal class VISAException : Exception
{
public int ErrorCode { get; private set; }
private static string GetMessage(int vi, int error)
{
StringBuilder sb = new StringBuilder(1024);
if (Visa.viStatusDesc(vi, error, sb) == Visa.VI_SUCCESS)
return sb.ToString();
else
return "Unknown error";
}
public VISAException(int vi, int error) : base(GetMessage(vi, error))
{
ErrorCode = error;
}
public VISAException(string message, int error) : base(message)
{
ErrorCode = error;
}
}
///
/// Implements a connection to talk to any SCPI-enabled instrument.
///
[Display("Generic SCPI Instrument", Description: "Allows you to configure a VISA based connection to a SCPI instrument.")]
public class ScpiInstrument : Instrument, IScpiInstrument
{
private readonly IScpiIO2 scpiIO;
/// Some instruments does not support reading the status byte. This is detected during Open.
bool readStbSupported = true;
/// since sending scpi commands/queries (and reading) is not thread safe, but steps using SCPI can run in multiple threads
/// We lock IO with this commandLock.
readonly object commandLock = new object();
IScpiIO IScpiInstrument.IO { get { return scpiIO; } }
static readonly TraceSource staticLog = OpenTap.Log.CreateSource("SCPI");
static int visa_resource = Visa.VI_NULL;
static bool visa_tried_load = false;
static bool visa_failed = false;
private void RaiseError(ScpiIOResult error)
{
switch (error)
{
case ScpiIOResult.Error_ConnectionLost: throw new VISAException("Connection lost", Visa.VI_ERROR_CONN_LOST);
case ScpiIOResult.Error_General: throw new VISAException("General error", Visa.VI_ERROR_IO);
case ScpiIOResult.Error_ResourceLocked: throw new VISAException("Resource locked", Visa.VI_ERROR_RSRC_LOCKED);
case ScpiIOResult.Error_Timeout: throw new VISAException("IO Timeout", Visa.VI_ERROR_TMO);
case ScpiIOResult.Error_ResourceNotFound: throw new VISAException("Resource not found", Visa.VI_ERROR_RSRC_NFOUND);
}
}
private static void RaiseError2(int error)
{
if (error < 0)
throw new VISAException(0, error);
}
// Required by .NET to catch AccessViolationException.
[System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
internal static int GetResourceManager()
{
try
{
if (visa_resource == Visa.VI_NULL && !visa_tried_load)
RaiseError2(Visa.viOpenDefaultRM(out visa_resource));
}
catch (Exception e)
{
visa_failed = true;
visa_tried_load = true;
staticLog.Warning("Could not create VISA resource manager. This could be because of a missing VISA provider. Please install/reinstall Keysight IO Libraries.");
staticLog.Debug(e);
}
return visa_resource;
}
static bool checkVisaAddressAsync(string str, int millisecondTimeout)
{
if (visa_failed) return true;
bool ok = true;
using (var semaphore = new Semaphore(0, 1))
{
TapThread.Start(() =>
{
try
{
ok = checkVisaAddress(str);
}
finally
{
semaphore.Release();
}
});
if (!semaphore.WaitOne(millisecondTimeout))
visa_failed = true;
}
return ok;
}
static bool checkVisaAddress(string str)
{
if (visa_failed) return true;
try
{
short ifType = 0;
short partNumber = 0;
var error = Visa.viParseRsrc(visa_resource, str, ref ifType, ref partNumber);
if (error < 0) return false;
return true;
}
catch (Exception)
{
return false;
}
}
static Memorizer validator = new Memorizer(s => checkVisaAddressAsync(s, 5000))
{
SoftSizeDecayTime = TimeSpan.FromSeconds(10)
};
private bool visaAddrValid()
{
if (string.IsNullOrWhiteSpace(VisaAddress)) return false;
// Ignore this validation if the visa address is a valid ip address
if (Regex.IsMatch(VisaAddress, IpPattern)) return true;
validator.CheckConstraints();
return validator.Invoke(VisaAddress);
}
#region Settings
///
/// The VISA address of the instrument that this class represents a connection to.
///
[Display("Address", Groups: new []{"VISA"}, Order: 1, Description: "The VISA address of the instrument e.g. 'TCPIP::1.2.3.4::INSTR' or 'GPIB::14::INSTR'")]
[VisaAddress]
public string VisaAddress { get; set; }
///
/// The timeout used by the underlying VISA driver when communicating with the instrument [ms].
///
[Display("I/O Timeout", Groups: new []{"VISA"}, Order: 1, Description: "The timeout used in the VISA driver when communicating with the instrument. (Default is 2 s and resolutions is 1 ms)")]
[Unit("s", PreScaling: 1000, UseEngineeringPrefix: true)]
public int IoTimeout
{
get { return scpiIO.IOTimeoutMS; }
set { scpiIO.IOTimeoutMS = value; }
}
private uint lockRetries = 5;
private double lockHoldoff = 0.1;
///
/// If enabled ScpiInstrument acquires an exclusive lock when opening the instrument.
///
[Display("Lock Queries", Groups: new []{"VISA", "Locking"}, Order: 2.6, Collapsed: true, Description: "If enabled the instrument will acquire an exclusive lock when performing SCPI queries. This might degrade performance")]
[EnabledIf("Lock", false)]
public bool FinegrainedLock { get; set; }
///
/// If enabled ScpiInstrument acquires an exclusive lock when opening the instrument.
///
[Display("Lock Instrument", Groups: new []{"VISA", "Locking"}, Order: 3, Collapsed: true, Description: "If enabled the instrument will be opened with exclusive access. This will disallow other clients from accessing the instrument.")]
public bool Lock { get; set; }
///
/// Specifies how many times the SCPI instrument should retry an operation, if it was canceled by another host locking the device.
///
[Display("Lock Retries", Groups: new []{"VISA", "Locking"}, Order: 3, Collapsed: true, Description: "Specifies how many times the SCPI instrument should retry an operation, if it was canceled by another host locking the device.")]
public uint LockRetries
{
get { return lockRetries; }
set { lockRetries = value; }
}
///
/// Specifies how long the SCPI instrument should wait before it retries an operation, if it was canceled by another host locking the device.
///
[Unit("s", true)]
[Display("Lock Hold Off", Groups: new []{"VISA", "Locking"}, Order: 2.8, Collapsed: true, Description: "Specifies how long the SCPI instrument should wait before it retries an operation, if it was canceled by another host locking the device.")]
public double LockHoldoff
{
get { return lockHoldoff; }
set { lockHoldoff = value; }
}
///
/// When enabled, causes the instrument driver to ask the instrument SYST:ERR? after every command. Useful when debugging.
///
[Display("Error Checking", Groups: new []{"VISA", "Debug"}, Order: 4, Collapsed: true, Description: "When enabled, the instrument driver will ask the instrument SYST:ERR? after every command.")]
public bool QueryErrorAfterCommand { get; set; }
///
/// When true, will send VIClear() right after establishing a connection.
///
[Display("Send VIClear On Connect", Groups: new []{"VISA", "Debug"}, Order: 4.1, Collapsed: true, Description: "Send VIClear() when opening the connection to the instrument.")]
public bool SendClearOnConnect { get; set; }
/// Gets or sets whether Verbose SCPI logging is enabled.
[Display("Verbose SCPI Logging", Groups: new []{"VISA", "Debug"}, Order:4.2, Collapsed:true, Description: "Enables verbose logging of SCPI communication.")]
public bool VerboseLoggingEnabled { get; set; } = true;
///
/// When true, will send *IDN? right after establishing a connection.
///
[Display("Send *IDN? On Connect", Groups: new[] { "VISA", "Debug" }, Order: 4.3, Collapsed: true, Description: "Send *IDN? when opening the connection to the instrument.")]
public bool SendIDNOnConnect { get; set; }
///
/// When true, will send *CLS right after establishing a connection.
///
[Display("Send *CLS On Connect", Groups: new[] { "VISA", "Debug" }, Order: 4.4, Collapsed: true, Description: "Send *CLS when opening the connection to the instrument.")]
public bool SendCLSOnConnect { get; set; }
#endregion
///
/// Overrides ToString() to give more meaningful names.
///
///
public override string ToString()
{
return String.Format("{0} ({1})", Name, VisaAddress);
}
///
/// Gets the instrument identity string. (As returned by the SCPI command *IDN?).
///
[XmlIgnore]
[MetaData]
public string IdnString { get; private set; }
private string IpPattern =
"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
/// Initializes a new instance of the ScpiInstrument class.
public ScpiInstrument() : this(new ScpiIO())
{
}
/// Initialize a new instance of ScpiInstrument, specifying a IScpiIO interface to use.
/// An IO Implementation for doing communication.
public ScpiInstrument(IScpiIO2 io)
{
Name = "SCPI";
this.scpiIO = io;
IoTimeout = 2000;
// Just trigger the use of the resource manager to test if VISA libraries is installed.
GetResourceManager();
// default value for settings:
SendClearOnConnect = true;
SendIDNOnConnect = true;
SendCLSOnConnect = true;
Rules.Add(() => LockHoldoff >= 0, "Lock holdoff must be positive.", nameof(LockHoldoff));
Rules.Add(() => IoTimeout >= 0, "I/O timeout must be positive.", nameof(IoTimeout));
Rules.Add(visaAddrValid, "Invalid VISA address format.", nameof(VisaAddress));
Rules.Add(() => !Regex.IsMatch(VisaAddress ?? "", IpPattern), () => "Invalid VISA address, did you mean 'TCPIP::" + VisaAddress + "::INSTR'?", nameof(VisaAddress));
}
private byte TerminationCharacter { get { return scpiIO.TerminationCharacter; } set { scpiIO.TerminationCharacter = value; } }
private bool TerminationCharacterEnabled { get { return scpiIO.UseTerminationCharacter; } set { scpiIO.UseTerminationCharacter = value; } }
private bool SendEndEnabled { get { return scpiIO.SendEnd; } set { scpiIO.SendEnd = value; } }
///
/// Called by InstrumentBase.Open() before the newly opened connection is used for anything.
/// Allows specializations of InstrumentBase to customize connection parameters such as
/// TerminationCharacter.
///
protected virtual void SetTerminationCharacter(int inst)
{
// inst.ResourceName expands eventual aliases.
if (scpiIO.ResourceClass.Contains("SOCKET"))
{
TerminationCharacterEnabled = true;
}
}
private void LockRetry(Action action)
{
int Retry = 0;
do
{
try
{
action();
return;
}
catch (VISAException ex)
{
if (ex.ErrorCode != Visa.VI_ERROR_RSRC_LOCKED)
throw;
}
Thread.Sleep(TimeSpan.FromSeconds(lockHoldoff));
}
while (Retry++ < lockRetries);
throw new IOException("Instrument locked");
}
private T LockRetry(Func action)
{
int Retry = 0;
do
{
try
{
var res = action();
return res;
}
catch (VISAException ex)
{
if (ex.ErrorCode != Visa.VI_ERROR_RSRC_LOCKED)
throw;
}
Thread.Sleep(TimeSpan.FromSeconds(lockHoldoff));
}
while (Retry++ < lockRetries);
throw new IOException("Instrument locked");
}
///
/// Opens the connection to the Instrument.
/// Assumes Visa Address property is specified.
///
public override void Open()
{
if (scpiIO is ScpiIO && visa_failed)
{
Log.Error("No VISA provider installed. Please install/reinstall Keysight IO Libraries, or similar VISA provider.");
throw new Exception("Could not create VISA connection.");
}
int retry = 0;
do
{
Log.Debug("Connecting to '{0}'", VisaAddress);
try
{
RaiseError(scpiIO.Open(VisaAddress, Lock));
if (SendClearOnConnect)
{
LockRetry(() => DoClear());
}
{
base.Open();
SetTerminationCharacter(scpiIO.ID);
if (SendIDNOnConnect)
{
IdnString = QueryIdn();
IdnString = IdnString.Trim();
Log.Info("Now connected to: " + IdnString);
}
if (SendCLSOnConnect)
{
CommandCls(); // Empty error log
}
try
{
scpiIO.OpenSRQ();
}
catch (Exception ex)
{
Log.Error($"Unable to attach SRQ handler: {ex.Message}");
Log.Debug(ex);
throw;
}
try
{
DoReadSTB();
}
catch
{
readStbSupported = false;
}
}
return; // We are done opening the device
}
catch (VISAException ex)
{
switch (ex.ErrorCode)
{
case Visa.VI_ERROR_RSRC_LOCKED:
if (retry < lockRetries) Thread.Sleep(TimeSpan.FromSeconds(lockHoldoff));
break;
default:
string errorMsg = String.Format("Cannot connect to instrument on address '{0}'.", VisaAddress);
throw new IOException(errorMsg, ex);
}
}
catch (Exception ex)
{
string errorMsg = String.Format("Cannot connect to instrument on address '{0}'.", VisaAddress);
throw new IOException(errorMsg, ex);
}
}
while (retry++ < lockRetries);
string errorMsg2 = String.Format("Cannot connect to instrument on address '{0}'. Device already locked by another connection.", VisaAddress);
throw new IOException(errorMsg2);
}
///
/// Closes the connection to the instrument. Assumes connection is open.
///
public override void Close()
{
try
{
try
{
scpiIO.CloseSRQ();
}
catch (Exception ex)
{
Log.Error("Caught error while closing SRQ.");
Log.Debug(ex);
}
try
{
RaiseError(scpiIO.Close());
}
catch (Exception ex)
{
Log.Error("Caught error while closing instrument.");
Log.Debug(ex);
}
}
finally
{
base.Close();
}
}
///
/// Clears the device SCPI buffers.
///
protected virtual void DoClear()
{
lock(commandLock)
RaiseError(scpiIO.DeviceClear());
}
///
/// Reads the status byte.
///
///
protected virtual short DoReadSTB()
{
byte status = 0;
lock(commandLock)
RaiseError(scpiIO.ReadSTB(ref status));
return status;
}
private ScpiIOResult Read(ArraySegment data, int count, out int read)
{
bool eoi = false;
read = 0;
var res = scpiIO.Read(data, count, ref eoi, ref read);
RaiseError(res);
return res;
}
private byte[] Read(int count)
{
byte[] buffer = new byte[count];
int read;
Read(new ArraySegment(buffer), count, out read);
if (read != count)
Array.Resize(ref buffer, read);
return buffer;
}
private string ReadString(int count)
{
return System.Text.Encoding.ASCII.GetString(Read(count));
}
const int BufferSize = 16 * 1024;
[ThreadStatic] static byte[] readBuffer; //[ThreadStatic] field cannot have an initializer.
private string ReadString()
{
// copy readBuffer to avoid reading/writing the thread static variable more than necessary.
// this is known to be a relatively slow operation.
byte[] buffer = readBuffer ?? new byte[BufferSize];
int total = 0;
var res = Read(new ArraySegment(buffer), buffer.Length, out total);
while (res == ScpiIOResult.Success_MaxCount)
{
Array.Resize(ref buffer, (buffer.Length * 4) / 3); // Geometric resizing
int subRead;
res = Read(new ArraySegment(buffer, total, buffer.Length - total), buffer.Length - total, out subRead);
if (subRead > 0)
total += subRead;
}
readBuffer = buffer;
return Encoding.ASCII.GetString(readBuffer, 0, total);
}
private int Write(byte[] data, int count)
{
int sent = 0;
var res = scpiIO.Write(new ArraySegment(data), count, ref sent);
RaiseError(res);
return sent;
}
private void WriteString(string data)
{
if (SendEndEnabled && !data.EndsWith("\n"))
data = data + "\n";
byte[] buf = System.Text.Encoding.ASCII.GetBytes(data);
Write(buf, buf.Length);
}
private void WriteIEEEBlock(string command, byte[] data)
{
var origEndEnabled = SendEndEnabled;
try
{
SendEndEnabled = false;
string header = data.Length.ToString();
header = '#' + header.Length.ToString() + header;
WriteString(command + header);
Write(data, data.Length);
SendEndEnabled = origEndEnabled;
WriteString("\n");
}
finally
{
SendEndEnabled = origEndEnabled;
}
}
private string ReadStringOrBlock()
{
if (!TerminationCharacterEnabled)
{
return LockRetry(() => ReadString());
}
else
{
// To make sure we get the same behavior when TerminationCharacter is enabled.
// we need to manually check and handle IEEE488 formatted blocks.
char firstChar = LockRetry(() => (char)Read(1).First());
if (firstChar == '#') // is this a IEEE488 formatted block.
{
int lengthFieldLength = int.Parse((LockRetry(() => (char)Read(1).First())).ToString());
string lengthFieldText = LockRetry(() => ReadString(lengthFieldLength));
int length = int.Parse(lengthFieldText);
TerminationCharacterEnabled = false;
string text = LockRetry(() => ReadString(length));
if (text.LastOrDefault() == TerminationCharacter)
{
// Do nothing, this is a hack to fix an issue with instruments including the termination
// character as the last character in the block.
}else
{
text += TerminationCharacter;
// Read the terminating character to get it off the output buffer
LockRetry(() => ReadString());
}
TerminationCharacterEnabled = true;
return text;
}
else
{
string theRest = LockRetry(() => ReadString());
return firstChar + theRest;
}
}
}
private void DoLock()
{
RaiseError(scpiIO.Lock(ScpiLockType.Exclusive));
}
private void DoUnlock()
{
RaiseError(scpiIO.Unlock());
}
///
/// Sends a SCPI query to the instrument and waits for a response.
///
/// The SCPI query to send.
/// True to suppress log messages.
/// The response from the instrument.
public virtual string ScpiQuery(string query, bool isSilent = false)
{
if (query == null)
throw new ArgumentNullException(nameof(query));
if (!IsConnected)
throw new IOException("Not connected.");
lock (commandLock)
{
OnActivity();
string result = String.Empty;
try
{
Stopwatch timer = Stopwatch.StartNew();
if (FinegrainedLock && !Lock)
{
LockRetry(() => DoLock());
try
{
LockRetry(() => WriteString(query));
LockRetry(() => result = ReadStringOrBlock());
}
finally
{
DoUnlock();
}
}
else
{
LockRetry(() => WriteString(query));
LockRetry(() => result = ReadStringOrBlock());
}
if (!isSilent && VerboseLoggingEnabled)
Log.Debug(timer, "SCPI >> {0}", query);
}
catch (VISAException ex)
{
if (ex.ErrorCode == Visa.VI_ERROR_CONN_LOST)
{
Log.Error("Connection lost");
IsConnected = false;
throw new IOException("Connection lost");
}
if (ex.ErrorCode == Visa.VI_ERROR_TMO)
{
Log.Error("Not responding (query '{0}' timed out)", query);
throw new TimeoutException("Not responding");
}
Log.Error("SCPI query failed ({0})", query);
Log.Error(ex);
return null;
}
IsConnected = true;
if (!isSilent && VerboseLoggingEnabled)
Log.Debug("SCPI << {0}", result);
return result;
}
}
///
/// As ScpiQuery except it will try to parse the returned string to T. See for details on parsing.
///
///
///
/// True to suppress log messages.
///
public T ScpiQuery(string query, bool isSilent = false)
{
string response = ScpiQuery(query, isSilent);
return Scpi.Parse(response);
}
///
/// Sends a IEEE Block SCPI query to the instrument and waits for a response. The response is assumed to be IEEE block data.
///
/// The SCPI query to send.
/// The response from the instrument.
public byte[] ScpiQueryBlock(string query)
{
return ScpiQueryBlock(query);
}
private T[] ReadIEEEBlock() where T : struct
{
return LockRetry(() => DoReadIEEEBlock());
}
//Work around for bug found IVI Shared Components. #2487
private T[] DoReadIEEEBlock(bool seekToBlock = true, bool flushToEND = true) where T: struct
{
//Variable Declarations
int dataSize;
int dataSizeLen;
byte[] dataBytes;
//save the termchar for later use
byte termChar = TerminationCharacter;
bool termCharEn = TerminationCharacterEnabled;
//
//Part 1) Parse the header and grab the data
//
//If seekToBlock = True, scan for "#". Will timeout and throw an Exception if "#" is not found.
if (seekToBlock)
{
// Do an early test by just reading one character. This will most likely be the common case
var buff = Read(1);
if ((buff.Length != 1) || (buff[0] != (byte)'#'))
{
TerminationCharacter = (byte)'#';
TerminationCharacterEnabled = true;
ReadString(); // if it can't find a # it will timeout
//If we get to this point, we know that the # was found. Disable the Termination Character.
TerminationCharacter = termChar;
TerminationCharacterEnabled = false;
}
}
else
{
//If the first char in the buffer is not a "#", throw an Exception.
var buff = Read(1);
if (buff[0] != (byte)'#')
{
throw new VISAException("The Binary Block could not be parsed.", -2147221439);
}
}
//Get the size of the "binary block data length field"
var digits = ReadString(1);
dataSizeLen = int.Parse(digits);
if (dataSizeLen == 0)
{
// Indefinite length arbitrary block. Terminated by newline
TerminationCharacterEnabled = true;
TerminationCharacter = (byte)'\n';
dataBytes = System.Text.Encoding.ASCII.GetBytes(ReadString());
dataSize = dataBytes.Length;
}
else
{
//Get the size of the "binary block data"
var length = ReadString(dataSizeLen);
dataSize = int.Parse(length);
TerminationCharacterEnabled = false;
//Grab the data
dataBytes = Read(dataSize);
}
if (flushToEND)
{
int originalTimeout = IoTimeout;
IoTimeout = 100;
TerminationCharacter = termChar;
TerminationCharacterEnabled = true;
try
{
ReadString(); //throw everything away until we get a trailing \n, but fail in 100 ms if its not there
}
finally
{
IoTimeout = originalTimeout;
TerminationCharacterEnabled = termCharEn;
}
}
else
{
//Reset I/O to state before call
TerminationCharacter = termChar;
TerminationCharacterEnabled = termCharEn;
}
//Variables to process bytes coming back from the instrument
int dataTypeLen = Marshal.SizeOf();
//Part 2) Format the bytes that come back from the read.
//Note, if the instrument returns big endian data, we have to correct for it.
var data = new T[dataSize / dataTypeLen];
// Emulate VISA-COM behavior.
if (dataSize != data.Length * dataTypeLen)
throw new VISAException("The Safearray cannot be converted to the desired type.", -2147221439);
Buffer.BlockCopy(dataBytes, 0, data, 0, dataBytes.Length);
return data;
}
///
/// Sends a IEEE Block SCPI query to the instrument and waits for a response. The response is assumed to be IEEE block data.
///
/// The SCPI query to send.
/// The response from the instrument.
///
/// The format for data returned by the query must be configured to a type matching the type of .
/// Example 1:
///
/// ScpiCommand("FORMat:TRACe:DATA REAL,32", true);
/// var data = ScpiQueryBlock<float>(":TRACe:DATA? TRACE1");
///
/// Example 2:
///
/// ScpiCommand("FORMat:TRACe:DATA REAL,64", true);
/// var data = ScpiQueryBlock<double>(":TRACe:DATA? TRACE1");
///
///
public virtual T[] ScpiQueryBlock(string query) where T : struct
{
if (query == null)
throw new ArgumentNullException(nameof(query));
lock (commandLock)
{
if (FinegrainedLock && !Lock)
LockRetry(() => DoLock());
try
{
ScpiCommandInternal(query, false);
switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.Int16:
case TypeCode.UInt16:
case TypeCode.Int32:
case TypeCode.UInt32:
case TypeCode.Single:
case TypeCode.Double:
return ReadIEEEBlock();
// Do binary conversion for 64 bit integers
case TypeCode.Int64:
case TypeCode.UInt64:
{
// TODO: This looks unnecessary
// Figure out if any special handling is required for 64 bit reads at all
byte[] result = ReadIEEEBlock();
if (result.Length < 8)
return Array.Empty();
T[] arr = new T[result.Length / 8];
Buffer.BlockCopy(result, 0, arr, 0, arr.Length * 8);
return arr;
}
default:
throw new Exception("Unsupported IEEE488.2 block type: " + typeof(T).Name);
}
}
finally
{
if (FinegrainedLock && !Lock)
DoUnlock();
}
}
}
///
/// Sends a SCPI command to the instrument, but with special handling for parameters. See for more information.
///
/// The command to send, including placement arguments.
/// arguments.
/// Non-blocking.
public void ScpiCommand(string command, params object[] parameters)
{
if (command == null)
throw new ArgumentNullException(nameof(command));
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
ScpiCommand(Scpi.Format(command, parameters));
}
///
/// Sends a SCPI command to the instrument.
///
/// The command to send.
/// Non-blocking.
public virtual void ScpiCommand(string command)
{
if (command == null)
throw new ArgumentNullException(nameof(command));
ScpiCommandInternal(command, QueryErrorAfterCommand);
}
void ScpiCommandInternal(string command, bool checkErrors)
{
if (!IsConnected)
throw new IOException("Not connected.");
lock (commandLock)
{
OnActivity();
try
{
Stopwatch timer = Stopwatch.StartNew();
LockRetry(() => WriteString(command));
timer.Stop();
if (VerboseLoggingEnabled)
Log.Debug(timer, "SCPI >> {0}", command);
if (checkErrors)
{
WaitForOperationComplete();
queryErrors(noAllocation: true);
}
}
catch (VISAException ex)
{
if (ex.ErrorCode == Visa.VI_ERROR_CONN_LOST)
{
Log.Error("Connection lost");
IsConnected = false;
throw new IOException("Connection lost");
}
if (ex.ErrorCode == Visa.VI_ERROR_TMO)
{
Log.Error("Not responding (command '{0}' timed out)", command);
throw new IOException("Not responding");
}
Log.Error(ex);
}
}
}
///
/// Sends a IEEE Block SCPI command to the instrument.
///
public virtual void ScpiIEEEBlockCommand(string command, byte[] data)
{
if (command == null)
throw new ArgumentNullException(nameof(command));
if (command.Contains("?"))
throw new ArgumentException("command is a query: " + command);
if (!IsConnected)
throw new IOException("Not connected.");
lock (commandLock)
{
OnActivity();
try
{
Stopwatch timer = Stopwatch.StartNew();
LockRetry(() => WriteIEEEBlock(command, data));
timer.Stop();
if (VerboseLoggingEnabled)
Log.Debug(timer, "SCPI >> {0}", command);
if (QueryErrorAfterCommand)
{
WaitForOperationComplete();
queryErrors(noAllocation: true);
}
}
catch (VISAException ex)
{
if (ex.ErrorCode == Visa.VI_ERROR_CONN_LOST)
{
Log.Error("Connection lost");
IsConnected = false;
throw new IOException("Connection lost");
}
if (ex.ErrorCode == Visa.VI_ERROR_TMO)
{
Log.Error("Not responding (command '{0}' timed out)", command);
throw new IOException("Not responding");
}
Log.Error(ex);
}
}
}
///
/// Sends a IEEE Block SCPI command to the instrument with a Streaming interface for large data size. Uses DirectIO.
///
public virtual void ScpiIEEEBlockCommand(string command, Stream data, long maxSize = 0)
{
if (command == null)
throw new ArgumentNullException(nameof(command));
if (data == null)
throw new ArgumentNullException(nameof(data));
if (command.Contains("?"))
throw new ArgumentException("command is a query: " + command);
if (!IsConnected)
throw new IOException("Not connected.");
lock (commandLock)
{
OnActivity();
//TapThread.Sleep(); // Just giving the TestPlan a chance to abort if it has been requested to do so
try
{
//send SCPI ASCII header
long sendLength = data.Length - data.Position; //available data to send
if (!((sendLength > 0) && (maxSize > 0)))
{
Log.Debug("SCPI Write Command Not Sent: available length={0} max={1}", sendLength, maxSize);
}
else
{
Stopwatch timer = Stopwatch.StartNew();
if (TerminationCharacterEnabled == false)
{
SendEndEnabled = false;
}
if (sendLength > maxSize)
{
Log.Debug("SCPI Write Command truncated to maximum: available length={0} max={1}",
sendLength, maxSize);
sendLength = maxSize; //limit data to send to max
}
var sendLengthStr = string.Format("{0}", sendLength);
var commandBlk = string.Format("{0}#{1}{2}", command, sendLengthStr.Length, sendLengthStr);
byte[] commandB = Encoding.ASCII.GetBytes(commandBlk);
int retSentCnt = LockRetry(() => Write(commandB, commandB.Length));
if (retSentCnt != commandB.Length)
{
Log.Error("SCPI transmission incomplete, IO.Write return={0}, expect={1}", retSentCnt,
commandB.Length);
throw new IOException("SCPI block transmission incomplete");
}
//send binary data
int bufferSize = 0x10000;
byte[] buffer = new byte[bufferSize];
int readSize = 0;
int sendingSize = 0;
while (0 != (readSize = data.Read(buffer, 0, bufferSize)))
{
sendingSize = sendingSize + readSize;
if (sendingSize >= sendLength)
{
//send last segment before exceeding limit
readSize = readSize - (int)(sendingSize - sendLength); //remove overshoot
retSentCnt = LockRetry(() => Write(buffer, readSize));
if (retSentCnt != readSize)
{
Log.Error("SCPI transmission incomplete, IO.Write return={0}, expect={1}", retSentCnt,
readSize);
throw new IOException("SCPI block transmission incomplete");
}
break;
}
retSentCnt = LockRetry(() => Write(buffer, readSize));
if (retSentCnt != readSize)
{
Log.Error("SCPI transmission incomplete, IO.Write return={0}, expect={1}", retSentCnt,
readSize);
throw new IOException("SCPI block transmission incomplete");
}
}
if (TerminationCharacterEnabled == false)
{
SendEndEnabled = true;
}
LockRetry(() => Write(Encoding.ASCII.GetBytes("\n"), 1));
timer.Stop();
if (VerboseLoggingEnabled)
Log.Debug(timer, String.Format("SCPI >> {0}", command));
if (QueryErrorAfterCommand)
{
WaitForOperationComplete();
queryErrors(noAllocation: true);
}
}
}
catch (VISAException ex)
{
if (ex.ErrorCode == Visa.VI_ERROR_CONN_LOST)
{
Log.Error("Connection lost");
IsConnected = false;
throw new IOException("Connection lost");
}
if (ex.ErrorCode == Visa.VI_ERROR_TMO)
{
Log.Error("Not responding (command '{0}' timed out)", command);
throw new IOException("Not responding");
}
if (ex.Message.StartsWith("SCPI block transmission incomplete"))
{
throw;
}
Log.Error(ex);
}
finally
{
if (TerminationCharacterEnabled == false)
{
SendEndEnabled = true;
}
}
}
}
///
public ScpiIOResult EnableEvent(ScpiEvent eventType, ScpiEventMechanism mechanism)
{
if (scpiIO is IScpiIO3 scpiIO3)
{
return scpiIO3.EnableEvent(eventType, mechanism);
}
throw new NotSupportedException($"{scpiIO} does not support EnableEvent");
}
///
public ScpiIOResult DisableEvent(ScpiEvent eventType, ScpiEventMechanism mechanism)
{
if (scpiIO is IScpiIO3 scpiIO3)
{
return scpiIO3.DisableEvent(eventType, mechanism);
}
throw new NotSupportedException($"{scpiIO} does not support DisableEvent");
}
///
public ScpiIOResult WaitOnEvent(ScpiEvent eventType, int timeout, out ScpiEvent outEventType)
{
if (scpiIO is IScpiIO3 scpiIO3)
{
return scpiIO3.WaitOnEvent(eventType, timeout, out outEventType);
}
throw new NotSupportedException($"{scpiIO} does not support WaitOnEvent");
}
///
/// Polls the instrument for an event.
///
///
///
/// Usually used for error handling.
///
public bool PollStatusEvent()
{
return (LockRetry(() => DoReadSTB()) & 0x20) == 0x20;
}
///
/// Specifies a dictionary that will map specific error codes to log levels other than Error (the level to which typically logs error messages).
///
protected readonly Dictionary ScpiErrorsLogLevelOverrides = new Dictionary();
/// Returns all the errors on the instrument error stack. Clears the list in the same call.
/// better performance version of QueryErrors that does not use additional memory when no errors exists.
/// Dont emit log messages.
/// The maximal number of error messages to read.
/// Dont do any allocation. The result will always be empty.
///
IList queryErrors(bool suppressLogMessages = false, int maxErrors = 1000, bool noAllocation = false)
{
bool errorExists()
{
if (readStbSupported)
return (LockRetry(() => DoReadSTB()) & 0x4) != 0x00;
return true;
}
IList errors = Array.Empty();
while ( errorExists() && errors.Count < maxErrors)
{
ScpiError error = queryErrorParse();
if (error.Code == 0)
break;
LogEventType logLevel = LogEventType.Error;
if (ScpiErrorsLogLevelOverrides.ContainsKey(error.Code))
{
logLevel = ScpiErrorsLogLevelOverrides[error.Code];
}
if (!suppressLogMessages)
{
Log.Debug("SCPI >> SYSTem:ERRor?");
Log.TraceEvent(logLevel, 0, string.Format("SCPI << {0}", error));
}
if (!noAllocation)
{
if (errors.IsReadOnly)
errors = new List();
errors.Add(error);
}
}
return errors;
}
///
/// Returns all the errors on the instrument error stack. Clears the list in the same call.
///
/// if true the errors will not be logged.
/// The max number of errors to retrieve. Useful if instrument generates errors faster than they can be read.
///
public List QueryErrors(bool suppressLogMessages = false, int maxErrors = 1000)
{
lock (commandLock)
{
var errors = queryErrors(suppressLogMessages, maxErrors);
if (errors is List lst)
return lst;
return errors.ToList();
}
}
///
/// A SCPI error.
///
public struct ScpiError
{
///
/// Error code.
///
public int Code;
///
/// Error message.
///
public string Message;
///
/// Returns a string formatted the same way as the output of SYST:ERR?
///
///
public override string ToString()
{
return string.Format("{0},\"{1}\"", Code, Message);
}
}
private ScpiError queryErrorParse()
{
int errorCode = 0;
string error = String.Empty;
string errorStr = QueryErr(true).Trim();
Match regexMatch = Regex.Match(errorStr, "(?[\\-\\+0-9]+),\"(?.+)\"");
if (regexMatch.Success)
{
error = regexMatch.Groups["msg"].Value;
errorCode = int.Parse(regexMatch.Groups["code"].Value);
}
else
{
error = errorStr;
errorCode = 0;
}
return new ScpiError { Code = errorCode, Message = error };
}
///
/// Waits for a all previously executed SCPI commands to complete.
///
/// Maximum time to wait, default is 2 seconds. If value is less than IoTimeout, IoTimeout will be used.
public void WaitForOperationComplete(int timeoutMs = 2000)
{
int orgTimeout = IoTimeout;
if (orgTimeout < timeoutMs) // Don't decrease timeout
{
IoTimeout = timeoutMs;
}
try
{
QueryOpc();
}
finally
{
if (orgTimeout < timeoutMs) // Don't decrease timeout
{
IoTimeout = orgTimeout;
}
}
}
///
/// Aborts the currently running measurement and makes the default measurement active.
///
public void Reset()
{
CommandRst();
}
/// *IDN / Queries the instrument for a IDN string.
protected virtual string QueryIdn() => ScpiQuery("*IDN?");
/// *OPC / Operation Complete Query
protected virtual string QueryOpc() => ScpiQuery("*OPC?");
/// *RST / Reset Command
protected virtual void CommandRst() => ScpiCommand("*RST");
/// *CLS / Clear Status Command
protected virtual void CommandCls() => ScpiCommand("*CLS");
/// SYST:ERR? / Queries the instrument for errors.
/// This will normally be in a format like '123,"Error message"'.
protected virtual string QueryErr(bool isSilent = false) => ScpiQuery("SYST:ERR?", isSilent);
#region Service Request routines
///
/// A delegate that is used by the event.
///
/// A reference to the that the SRQ originated from.
public delegate void ScpiSRQDelegate(ScpiInstrument sender);
Dictionary srqDelegates = new Dictionary();
///
/// This event is called whenever a SRQ is generated by the instrument.
/// Adding a handler to this event will automatically enable SRQ transactions from the instrument when the instrument is opened/closed, or while the instrument is open.
///
/// To disable SRQ transactions all handlers added must be removed.
///
public event ScpiSRQDelegate SRQ
{
// SRQ event delegates are primarily handled by the internal scpi IO
// but the delegates needs to be converted into another kind to work.
// hence the dictionary custom stuff.
add
{
ScpiIOSrqDelegate delegate2 = x => value(this);
scpiIO.SRQ += delegate2;
srqDelegates[value] = delegate2;
}
remove
{
var del = srqDelegates[value];
scpiIO.SRQ -= del;
srqDelegates.Remove(value);
}
}
#endregion
}
}