// 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(); } } }