//----------------------------------------------------------------------- // // 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 ObdSimulator { using System; using System.Collections.Generic; using System.IO.Ports; using System.Threading; using System.Windows.Forms; using global::FtdiApi; using global::ObdSimulator.Protocol; using global::SharedObjects; using global::SharedObjects.Protocol; /// /// OBD Simulator for testing OBD tool. /// public partial class ObdSerialSimulator : UserControl, IObdSimulator, IObdCommunicator, IDataLogging { /// /// Byte pairs that are answers on five baud init request. /// public static readonly byte[][] EXPECT_BYTE_PAIRS = new byte[][] { new byte[] { (byte)0x08, (byte)0x08 }, new byte[] { (byte)0x94, (byte)0x94 }, new byte[] { (byte)0x8f, (byte)0xe9 }, new byte[] { (byte)0x8f, (byte)0x6b }, new byte[] { (byte)0x8f, (byte)0x6d }, new byte[] { (byte)0x8f, (byte)0xef }, }; /// /// Gets or sets time in milliseconds between sent frames. /// Ignored by this class for now, present due to requirement by interface. /// public long frameSpacing { get; set; } /// /// Current COM port. /// public string comPort { get; set; } /// /// Current COM port speed. /// public int speed { get; set; } /// /// FTDI API instance. /// private FTDI ftdi; /// /// Serial device instance. /// private SimpleSerialDevice simpleSerialDevice = null; /// /// Selected FTDI port instance. /// private FT_DEVICE_INFO_NODE selectedFtdiPort = null; /// /// Base time reference. /// private long t0 = 0L; /// /// State indicating that client has connected. /// private bool connected = false; /// /// Connection processing passed first stage. /// private bool firstStage = false; /// /// Connection processing passed second stage. /// private bool secondStage = false; /// /// Connection processing passed third stage. /// private bool thirdStage = false; /// /// Index value for selected protocol. /// private int protocolIndex = 0; /// /// Expected incoming byte. /// private byte expectByte = 0; /// /// Data parser instance. /// private IDataParser dataParser = null; /// /// 'true' if checksum shall be expected on commands. /// private bool expectChecksum = true; /// /// Set checksum on response data. /// private bool setChecksumFlag = true; /// /// Message counter. /// private int rowCounter = 0; /// /// Rows collection for datagridview. /// private DataGridViewRowCollection rows; /// /// List of data receivers. /// private IList iSessionLayerList = new List(); /// /// Event logging instance. /// private ILogging iLogging; /// /// Initializes a new instance of the class. /// public ObdSerialSimulator(ILogging iLogging) { this.iLogging = iLogging; this.ftdi = new FTDI(); this.InitializeComponent(); this.speed = 10400; this.rows = this.trafficDGV.Rows; resizeDataColumn(this.trafficDGV); this.refreshPorts(null); this.timer1.Start(); this.t0 = DateTime.Now.Ticks; this.comboBox1.SelectedIndex = this.protocolIndex; this.checksumCB.Checked = this.expectChecksum; this.setChecksumCheckBox.Checked = this.setChecksumFlag; } /// /// Unregister as a data receiver instance /// /// Data receiver instance. public void removeDataReceiver(ISessionLayer iSessionLayer) { this.iLogging.appendTextLn("Removed session layer: " + iSessionLayer.ToString()); if (this.iSessionLayerList.Contains(iSessionLayer)) { this.iSessionLayerList.Remove(iSessionLayer); } } /// /// Register as a data receiver instance /// /// Data receiver instance. public void addDataReceiver(ISessionLayer iSessionLayer) { this.iLogging.appendTextLn("Added session layer: " + iSessionLayer.ToString()); if (!this.iSessionLayerList.Contains(iSessionLayer)) { this.iSessionLayerList.Add(iSessionLayer); } } /// /// Start CAN communication. /// public void startConnection() { this.openConnection(); } /// /// Stop CAN communication. /// public void stopConnection() { this.close(); } /// /// Receive data. /// /// Data byte. /// Delta time between packets. public void rxData(byte data, long delta) { this.t0 = DateTime.Now.Ticks; if (!this.connected) { this.iLogging.appendText("connected == False\r\n"); if (this.protocolIndex == 0 || this.protocolIndex == 1) { if (!this.thirdStage) { if (data == 0x00) { this.iLogging.appendText("Data == 0x00\r\n"); if (!this.firstStage) { this.firstStage = true; this.iLogging.appendText("firstStage == " + this.firstStage + "\r\n"); } else { if (!this.secondStage) { if (delta >= 550 && delta <= 670) { this.secondStage = true; this.iLogging.appendText("secondStage == " + this.secondStage + "\r\n"); } } else { if (delta >= 730 && delta <= 880) { this.thirdStage = true; Thread.Sleep(1000); this.simpleSerialDevice.write(new byte[] { 0x55 }); Thread.Sleep(10); int ix = 0; // ISO9141 if (this.protocolIndex == 1) { ix = 2; // KWP2000 } this.simpleSerialDevice.write(EXPECT_BYTE_PAIRS[ix]); this.expectByte = (byte)(~EXPECT_BYTE_PAIRS[ix][1] & 0xff); this.iLogging.appendText("thirdStage == " + this.thirdStage + "\r\n"); } } } } else { this.firstStage = false; this.secondStage = false; this.thirdStage = false; } } else { if (data == this.expectByte) { // Final ACK. byte[] ca = new byte[] { (byte)(0xcc) }; this.simpleSerialDevice.write(ca); this.firstStage = false; this.secondStage = false; this.thirdStage = false; this.connected = true; this.setImage(); this.iLogging.appendText("connected=" + this.connected + "\r\n"); this.dataParser = new DataParser(this.iLogging, this, this.iSessionLayerList, this.expectChecksum); } } } } else { this.iLogging.appendText(" >>> 0x" + data.ToString("x2") + "\r\n"); if (this.dataParser != null) { this.dataParser.parse(data); } } } /// /// Send data. /// /// Target address. /// Data to send. public void send(uint outAddress, byte[] payload) { switch (this.protocolIndex) { case 0: sendIso9141(payload); break; case 1: case 2: break; } } /// /// Send data according to ISO 14230 format. /// /// private void sendIso14230(byte[] payload) { if (payload.Length < 64) { byte[] data = new byte[payload.Length + 3 + (this.setChecksumFlag ? 1 : 0)]; data[0] = 0xC0; data[1] = 0xF1; data[2] = 0x33; data[0] |= (byte)(payload.Length & 0xFF); Array.Copy(payload, 0, data, 3, payload.Length); if (this.setChecksumFlag) { setChecksum(data); } this.simpleSerialDevice.write(data); } else { byte[] data = new byte[payload.Length + 4 + (this.setChecksumFlag ? 1 : 0)]; data[0] = 0xC0; data[1] = 0xF1; data[2] = 0x33; data[3] = (byte)(payload.Length & 0xFF); Array.Copy(payload, 0, data, 4, payload.Length); if (this.setChecksumFlag) { setChecksum(data); } this.simpleSerialDevice.write(data); } } /// /// Send data according to ISO9141 format. /// /// private void sendIso9141(byte[] payload) { byte[] data = new byte[payload.Length + 3 + (this.setChecksumFlag ? 1 : 0)]; data[0] = 0x48; data[1] = 0x6B; data[2] = 0x10; Array.Copy(payload, 0, data, 3, payload.Length); if (this.setChecksumFlag) { setChecksum(data); } this.simpleSerialDevice.write(data); } /// /// Set the checksum byte on the data. /// /// Data to set checksum byte on. private static void setChecksum(byte[] data) { byte cs = 0; for (int i = 0; i < data.Length - 1; i++) { cs += data[i]; } data[data.Length - 1] = cs; } /// /// Append one row to the data grid view. /// /// Text indicating data direction. /// Data to display. public void appendRow(string direction, byte[] data) { if (this.InvokeRequired) { try { this.Invoke(new appendRowFunc(this.appendRow), new object[] { direction, data }); } catch (ThreadAbortException) { throw; } catch (System.Reflection.TargetParameterCountException ex) { MessageBox.Show("appendText: " + ex.Message + "\r\n"); } catch (System.ObjectDisposedException) { // Ignore. } catch (Exception ex) { MessageBox.Show("appendText: " + ex.GetType().ToString() + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } } else { // Only keep the 1000 last entries. while (this.rows.Count > 1000) { this.rows.RemoveAt(0); } int n = this.rows.Add(); DataGridViewRow row = this.rows[n]; DataGridViewCellCollection cells = row.Cells; cells["traffic_count"].Value = this.rowCounter.ToString(); cells["traffic_direction"].Value = direction; string dataStr = string.Empty; if (data != null) { foreach (byte ch in data) { if (!string.IsNullOrEmpty(dataStr)) { dataStr += " "; } dataStr += ch.ToString("x2"); } } cells["traffic_data"].Value = dataStr; this.rowCounter++; } } /// /// Calculate and update size of data column. /// /// Data grid view to update column size on. private static void resizeDataColumn(DataGridView dataGridView) { int n = dataGridView.Width; n -= 60; n -= dataGridView.Columns["traffic_count"].Width; n -= dataGridView.Columns["traffic_direction"].Width; dataGridView.Columns["traffic_data"].Width = n; } /// /// Set image indicating connection status. /// private delegate void setImageFunc(); /// /// Append one row to the data grid view. /// /// Text indicating data direction. /// Data to display. private delegate void appendRowFunc(string direction, byte[] data); /// /// Set image indicating connection status. /// private void setImage() { if (this.InvokeRequired) { try { this.Invoke(new setImageFunc(this.setImage), new object[] { }); } catch (ThreadAbortException) { throw; } catch (System.Reflection.TargetParameterCountException ex) { MessageBox.Show("appendText: " + ex.Message + "\r\n"); } catch (System.ObjectDisposedException) { // Ignore. } catch (Exception ex) { MessageBox.Show("appendText: " + ex.GetType().ToString() + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } } else { if (this.connected) { this.pictureBox1.Image = global::ObdSimulator.Properties.Resources.connect_green; } else { this.pictureBox1.Image = global::ObdSimulator.Properties.Resources.disconnect; } } } /// /// Handle changed size of window. /// /// Sending object. /// Event data. private void dataGridView1_SizeChanged(object sender, EventArgs e) { DataGridView dataGridView = (DataGridView)sender; resizeDataColumn(dataGridView); } /// /// Refresh ports combo box. /// /// Port name. private delegate void refreshPortsFunc(string actionPort); /// /// Refresh ports combo box. /// /// Port name. private void refreshPorts(string actionPort) { if (this.InvokeRequired) { try { this.Invoke(new refreshPortsFunc(this.refreshPorts), new object[] { actionPort }); } catch (ThreadAbortException) { throw; } catch (System.Reflection.TargetParameterCountException ex) { MessageBox.Show("appendText: " + ex.Message + "\r\n"); } catch (System.ObjectDisposedException) { // Ignore. } catch (Exception ex) { MessageBox.Show("appendText: " + ex.GetType().ToString() + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } } else { if (this.portCB.Enabled) { this.portCB.Items.Clear(); int i = 0; int sel = -1; foreach (string portName in SerialPort.GetPortNames()) { this.portCB.Items.Add(portName); if (portName == actionPort) { sel = i; this.iLogging.appendText("'" + portName + "'='" + actionPort + "': " + (portName == actionPort) + ", sel=" + sel + "\r\n"); } i++; } try { FT_DEVICE_INFO_NODE[] devices = SimpleSerialDevice.ftdiDevices(); if (devices != null) { foreach (FT_DEVICE_INFO_NODE device in devices) { this.iLogging.appendText("device: " + device + "\r\n"); this.portCB.Items.Add(device); } } } catch (Exception ex) { this.iLogging.appendText("refreshPorts: " + ex.GetType().ToString() + "\r\n" + ex.Message + "\r\n" + "\r\n" + ex.StackTrace); } if (sel >= 0) { this.portCB.SelectedIndex = sel; } } } } /// /// Append event logging text. /// /// Text to append. private delegate void appendTextFunc(string text); /// /// Handle click on Open button. /// /// Sending object. /// Event data. private void openButton_Click(object sender, EventArgs e) { this.speed = Convert.ToInt32(this.speedCb.Text); openConnection(); } /// /// Open serial port connection. /// private void openConnection() { if (this.comPort != null) { this.iLogging.appendText("Opening port '" + this.comPort + "', speed=" + this.speed + "\r\n"); this.simpleSerialDevice = new SimpleSerialDevice(this.ftdi, this.comPort, this.selectedFtdiPort, this.speed, this.iLogging, this); this.simpleSerialDevice.open(); this.portCB.Enabled = false; this.openButton.Enabled = false; this.closeButton.Enabled = true; } else { this.iLogging.appendText("No port selected.\r\n"); } } /// /// Handle change of selected port combo box. /// /// Sending object. /// Event data. private void portCB_SelectedIndexChanged(object sender, EventArgs e) { if (this.portCB.SelectedIndex >= 0) { if (this.portCB.Items[this.portCB.SelectedIndex] is FT_DEVICE_INFO_NODE) { this.selectedFtdiPort = (FT_DEVICE_INFO_NODE)this.portCB.Items[this.portCB.SelectedIndex]; } else { this.selectedFtdiPort = null; } this.comPort = this.portCB.Text; } else { this.comPort = null; } } /// /// Handle click on Close button. /// /// Sending object. /// Event data. private void closeButton_Click(object sender, EventArgs e) { this.close(); } /// /// Close port. /// private void close() { this.iLogging.appendText("\r\nClosing... "); try { if (this.simpleSerialDevice != null) { this.simpleSerialDevice.close(); } } catch (Exception ex) { MessageBox.Show("close: " + ex.GetType().ToString() + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } finally { this.iLogging.appendText("\r\nClosed!\r\n"); this.closeButton.Enabled = false; this.portCB.Enabled = true; this.openButton.Enabled = true; } } /// /// Handle timer tick to detect connection timeout. /// /// Sending object. /// Event data. private void timer1_Tick(object sender, EventArgs e) { if (this.connected && (((DateTime.Now.Ticks - this.t0) / TimeSpan.TicksPerMillisecond) > 5000)) { this.connected = false; this.firstStage = false; this.secondStage = false; this.thirdStage = false; this.setImage(); this.iLogging.appendText("Timeout: connected=" + this.connected + ", firstStage=" + this.firstStage + ", secondStage=" + this.secondStage + ", thirdStage=" + this.thirdStage + "\r\n"); } } /// /// Handle change of protocol. /// /// Sending object. /// Event data. private void comboBox1_SelectedIndexChanged(object sender, EventArgs e) { this.protocolIndex = this.comboBox1.SelectedIndex; } /// /// Handle click on Tx button for sending test data. /// /// Sending object. /// Event data. private void txButton_Click(object sender, EventArgs e) { this.simpleSerialDevice.write(new byte[] { (byte)0x55 }); } /// /// Handle click on Clear button for event log window. /// /// Sending object. /// Event data. private void button1_Click(object sender, EventArgs e) { this.trafficDGV.Rows.Clear(); } /// /// Toggle the expect checksum flag. /// /// Sending object. /// Event data. private void checksumCB_CheckedChanged(object sender, EventArgs e) { this.expectChecksum = this.checksumCB.Checked; } /// /// Checkbox for sending checksum changed. /// /// Sending object. /// Event data. private void setChecksumCheckBox_CheckedChanged(object sender, EventArgs e) { this.setChecksumFlag = this.setChecksumCheckBox.Checked; } } }