using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Reflection; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp; using PepperDash.Core; using PepperDash.Essentials.Core.Config; using Newtonsoft.Json; using Serilog.Events; namespace PepperDash.Essentials.Core { public static class JoinMapHelper { /// /// Attempts to get the serialized join map from config /// /// /// /// /// GetSerializedJoinMapForDevice method /// public static string GetSerializedJoinMapForDevice(string joinMapKey) { if (string.IsNullOrEmpty(joinMapKey)) return null; var joinMap = ConfigReader.ConfigObject.JoinMaps[joinMapKey]; return joinMap.ToString(); } /// /// Attempts to get the serialized join map from config /// /// /// /// /// GetJoinMapForDevice method /// public static string GetJoinMapForDevice(string joinMapKey) { return GetSerializedJoinMapForDevice(joinMapKey); } /// /// Attempts to find a custom join map by key and returns it deserialized if found /// /// /// public static Dictionary TryGetJoinMapAdvancedForDevice(string joinMapKey) { try { if (string.IsNullOrEmpty(joinMapKey)) return null; if (!ConfigReader.ConfigObject.JoinMaps.ContainsKey(joinMapKey)) { Debug.LogMessage(LogEventLevel.Verbose, "No Join Map found in config with key: '{0}'", joinMapKey); return null; } Debug.LogMessage(LogEventLevel.Verbose, "Attempting to load custom join map with key: {0}", joinMapKey); var joinMapJToken = ConfigReader.ConfigObject.JoinMaps[joinMapKey]; if (joinMapJToken == null) return null; var joinMapData = joinMapJToken.ToObject>(); return joinMapData; } catch (Exception e) { Debug.LogMessage(LogEventLevel.Verbose, "Error getting join map for key: '{0}'. Error: {1}", joinMapKey, e); return null; } } } /// /// Base class for join maps /// public abstract class JoinMapBaseAdvanced { protected uint JoinOffset; /// /// The collection of joins and associated metadata /// public Dictionary Joins { get; private set; } protected JoinMapBaseAdvanced(uint joinStart) { Joins = new Dictionary(); JoinOffset = joinStart - 1; } protected JoinMapBaseAdvanced(uint joinStart, Type type):this(joinStart) { AddJoins(type); } protected void AddJoins(Type type) { var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance) .Where(f => f.IsDefined(typeof (JoinNameAttribute), true)).ToList(); Debug.LogMessage(LogEventLevel.Debug, "Got {fields} with JoinNameAttribute", fields.Count); foreach (var field in fields) { var childClass = Convert.ChangeType(this, type, null); //this here is JoinMapBaseAdvanced, not the child class. JoinMapBaseAdvanced has no fields. if (!(field.GetValue(childClass) is JoinDataComplete value)) { Debug.LogMessage(LogEventLevel.Information, "Unable to cast base class to {0}", type.Name); continue; } value.SetJoinOffset(JoinOffset); var joinName = value.GetNameAttribute(field); if (string.IsNullOrEmpty(joinName)) continue; Joins.Add(joinName, value); } if (Debug.Level > 0) { PrintJoinMapInfo(); } } /// /// PrintJoinMapInfo method /// public void PrintJoinMapInfo() { var sb = JoinmapStringBuilder(); CrestronConsole.ConsoleCommandResponse(sb.ToString()); } private StringBuilder JoinmapStringBuilder() { var sb = new StringBuilder(); var lineEnding = "\r\n"; var digitals = Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Digital)) .ToDictionary(j => j.Key, j => j.Value); var analogs = Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Analog)) .ToDictionary(j => j.Key, j => j.Value); var serials = Joins.Where(j => j.Value.Metadata.JoinType.HasFlag(eJoinType.Serial)) .ToDictionary(j => j.Key, j => j.Value); Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Digital join count {digitalCount} Analog join count {analogCount} Serial join count {serialCount}", null, digitals.Count, analogs.Count, serials.Count); // Get the joins of each type and print them sb.Append($"# {GetType().Name}\r\n"); sb.Append(lineEnding); sb.Append($"## Digitals{lineEnding}"); sb.Append(lineEnding); // Get the joins of each type and print them var digitalSb = AppendJoinList(GetSortedJoins(digitals)); digitalSb.Append($"## Analogs{lineEnding}"); digitalSb.Append(lineEnding); var analogSb = AppendJoinList(GetSortedJoins(analogs)); analogSb.Append($"## Serials{lineEnding}"); analogSb.Append(lineEnding); var serialSb = AppendJoinList(GetSortedJoins(serials)); sb.EnsureCapacity(sb.Length + digitalSb.Length + analogSb.Length + serialSb.Length); sb.Append(digitalSb).Append(analogSb).Append(serialSb); return sb; } /// /// MarkdownJoinMapInfo method /// public void MarkdownJoinMapInfo(string deviceKey, string bridgeKey) { var pluginType = GetType().Name; CrestronConsole.ConsoleCommandResponse("{0}:\n", pluginType); WriteJoinmapMarkdown(JoinmapStringBuilder(), pluginType, bridgeKey, deviceKey); } private static void WriteJoinmapMarkdown(StringBuilder stringBuilder, string pluginType, string bridgeKey, string deviceKey) { var fileName = string.Format("{0}{1}{2}__{3}__{4}.md", Global.FilePathPrefix, "joinMaps/", pluginType, bridgeKey, deviceKey); using (var sw = new StreamWriter(fileName)) { sw.WriteLine(stringBuilder.ToString()); CrestronConsole.ConsoleCommandResponse("Joinmap Readme generated and written to {0}", fileName); } } /// /// Returns a sorted list by JoinNumber /// /// /// static List> GetSortedJoins(Dictionary joins) { var sortedJoins = joins.ToList(); sortedJoins.Sort((pair1, pair2) => pair1.Value.JoinNumber.CompareTo(pair2.Value.JoinNumber)); return sortedJoins; } static StringBuilder AppendJoinList(List> joins) { var sb = new StringBuilder(); const string stringFormatter = "| {0} | {1} | {2} | {3} | {4} |\r\n"; const int joinNumberLen = 11; const int joinSpanLen = 9; const int typeLen = 19; const int capabilitiesLen = 12; var descriptionLen = (from @join in joins select @join.Value into j select j.Metadata.Description.Length).Concat(new[] {11}).Max(); //build header sb.Append(string.Format(stringFormatter, string.Format("Join Number").PadRight(joinNumberLen, ' '), string.Format("Join Span").PadRight(joinSpanLen, ' '), string.Format("Description").PadRight(descriptionLen, ' '), string.Format("Type").PadRight(typeLen, ' '), string.Format("Capabilities").PadRight(capabilitiesLen, ' '))); //build table seperator sb.Append(string.Format(stringFormatter, new string('-', joinNumberLen), new string('-', joinSpanLen), new string('-', descriptionLen), new string('-', typeLen), new string('-', capabilitiesLen))); foreach (var join in joins) { sb.Append(join.Value.GetMarkdownFormattedData(stringFormatter, descriptionLen)); } sb.Append("\r\n"); return sb; } /// /// Attempts to find the matching key for the custom join and if found overwrites the default JoinData with the custom /// /// /// /// SetCustomJoinData method /// public void SetCustomJoinData(Dictionary joinData) { foreach (var customJoinData in joinData) { JoinDataComplete join; if (!Joins.TryGetValue(customJoinData.Key, out join)) { Debug.LogMessage(LogEventLevel.Verbose, "No matching key found in join map for: '{0}'", customJoinData.Key); continue; } if (join != null) { join.SetCustomJoinData(customJoinData.Value); } } PrintJoinMapInfo(); } /// /// Returns the join number for the join with the specified key /// /// /// //public uint GetJoinForKey(string key) //{ // return Joins.ContainsKey(key) ? Joins[key].JoinNumber : 0; //} /// /// Returns the join span for the join with the specified key /// /// /// //public uint GetJoinSpanForKey(string key) //{ // return Joins.ContainsKey(key) ? Joins[key].JoinSpan : 0; //} } /// /// Read = Provides feedback to SIMPL /// Write = Responds to sig values from SIMPL /// [Flags] public enum eJoinCapabilities { None = 0, ToSIMPL = 1, FromSIMPL = 2, ToFromSIMPL = ToSIMPL | FromSIMPL, ToFusion = 4, FromFusion = 8, ToFromFusion = ToFusion | FromFusion, } [Flags] /// /// Enumeration of eJoinType values /// public enum eJoinType { None = 0, Digital = 1, Analog = 2, Serial = 4, DigitalAnalog = Digital | Analog, DigitalSerial = Digital | Serial, AnalogSerial = Analog | Serial, DigitalAnalogSerial = Digital | Analog | Serial, } /// /// Represents a JoinMetadata /// public class JoinMetadata { private string _description; /// /// A description for the join to better describe its usage /// [JsonProperty("description")] public string Description { get { return _description; } set { _description = value; } } /// /// Signal type(s) /// [JsonProperty("joinType")] /// /// Gets or sets the JoinType /// public eJoinType JoinType { get; set; } /// /// Indicates whether the join is read and/or write /// [JsonProperty("joinCapabilities")] /// /// Gets or sets the JoinCapabilities /// public eJoinCapabilities JoinCapabilities { get; set; } /// /// Indicates a set of valid values (particularly if this translates to an enum /// [JsonProperty("validValues")] /// /// Gets or sets the ValidValues /// public string[] ValidValues { get; set; } } /// /// Data describing the join. Can be overridden from configuratino /// public class JoinData { /// /// Join number (based on join offset value) /// [JsonProperty("joinNumber")] public uint JoinNumber { get; set; } /// /// Join range span. If join indicates the start of a range of joins, this indicated the maximum number of joins in the range /// [JsonProperty("joinSpan")] public uint JoinSpan { get; set; } /// /// Fusion Attribute Name (optional) /// [JsonProperty("attributeName")] /// /// Gets or sets the AttributeName /// public string AttributeName { get; set; } } /// /// A class to aggregate the JoinData and JoinMetadata for a join /// public class JoinDataComplete { private uint _joinOffset; private JoinData _data; public JoinMetadata Metadata { get; set; } /// /// To store some future information as you please /// public object UserObject { get; private set; } public JoinDataComplete(JoinData data, JoinMetadata metadata) { _data = data; Metadata = metadata; } public string GetMarkdownFormattedData(string stringFormatter, int descriptionLen) { //Fixed Width Headers var joinNumberLen = string.Format("Join Number").Length; var joinSpanLen = string.Format("Join Span").Length; var typeLen = string.Format("AnalogDigitalSerial").Length; var capabilitiesLen = string.Format("ToFromFusion").Length; //Track which one failed, if it did const string placeholder = "unknown"; var dataArray = new Dictionary { {"joinNumber", placeholder.PadRight(joinNumberLen, ' ')}, {"joinSpan", placeholder.PadRight(joinSpanLen, ' ')}, {"description", placeholder.PadRight(descriptionLen, ' ')}, {"joinType", placeholder.PadRight(typeLen, ' ')}, {"capabilities", placeholder.PadRight(capabilitiesLen, ' ')} }; try { dataArray["joinNumber"] = string.Format("{0}", JoinNumber.ToString(CultureInfo.InvariantCulture).ReplaceIfNullOrEmpty(placeholder)).PadRight(joinNumberLen, ' '); dataArray["joinSpan"] = string.Format("{0}", JoinSpan.ToString(CultureInfo.InvariantCulture).ReplaceIfNullOrEmpty(placeholder)).PadRight(joinSpanLen, ' '); dataArray["description"] = string.Format("{0}", Metadata.Description.ReplaceIfNullOrEmpty(placeholder)).PadRight(descriptionLen, ' '); dataArray["joinType"] = string.Format("{0}", Metadata.JoinType.ToString().ReplaceIfNullOrEmpty(placeholder)).PadRight(typeLen, ' '); dataArray["capabilities"] = string.Format("{0}", Metadata.JoinCapabilities.ToString().ReplaceIfNullOrEmpty(placeholder)).PadRight(capabilitiesLen, ' '); return string.Format(stringFormatter, dataArray["joinNumber"], dataArray["joinSpan"], dataArray["description"], dataArray["joinType"], dataArray["capabilities"]); } catch (Exception e) { //Don't Throw - we don't want to kill the system if this falls over - it's not mission critical. Print the error, use placeholder data var errorKey = string.Empty; foreach (var item in dataArray) { if (item.Value.TrimEnd() == placeholder) continue; errorKey = item.Key; break; } Debug.LogMessage(LogEventLevel.Information, "Unable to decode join metadata {1}- {0}", e.Message, !string.IsNullOrEmpty(errorKey) ? (' ' + errorKey) : string.Empty); return string.Format(stringFormatter, dataArray["joinNumber"], dataArray["joinSpan"], dataArray["description"], dataArray["joinType"], dataArray["capabilities"]); } } /// /// Sets the join offset value /// /// /// /// SetJoinOffset method /// public void SetJoinOffset(uint joinOffset) { _joinOffset = joinOffset; } /// /// The join number (including the offset) /// public uint JoinNumber { get { return _data.JoinNumber+ _joinOffset; } set { _data.JoinNumber = value; } } public uint JoinSpan { get { return _data.JoinSpan; } } public string AttributeName { get { return _data.AttributeName; } } /// /// SetCustomJoinData method /// public void SetCustomJoinData(JoinData customJoinData) { _data = customJoinData; } /// /// GetNameAttribute method /// public string GetNameAttribute(MemberInfo memberInfo) { var name = string.Empty; var attribute = (JoinNameAttribute)Attribute.GetCustomAttribute(memberInfo, typeof(JoinNameAttribute)); if (attribute == null) return name; name = attribute.Name; Debug.LogMessage(LogEventLevel.Verbose, "JoinName Attribute value: {0}", name); return name; } } [AttributeUsage(AttributeTargets.All)] /// /// Represents a JoinNameAttribute /// public class JoinNameAttribute : Attribute { private string _Name; public JoinNameAttribute(string name) { Debug.LogMessage(LogEventLevel.Verbose, "Setting Attribute Name: {0}",null, name); _Name = name; } public string Name { get { return _Name; } } } }