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