//----------------------------------------------------------------------- // // Copyright © 2012 Nils Hammar. All rights reserved. // //----------------------------------------------------------------------- /* * Software to access vehicle information via the OBD-II connector. * * Copyright © 2012 Nils Hammar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Alternative licensing is possible, see the licensing document. * * The above text may not be removed or modified. */ namespace UserInterface.GUI.OBD { using System; using System.Collections.Generic; using System.Drawing; using System.Threading; using System.Windows.Forms; using global::Protocol; using global::Protocol.OBD; using global::SharedObjects; using global::SharedObjects.DataMgmt; using global::SharedObjects.GUI; using global::SharedObjects.Misc; using global::SharedObjects.Protocol; /// /// Class for OBD data selection. /// public partial class ObdDataPanel : AbstractUserControl, IObdDataPanel { /// /// Position of ECU info column. /// private int ecuInfoColumnPos = 0; /// /// Data source instance. /// private IDataSource iDataSource; /// /// Indicate if FreezeFrame panel or Normal panel. /// private uint expectedMode; /// /// List of source addresses (ECUs) /// private List sourceAddresses = new List(); /// /// Columns in the data grid view. /// private DataGridViewColumnCollection cols; /// /// Rows in the data grid view. /// private DataGridViewRowCollection rows; /// /// Mode directory of PID groups. /// private SortedDictionary> modeData = new SortedDictionary>(); /// /// Array of known ECUs for vehicle. /// private List ecus; /// /// Icon indicating plot is possible. /// private Image iconPlot; /// /// Icon indicating plot is not possible. /// private Image iconNoplot; /// /// Data file directory path. /// private string dataFileDir; /// /// Messaging interface instance. /// private IMessaging iMessaging; /// /// Known ECU addresses. /// private Dictionary ecuAddressList = new Dictionary(); /// /// Row that was last clicked. /// private int clickRow = -1; /// /// Column that was last clicked. /// private int clickCol = -1; /// /// Initializes a new instance of the class. /// public ObdDataPanel() { this.InitializeComponent(); this.cols = this.dataGridView1.Columns; this.rows = this.dataGridView1.Rows; this.dataGridView1.SortCompare += new DataGridViewSortCompareEventHandler(this.dataGridView1_SortCompare); this.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); } /// /// Initializes the panel. /// /// Logging instance. /// Messaging instance. /// Application tree instance. /// Expected Mode byte value to handle. /// Data source interface instance. /// Vehicle instance. /// Data file directory path. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "5", Justification = "Reviewed, intentional.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "iMessaging", Justification = "Reviewed, intentional.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "expectedMode", Justification = "Reviewed, intentional.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "dataFileDir", Justification = "Reviewed, intentional.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "iLogging", Justification = "Reviewed, intentional.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "iDataSource", Justification = "Reviewed, intentional.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "freezeFrame", Justification = "Reviewed, intentional.")] public void init( ILogging iLogging, IMessaging iMessaging, IApplicationTree applicationTree, uint expectedMode, IDataSource iDataSource, XmlClass.vehicle vehicle, string dataFileDir) { this.iLogging = iLogging; this.iMessaging = iMessaging; this.expectedMode = expectedMode; this.iDataSource = iDataSource; this.ecus = vehicle.ecus; this.dataFileDir = dataFileDir; this.setApplicationTree(applicationTree); this.iconPlot = this.getImage("chart_curve.png"); this.iconNoplot = new Bitmap(16, 16); this.refreshModeData(vehicle); this.col_plotgroup.Items.Add("-"); this.col_plotgroup.Items.Add("1"); this.col_plotgroup.Items.Add("2"); this.col_plotgroup.Items.Add("3"); this.col_plotgroup.Items.Add("4"); this.col_plotgroup.Items.Add("5"); this.col_plotgroup.Items.Add("6"); this.col_plotgroup.Items.Add("7"); this.col_plotgroup.Items.Add("8"); this.col_plotgroup.Items.Add("9"); } /// /// Refresh the ModeData dictionary. /// /// Current vehicle instance. public void refreshModeData(XmlClass.vehicle vehicle) { if (vehicle != null) { this.modeData = new SortedDictionary>(); foreach (XmlClass.vehicle.pidgroup vehiclePidGroup in vehicle.pidgroups) { foreach (XmlClass.pidgroup pidGroup in this.iDataSource.restrictedPidgroups) { if (pidGroup.id == vehiclePidGroup.id && !pidGroup.hasMap) { SortedDictionary pidList = this.getPidList(pidGroup.mode_int); foreach (XmlClass.pidgroup.pidlist pid in pidGroup.pids) { if (!pidList.ContainsKey(pid.pid_int)) { PidItemWrapper pidItemWrapper = new PidItemWrapper(pidGroup.mode_int, pidGroup.mode_int, pid, true); pidList.Add(pid.pid_int, pidItemWrapper); } } } } } } } /// /// Clear the current table. /// public void clear() { this.rows.Clear(); } /// /// Add one data item. /// /// Source address. /// Mode byte value. /// PID value. /// Bitmap mask for active items. public void addDataItem(uint srcAddress, byte mode0, uint pid, int bitmap) { byte mode = (byte)(mode0 & 0xbf); if (mode == this.expectedMode) { byte searchMode = mode; if (mode == 0x02) { searchMode = 0x01; } if (!this.sourceAddresses.Contains(srcAddress)) { this.addColumn(srcAddress); this.ecuInfoColumnPos++; this.sourceAddresses.Add(srcAddress); } int srcAddressIndex = this.sourceAddresses.IndexOf(srcAddress); SortedDictionary pidList = this.getPidList(mode); for (int i = 0; i < 31; i++) { bool enable = (((bitmap >> (31 - i)) & 0x01) == 1); if (enable) { uint pidNum = (uint)(pid + i + 1); XmlClass.pidgroup.pidlist item = this.findPidItem(searchMode, pidNum); if (item != null) { PidItemWrapper pidItemWrapper; if (pidList.ContainsKey(item.pid_int)) { pidItemWrapper = pidList[item.pid_int]; } else { pidItemWrapper = new PidItemWrapper(mode, searchMode, item, false); pidList.Add(item.pid_int, pidItemWrapper); } if (!pidItemWrapper.checkedColumns.Contains(srcAddressIndex)) { pidItemWrapper.checkedColumns.Add(srcAddressIndex); } } } } } } /// /// Display the data fields. /// public void displayFields() { if (this.dataGridView1.InvokeRequired) { try { this.Invoke(new displayFieldsFunc(this.displayFields), new object[] { }); } catch (ThreadAbortException) { throw; } catch (System.Reflection.TargetParameterCountException ex) { MessageBox.Show( "Exception: " + ex.Message + "\r\n", "Error", MessageBoxButtons.OK, MessageBoxIcon.Stop); } catch (System.ObjectDisposedException) { // Ignore. } } else { this.rows.Clear(); foreach (KeyValuePair> kvp in this.modeData) { foreach (KeyValuePair kvp2 in kvp.Value) { PidItemWrapper pidItemWrapper = kvp2.Value; int rowNum = this.rows.Add(); DataGridViewRow row = this.rows[rowNum]; DataGridViewCellCollection cells = row.Cells; cells["col_item"].Value = pidItemWrapper; cells["col_mode"].Value = "0x" + kvp.Key.ToString("x2"); cells["col_pid"].Value = "0x" + pidItemWrapper.pidItem.pid_int.ToString("x2"); cells["col_freq"].Value = string.IsNullOrEmpty(pidItemWrapper.pidItem.refresh) ? "1:1" : pidItemWrapper.pidItem.refresh; cells["col_description"].Value = pidItemWrapper.pidItem.name; cells["col_plottable"].Value = pidItemWrapper.pidItem.canPlot ? this.iconPlot : this.iconNoplot; for (int i = 0; i < this.ecuInfoColumnPos; i++) { bool flg = pidItemWrapper.checkedColumns.Contains(i); cells["ecu_" + i].ReadOnly = !(flg || pidItemWrapper.defaultChecked); if (!cells["ecu_" + i].ReadOnly) { cells["ecu_" + i].ContextMenuStrip = this.contextMenuStrip1; } if (pidItemWrapper.defaultChecked) { cells["ecu_" + i].Style.BackColor = Color.Yellow; } else { cells["ecu_" + i].Style.BackColor = flg ? Color.LightGreen : Color.Gray; } } cells["col_plotgroup"].Value = "-"; } } } } /// /// Get list of selected items. /// /// List of selected items. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Reviewed.")] public IList getSelectedItems() { List items = new List(); foreach (DataGridViewRow row in this.rows) { DataGridViewCellCollection cells = row.Cells; if (cells != null) { List addrList = new List(); foreach (int ecuCol in this.ecuAddressList.Keys) { if (!cells["ecu_" + ecuCol].ReadOnly) { if (cells["ecu_" + ecuCol] != null && cells["ecu_" + ecuCol].Value != null && (bool)cells["ecu_" + ecuCol].Value) { addrList.Add(this.ecuAddressList[ecuCol]); } } } if (addrList.Count > 0) { string plotgroup = (string)cells["col_plotgroup"].Value; int plotgroupInt = 0; try { plotgroupInt = Convert.ToInt32(plotgroup); } catch { } PidItemWrapper pidItemWrapper = (PidItemWrapper)cells["col_item"].Value; pidItemWrapper.addrList = addrList; pidItemWrapper.plotGroup = plotgroupInt; items.Add(pidItemWrapper); } } } return items; } /// /// Restore saved data. /// /// Items to be checked at restore. public void selectItems(IList restoredItems) { if (restoredItems != null) { Dictionary colMapper = new Dictionary(); foreach (KeyValuePair kvp in this.ecuAddressList) { colMapper.Add(kvp.Value, kvp.Key); } foreach (DataGridViewRow row in this.rows) { DataGridViewCellCollection cells = row.Cells; cells["col_plotgroup"].Value = "-"; foreach (int col in this.ecuAddressList.Keys) { cells["ecu_" + col].Value = false; } PidItemWrapper pidItemWrapper = (PidItemWrapper)cells["col_item"].Value; foreach (XmlClass.selectionModePid item in restoredItems) { if (pidItemWrapper.pidMode == item.mode_int && pidItemWrapper.pidItem.pid_int == item.pid_int) { foreach (XmlClass.selectionModePid.addressItem addr in item.addrList) { if (colMapper.ContainsKey(addr.address)) { int col = colMapper[addr.address]; if (item.plotGroup > 0) { cells["col_plotgroup"].Value = item.plotGroup.ToString(); } cells["ecu_" + col].Value = true; } } } } } } } /// /// Construct a new column. /// /// Column header text. /// Name of column. /// DataGridViewColumn instance. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Reviewed, intentional.")] private static DataGridViewColumn getColumn(string header, string name) { DataGridViewCellStyle style = new DataGridViewCellStyle(); style.Alignment = DataGridViewContentAlignment.MiddleCenter; style.BackColor = Color.LightGray; DataGridViewColumn newCol = new System.Windows.Forms.DataGridViewCheckBoxColumn(); newCol.Width = 60; newCol.HeaderText = header; newCol.ReadOnly = true; newCol.Name = name; newCol.Visible = true; newCol.SortMode = DataGridViewColumnSortMode.Automatic; return newCol; } /// /// Get one image. /// /// Name of icon file. /// Image loaded from file. private Image getImage(string filename) { Image img = Image.FromFile(this.dataFileDir + "Icons\\" + filename); return img; } /// /// Display the data fields. /// private delegate void displayFieldsFunc(); /// /// Add one column for an ECU. /// /// ECU address. private delegate void addColumnFunc(uint srcAddress); /// /// Add one column for an ECU. /// /// ECU address. private void addColumn(uint srcAddress) { if (this.dataGridView1.InvokeRequired) { try { this.Invoke(new addColumnFunc(this.addColumn), new object[] { srcAddress }); } catch (ThreadAbortException) { throw; } catch (System.Reflection.TargetParameterCountException ex) { MessageBox.Show( "Exception: " + ex.Message + "\r\n", "Error", MessageBoxButtons.OK, MessageBoxIcon.Stop); } catch (System.ObjectDisposedException) { // Ignore. } } else { string ecuName = "ECU"; string addressStr = string.Empty; uint mask; if (srcAddress < 0x800 && srcAddress > 0xff) { mask = 0x07; addressStr = srcAddress.ToString("x4"); } else { mask = 0xff; addressStr = ((srcAddress >> 16) & 0xffff).ToString("x4") + " " + (srcAddress & 0xffff).ToString("x4"); } foreach (XmlClass.vehicle.ecuData ecu in this.ecus) { uint id = Utils.hexParse(ecu.id); if ((id & mask) == (srcAddress & mask)) { ecuName = ecu.name; break; } } uint? destinationAddress = DataRequester.getDestinationAddress(this.iMessaging.protocolHandler, srcAddress); if (destinationAddress != null) { this.ecuAddressList.Add(this.ecuInfoColumnPos, (uint)destinationAddress); } this.cols.Insert(5 + this.ecuInfoColumnPos, getColumn(ecuName + " 0x" + addressStr, "ecu_" + this.ecuInfoColumnPos)); this.calcColumnSize(); } } /// /// Get the PID list for the given mode. /// /// Mode to get list for. /// PID list. private SortedDictionary getPidList(uint mode) { SortedDictionary pidList; if (this.modeData.ContainsKey(mode)) { pidList = this.modeData[mode]; } else { pidList = new SortedDictionary(); this.modeData.Add(mode, pidList); } return pidList; } /// /// Find item in pid group list. /// /// Mode for item. /// PID number for item. /// PID Item. private XmlClass.pidgroup.pidlist findPidItem(byte mode, uint pidNum) { XmlClass.pidgroup.pidlist item = null; foreach (XmlClass.pidgroup pidgroup in this.iDataSource.restrictedPidgroups) { if (pidgroup.mode_int == (uint)mode) { foreach (XmlClass.pidgroup.pidlist pidItem in pidgroup.pids) { if (pidItem.pid_int == pidNum) { item = pidItem; break; } } if (item != null) { break; } } } return item; } /// /// Handle change of display size. /// /// Sending object. /// Event data. private void dataGridView1_SizeChanged(object sender, EventArgs e) { this.calcColumnSize(); } /// /// Recalculate the column size for the Description field. /// private void calcColumnSize() { int w0 = this.dataGridView1.Width - 60; foreach (DataGridViewColumn column in this.cols) { if (column.Visible) { w0 -= column.Width; } } w0 += this.cols["col_description"].Width; if (w0 < 100) { w0 = 100; } this.cols["col_description"].Width = w0; } /// /// Handle data error event. /// /// Sending object. /// Event data. private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs e) { if (MessageBox.Show( "col=" + e.ColumnIndex + ", row=" + e.RowIndex + "\r\n" + e.Exception.Message + "\r\n" + "Context=" + e.Context + "\r\n" + e.Exception.StackTrace, "Error", MessageBoxButtons.OKCancel, MessageBoxIcon.Stop) == DialogResult.Cancel) { e.ThrowException = true; } if (e.Exception.InnerException != null) { MessageBox.Show( "InnerException=" + e.Exception.InnerException.Message + "\r\n" + e.Exception.InnerException.StackTrace, "Error", MessageBoxButtons.OK, MessageBoxIcon.Stop); } e.Cancel = true; } /// /// Compare two rows for sorting on image. /// /// Sending object. /// Event data. private void dataGridView1_SortCompare(object sender, DataGridViewSortCompareEventArgs e) { try { switch (e.Column.Name) { case "col_mode": this.modeColumnComparer(e); break; case "col_plottable": this.plottableColumnComparer(e); break; default: if (e.Column.Name.StartsWith("ecu_")) { this.ecuColumnComparer(e); } break; } } catch (Exception ex) { this.iLogging.appendText(ex.GetType().ToString() + ": " + ex.Message + "\r\n" + ex.StackTrace + "\r\n"); } } /// /// Compare ECU column rows. /// /// Event data. private void ecuColumnComparer(DataGridViewSortCompareEventArgs e) { int res = 0; int col = e.Column.Index; int row1 = e.RowIndex1; int row2 = e.RowIndex2; if (!this.dataGridView1[col, row1].ReadOnly && this.dataGridView1[col, row2].ReadOnly) { res = -1; } else { if (this.dataGridView1[col, row1].ReadOnly && !this.dataGridView1[col, row2].ReadOnly) { res = 1; } else { if (e.CellValue1 != null && e.CellValue2 != null) { if (((bool)e.CellValue1) && !((bool)e.CellValue2)) { res = -1; } else { if (!((bool)e.CellValue1) && ((bool)e.CellValue2)) { res = 1; } } } else { if (e.CellValue1 != null && e.CellValue2 == null) { res = -1; } else { if (e.CellValue1 == null && e.CellValue2 != null) { res = 1; } } } } } // If equal then sort by mode&pid. if (res == 0) { res = this.modeColumnComparer(e); } e.SortResult = res; e.Handled = true; } /// /// Compare for the plottable column. /// This compares images by comparing their hashcode since /// we are just interested in getting them grouped. /// /// Event data. private void plottableColumnComparer(DataGridViewSortCompareEventArgs e) { int res = 0; if (e.CellValue1 != null && e.CellValue2 != null) { int h1 = e.CellValue1.GetHashCode(); int h2 = e.CellValue2.GetHashCode(); if (h1 > h2) { res = 1; } else { if (h1 < h2) { res = -1; } } } else { if (e.CellValue1 != null && e.CellValue2 == null) { res = 1; } else { if (e.CellValue1 == null && e.CellValue2 != null) { res = -1; } } } // If equal then sort by mode&pid. if (res == 0) { res = this.modeColumnComparer(e); } e.SortResult = res; e.Handled = true; } /// /// Compare Mode and PID columns to sort first on Mode and then on PID. /// /// Event data. /// Result of compare. private int modeColumnComparer(DataGridViewSortCompareEventArgs e) { int res = 0; DataGridViewRow row1 = this.dataGridView1.Rows[e.RowIndex1]; DataGridViewRow row2 = this.dataGridView1.Rows[e.RowIndex2]; string s1 = (string)row1.Cells["col_mode"].Value; string s2 = (string)row2.Cells["col_mode"].Value; uint i1 = Utils.hexParse(s1); uint i2 = Utils.hexParse(s2); res = ((i1 == i2) ? 0 : ((i1 > i2) ? 1 : -1)); if (res == 0) { string s1a = (string)row1.Cells["col_pid"].Value; string s2a = (string)row2.Cells["col_pid"].Value; uint i1a = Utils.hexParse(s1a); uint i2a = Utils.hexParse(s2a); res = ((i1a == i2a) ? 0 : ((i1a > i2a) ? 1 : -1)); } e.SortResult = res; e.Handled = true; return res; } /// /// Hide cells with readonly checkboxes for ECUs. /// /// Sending object. /// Event data. private void dataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { if (e.ColumnIndex >= 0 && e.RowIndex >= 0) { DataGridView dataGridView = (DataGridView)sender; DataGridViewColumn dataGridViewColumn = dataGridView.Columns[e.ColumnIndex]; if (dataGridViewColumn.Name.StartsWith("ecu_")) { DataGridViewCell dataGridViewCell = dataGridView[e.ColumnIndex, e.RowIndex]; if (dataGridViewCell.ReadOnly) { e.PaintBackground(e.ClipBounds, true); e.Handled = true; } } } } /// /// Select/unselect all columns from current column and onwards. /// /// Sending object. /// Event data. private void selectRightToolStripMenuItem_Click(object sender, EventArgs e) { DataGridViewCell dataGridViewCell = this.dataGridView1[this.clickCol, this.clickRow]; if (!dataGridViewCell.ReadOnly) { bool chkVal = dataGridViewCell.Value != null ? (bool)dataGridViewCell.Value : false; bool newVal = !chkVal; foreach (int ecuCol in this.ecuAddressList.Keys) { int i = ecuCol + 5; if (i >= this.clickCol) { if (!this.dataGridView1[i, this.clickRow].ReadOnly) { this.dataGridView1[i, this.clickRow].Value = newVal; } } } } } /// /// Select/unselect all rows from current row and down of the same type for the given column. /// /// Sending object. /// Event data. private void selectColumnDownToolStripMenuItem_Click(object sender, EventArgs e) { DataGridViewCell dataGridViewCell = this.dataGridView1[this.clickCol, this.clickRow]; object plotVal = this.dataGridView1.Rows[this.clickRow].Cells["col_plottable"].Value; bool plottable = (plotVal != null && this.iconPlot.Equals(plotVal)); if (!dataGridViewCell.ReadOnly) { bool chkVal = dataGridViewCell.Value != null ? (bool)dataGridViewCell.Value : false; bool newVal = !chkVal; int n = this.dataGridView1.Rows.Count; for (int i = this.clickRow; i < n; i++) { object plotVal1 = this.dataGridView1.Rows[i].Cells["col_plottable"].Value; bool plottable1 = (plotVal1 != null && this.iconPlot.Equals(plotVal1)); if (!this.dataGridView1[this.clickCol, i].ReadOnly && plottable1 == plottable) { this.dataGridView1[this.clickCol, i].Value = newVal; } } } } /// /// Save last cell where a mouse click occurred. /// /// This is used for the context menu actions. /// /// /// Sending object. /// Event data. private void dataGridView1_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e) { this.clickRow = e.RowIndex; this.clickCol = e.ColumnIndex; } } }