// 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.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
namespace OpenTap
{
///
/// Utility class for SCPI communication.
///
public static class Scpi
{
///
/// Enum converter for converting Enums to SCPI strings. use the GetEnumConv to get a converter.
///
class ScpiEnum
{
// Remembers how to convert types.
static readonly ConcurrentDictionary converters = new ConcurrentDictionary();
/// Get an enum converter for a specific enum type.
/// Must be a enum type!
///
public static ScpiEnum GetEnumConv(Type t)
{
return converters.GetOrAdd(t, t2 => new ScpiEnum(t2));
}
readonly ImmutableDictionary convertBack;
readonly ImmutableDictionary convert;
/// String to Enum.
///
///
public Enum FromString(string input)
{
return convertBack[input];
}
///
/// Enum to string.
///
///
///
public string ToString(Enum item)
{
return convert[item];
}
ScpiEnum(Type type)
{
// collect enum values and string conversions.
var enumValues = Enum.GetValues(type);
var enum2String = new Dictionary();
var string2Enum = new Dictionary();
foreach (Enum val in enumValues)
{
var member = type.GetMember(val.ToString())[0];
var scpiName = member.GetCustomAttribute()?.ScpiString ?? val.ToString();
enum2String[val] = scpiName;
string2Enum[scpiName] = val;
}
convert = enum2String.ToImmutableDictionary();
convertBack = string2Enum.ToImmutableDictionary();
}
}
static string valueToScpiString(object propertyValue)
{
var type = propertyValue.GetType();
string value;
if (type == typeof(bool))
{
value = (bool)propertyValue ? "ON" : "OFF";
}
else if (type.IsEnum)
{
value = ScpiEnum.GetEnumConv(type).ToString((Enum)propertyValue);
}
else if (type.IsArray)
{
var Props = (Array)propertyValue;
string[] PropStrings = new string[Props.Length];
for (int i = 0; i < Props.Length; i++)
PropStrings[i] = valueToScpiString(Props.GetValue(i));
value = string.Join(",", PropStrings);
}
else
{
value = Convert.ToString(propertyValue, CultureInfo.InvariantCulture);
}
return value;
}
///
/// Similar to , but makes args SCPI compatible. Bools are ON/OFF formatted. Enum values uses .
/// Arrays will have their elements formatted and separated by commas if available; if not they are converted using .
///
///
///
///
public static string Format(string command, params object[] args)
{
if (command == null)
throw new ArgumentNullException("command");
if (args == null)
throw new ArgumentNullException("args");
string[] stringArgs = new string[args.Length];
for (int i = 0; i < args.Length; i++)
{
stringArgs[i] = args[i] == null ? "" : valueToScpiString(args[i]);
}
return string.Format(command, stringArgs);
}
///
/// Extension method that checks whether a given char is a IEEE488.2 whitespace (7.4.1.2).
///
private static bool IsScpiWhitespace(this char c)
{
return (
((((int)c) >= 0) && (((int)c) <= 9)) ||
((((int)c) >= 11) && (((int)c) <= 32))
);
}
private static int FindStringEnd(string Resp, int Start)
{
int stop = Resp.IndexOf("\"", Start + 1);
if (stop < 0)
return -1;
// Skip inserted quotes
while ((stop < (Resp.Length - 1)) && (Resp.Substring(stop, 2) == "\"\""))
{
stop = Resp.IndexOf("\"", stop + 2);
if (stop < 0)
return -1;
}
return stop;
}
///
/// Splits a string into a list of valid separated SCPI response data strings. Needed because String.Split(resp, ',') is not tolerant to " characters.
///
private static List SplitScpiArray(string resp, bool ThrowIfInvalid = false)
{
List result = new List();
int start = 0;
int stop = 0;
while (start < (resp.Length - 1))
{
// Trim whitespace from the start of the string
char c = resp[start];
while (c.IsScpiWhitespace())
c = resp[++start];
stop = start;
// Test if the next character is a string quote
if (c == '"')
{
stop = FindStringEnd(resp, stop);
if (stop < 0)
{
if (ThrowIfInvalid) throw new Exception(string.Format("Unterminated SCPI response: '{0}'", resp));
stop = resp.Length;
}
else
stop++;
result.Add(resp.Substring(start, stop - start));
stop = resp.IndexOf(",", stop);
if (stop < 0)
stop = resp.Length;
start = stop + 1;
}
else
{
stop = resp.IndexOf(",", start);
if (stop < 0)
stop = resp.Length;
result.Add(resp.Substring(start, stop - start));
start = stop + 1;
}
}
if (start < resp.Length)
result.Add(resp.Substring(start));
return result;
}
///
/// Overloaded. Parses the result of a SCPI query back to T, with special parsing for enums, bools and arrays. Bools support 1/0 and ON/OFF formats.
/// If Enums are tagged with , will be used instead of .
///
///
///
///
public static object Parse(string scpiString, Type T)
{
if (scpiString == null)
throw new ArgumentNullException("scpiString");
if (T == null)
throw new ArgumentNullException("T");
scpiString = scpiString.Trim(); // Ensure no garbage.
if (T == typeof(bool))
{
return scpiString == "ON" || scpiString == "1";
}
else if (T.IsEnum)
{
return (object)ScpiEnum
.GetEnumConv(T)
.FromString(scpiString);
}
else if (T.IsArray)
{
List elements = SplitScpiArray(scpiString);
Array result = Array.CreateInstance(T.GetElementType(), elements.Count);
for (int i = 0; i < elements.Count; i++)
result.SetValue(Parse(elements[i], T.GetElementType()), i);
return result;
}
return Convert.ChangeType(scpiString, T, CultureInfo.InvariantCulture);
}
///
/// Parses the result of a SCPI query back to T. Special parsing for enums, bools and arrays. bools supports 1/0 and ON/OFF formats.
/// If Enums are tagged with will be used instead of .
///
///
///
///
public static T Parse(string scpiString)
{
return (T)Parse(scpiString, typeof(T));
}
///
/// .
///
///
///
///
///
static string getUnescapedScpi(object src, string ScpiString, PropertyInfo property)
{
if (ScpiString.Contains("%"))
{
string value;
object propertyValue = property.GetValue(src, null);
if (propertyValue == null)
return ScpiString;
value = valueToScpiString(propertyValue);
return ScpiString.Replace("%", value);
}
if (property.PropertyType == typeof(bool) && ScpiString.Contains("|"))
{
Regex rx = new Regex(".* (?[^|]+)\\|(?[^\\s]+)");
Match m = rx.Match(ScpiString);
if (m.Success)
{
bool value = (bool)property.GetValue(src, null);
if (value == true)
return ScpiString.Replace("|" + m.Groups["false"], "");
else
return ScpiString.Replace(m.Groups["true"] + "|", "");
}
}
return ScpiString;
}
///
/// Parses one or more items of 'property', replacing '%' with the value of the property given after formatting.
/// Note that 'property' must be a property with the , and 'src' is the object containing 'property', not the value of the property.
/// If property.PropertyType is bool, then from the value 'A|B' A is selected if true, and B is selected if false.
///
public static string[] GetUnescapedScpi(object src, PropertyInfo property)
{
if (property == null)
throw new ArgumentNullException("property");
var scpiStrings = property.GetCustomAttributes().Select(scpiAttr => scpiAttr.ScpiString);
return scpiStrings.Select(str => getUnescapedScpi(src, str, property)).ToArray();
}
}
}