//-----------------------------------------------------------------------
//
// 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 SimProtocol
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SharedObjects;
using SharedObjects.Protocol;
///
/// Session layer for CAN communication.
///
/// This class manages communication with a single peer (the test tool),
/// and is not able to handle multiple multi-frame communications.
///
///
public class CanSessionLayer : AbstractSessionLayer
{
///
/// Number of cycles we may get a timeout waiting for CTS when doing multi-frame before sending anyway.
///
private const int TIMEOUT_CYCLES = 10;
///
/// The block size to use when doing multi-frame communication.
///
/// Block size 1 means that every frame needs to be acked.
///
///
private const byte BLOCK_SIZE = 0x01;
///
/// The frame spacing to use in milliseconds between frames sent.
///
/// 20ms by default, change if needed.
///
///
private const byte FRAME_SPACING = 0x14;
///
/// Event logging instance.
///
private ILogging iLogging = null;
///
/// CAN bus communication engine.
///
private IObdCommunicator iObdCommunicator;
///
/// Instance of multi-frame data.
///
private MultiFrameData multiFrameData;
///
/// Default functional address.
///
private uint funcAddr;
///
/// Default physical address.
///
private uint physAddr;
///
/// Presentation layer instance.
///
private IList presentationLayerList = new List();
///
/// Simulation engine instance.
///
private ISimEngine iSimEngine;
///
/// Queue for sending of data.
///
private Queue sendQueue = new Queue();
///
/// Flag for send thread.
///
private bool doRun = false;
///
/// Number of frames before next CTS.
///
/// Only applicable for multi-frame.
///
///
private uint blockSize = 1;
///
/// Separation time between data frames.
///
private uint separationTime = 4;
///
/// Object to lock with when sending.
///
private object lockObject = new object();
///
/// Thread for transmission of data.
///
private Thread sendThread = null;
///
/// Common Simulator preferences.
///
private IObdCanSimPrefs iObdCanSimPrefs;
///
/// Number of frames received before sending next CTS.
///
/// Only applicable for multi-frame.
///
///
private uint rxBlockSize = 1;
///
/// Initializes a new instance of the class.
///
/// Event log appender instance.
/// Common Simulator preferences.
/// Functional address.
/// Physical address.
public CanSessionLayer(ILogging iLogging, IObdCanSimPrefs iObdCanSimPrefs, uint funcAddr, uint physAddr)
{
this.iLogging = iLogging;
this.iObdCanSimPrefs = iObdCanSimPrefs;
this.funcAddr = funcAddr;
this.physAddr = physAddr;
this.doRun = true;
this.sendThread = new Thread(this.sendThreadImpl);
this.sendThread.Name = "0x" + physAddr.ToString("X2");
this.sendThread.Start();
}
///
/// Set the simulation engine.
///
/// Simulation engine instance.
public override void setSimEngine(ISimEngine iSimEngine)
{
this.iSimEngine = iSimEngine;
}
///
/// Clean up.
///
public override void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Add presentation layer instance.
///
/// Presentation layer to add
public override void addPresentationLayer(IPresentationLayerToSession iPresentationLayer)
{
if (!this.presentationLayerList.Contains(iPresentationLayer))
{
this.presentationLayerList.Add(iPresentationLayer);
}
}
///
/// Remove presentation layer instance.
///
/// Presentation layer to remove.
public override void removePresentationLayer(IPresentationLayerToSession iPresentationLayer)
{
if (this.presentationLayerList.Contains(iPresentationLayer))
{
this.presentationLayerList.Remove(iPresentationLayer);
}
}
///
/// Set communicator instance.
///
/// Communicator instance.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "iObdCommunicator", Justification = "Reviewed, intentional")]
public override void setObdCommunicator(IObdCommunicator iObdCommunicator)
{
if (iObdCommunicator == null && this.iObdCommunicator != null)
{
this.iObdCommunicator.removeDataReceiver(this);
}
this.iObdCommunicator = iObdCommunicator;
if (iObdCommunicator != null)
{
this.iObdCommunicator.addDataReceiver(this);
}
}
///
/// Enqueue data to send on CAN.
///
/// Target address.
/// Payload data.
/// Payload data length.
public override void sendData(uint outAddress, IList response, uint dataLen)
{
if (response != null)
{
if (this.iSimEngine != null)
{
this.iSimEngine.addRow(rowMode.session_out, outAddress, 0, 0, 0, Utils.listToHexString(response));
}
if (response.Count <= 7)
{
response.Insert(0, (byte)dataLen); // Insert length for single frame data.
lock (this.sendQueue)
{
this.queueData(outAddress, 0x00, response);
Monitor.PulseAll(this.sendQueue);
}
}
else
{
uint maskedDataLen = dataLen & 0x0fff;
this.iLogging.appendTextLn("Datalen=" + maskedDataLen);
if (dataLen == maskedDataLen)
{
byte firstByte = (byte)((maskedDataLen >> 8) & 0x0f);
byte secondByte = (byte)(maskedDataLen & 0xff);
byte frameType = 0x01;
firstByte |= (byte)((frameType << 4) & 0xf0); // First Frame.
response.Insert(0, firstByte); // Insert length for multi frame data.
response.Insert(1, secondByte); // Insert length for multi frame data.
int responseLength = response.Count;
int partNumber = 0;
// Split up into parts and queue them.
IList nextPart = response;
lock (this.sendQueue)
{
while (nextPart.Count > 0)
{
int partSize = nextPart.Count < 8 ? nextPart.Count : 8;
List currentPart = new List(nextPart.Take(partSize));
this.queueData(outAddress, frameType, currentPart);
partNumber++;
frameType = 0x02;
nextPart = new List(nextPart.Skip(partSize));
if (nextPart.Count > 0)
{
byte cfData = (byte)((byte)((frameType << 4) & 0xf0) | (partNumber & 0x0f));
nextPart.Insert(0, cfData);
}
}
Monitor.PulseAll(this.sendQueue);
}
}
else
{
this.iLogging.appendTextLn("Trying to send more data than protocol supports.");
}
}
}
}
///
/// Receive one data packet.
///
/// Address value.
/// Payload data.
/// Flags value.
/// RES1 Value.
/// RES2 Value.
public override void receiveData(uint id, byte[] data0, ushort flags, ulong res1, ulong res2)
{
if (data0 != null)
{
uint testerAddr1;
bool physicalAddress;
if (this.matchAddress(id, out testerAddr1, out physicalAddress))
{
uint outAddress = this.getRespAddr(testerAddr1);
if (this.iSimEngine != null)
{
this.iSimEngine.addRow(rowMode.transport_in, id, 0x00, 0, 0, Utils.arrayToString(data0, 0) + ", flags=0x" + flags.ToString("X2") + ", res1=0x" + res1.ToString("X2") + ", res2=0x" + res2.ToString("X2"));
}
if (data0.Length >= 3)
{
uint p0 = 0;
uint zLen = (uint)data0[p0++]; // Only length in some cases, but works for now.
uint frameType = (zLen >> 4) & 0x0f;
if (physicalAddress)
{
switch (frameType)
{
case 0:
this.multiFrameData = null; // Discard any old data.
this.singleFrameHandler(id, outAddress, zLen, data0);
break;
case 1:
this.multiFrameData = new MultiFrameData(zLen, data0, p0);
this.sendFlowCTS(outAddress);
break;
case 2:
this.multiFrameHandler(outAddress, id, zLen, data0, p0);
break;
case 3: // We got a flow control frame, let's see what we shall do about it.
this.handleFcFrame(zLen, data0, p0);
break;
}
}
else
{
switch (frameType)
{
case 0:
this.multiFrameData = null; // Discard any old data.
this.singleFrameHandler(id, outAddress, zLen, data0);
break;
default:
this.iLogging.appendText("Got incorrect frame type [" + frameType + "] for functional address.");
break;
}
}
}
else
{
if (this.iLogging != null)
{
this.iLogging.appendTextLn("Received less than 2 bytes of data, ignoring.");
}
}
}
}
}
///
/// Clean up.
///
/// 'true' if disposing.
protected virtual void Dispose(bool disposing)
{
this.doRun = false;
this.sendThread.Abort();
}
///
/// Put data into queue.
///
/// Destination address.
/// Frame type value.
/// Payload data.
private void queueData(uint outAddress, byte frameType, IList response)
{
byte[] rspBa = this.prepareResponse(response);
QueueItem queueItem = new QueueItem(outAddress, frameType, rspBa);
this.sendQueue.Enqueue(queueItem);
}
///
/// Thread for sending on CAN.
///
private void sendThreadImpl()
{
long t0 = DateTime.Now.Ticks;
// Used to detect "stuck" situations.
int timeoutCounter = 0;
while (this.doRun)
{
QueueItem queueItem = null;
lock (this.sendQueue)
{
if (this.sendQueue.Count == 0)
{
if (!Monitor.Wait(this.sendQueue, 4000) && this.sendQueue.Count > 0)
{
this.iLogging.appendTextLn("SessionLayer.sendThreadImpl(): Timeout with data present.");
}
}
if (this.sendQueue.Count > 0)
{
queueItem = this.sendQueue.First();
if (timeoutCounter > TIMEOUT_CYCLES)
{
this.iLogging.appendTextLn(TIMEOUT_CYCLES + " cycles of " + this.separationTime + "ms waiting for CTS, sending anyway.");
}
if ((queueItem.frameType == 0x00 || queueItem.frameType == 0x01 || queueItem.frameType == 0x03)
|| ((queueItem.frameType == 0x02) && this.blockSize > 0)
|| (timeoutCounter > TIMEOUT_CYCLES))
{
if (queueItem.frameType == 0x01)
{
this.blockSize = 1;
}
queueItem = this.sendQueue.Dequeue();
timeoutCounter = 0;
if (this.blockSize > 0 && (queueItem.frameType == 0x01 || queueItem.frameType == 0x02))
{
this.blockSize--;
}
}
else
{
queueItem = null;
}
}
}
if (queueItem != null)
{
long t1 = DateTime.Now.Ticks;
long n = this.separationTime - ((t1 - t0) / TimeSpan.TicksPerMillisecond);
if (n > 0)
{
Thread.Sleep((int)n);
}
t0 = t1;
uint outAddress = queueItem.outAddress;
byte[] payload = queueItem.payload;
this.sendDataCan(outAddress, payload);
}
else
{
if (this.sendQueue.Count > 0)
{
timeoutCounter++;
}
else
{
timeoutCounter = 0;
}
if (this.separationTime == 0)
{
Thread.Sleep(30);
}
else
{
Thread.Sleep((int)this.separationTime);
}
}
}
}
///
/// Do a direct send on CAN with the given data.
///
/// Destination address.
/// Payload data.
private void sendDataCan(uint outAddress, byte[] payload)
{
lock (this.lockObject)
{
if (this.iSimEngine != null)
{
this.iSimEngine.addRow(rowMode.transport_out, outAddress, 0x00, 0, 0, Utils.arrayToString(payload, 0));
}
uint a2 = outAddress;
if (this.iObdCommunicator != null)
{
this.iObdCommunicator.send(outAddress, payload);
}
}
}
///
/// Send CTS to caller, tell it that we can take one frame at a time.
///
/// Response address.
private void sendFlowCTS(uint outAddress)
{
this.rxBlockSize = this.iObdCanSimPrefs.blockSize;
List response = new List();
response.Add(0x30); // CTS.
response.Add(this.iObdCanSimPrefs.blockSize);
response.Add(this.iObdCanSimPrefs.waitTime);
// Send flow control messages right away instead of queueing them.
this.sendDataCan(outAddress, this.prepareResponse(response));
}
///
/// Send Abort to caller.
///
/// Response address.
private void sendFlowAbort(uint outAddress)
{
this.rxBlockSize = this.iObdCanSimPrefs.blockSize;
List response = new List();
response.Add(0x32); // Abort.
response.Add(this.iObdCanSimPrefs.blockSize);
response.Add(this.iObdCanSimPrefs.waitTime);
// Send flow control messages right away instead of queueing them.
this.sendDataCan(outAddress, this.prepareResponse(response));
}
///
/// Prepare the response to a byte array from a list of bytes.
///
/// List of bytes.
/// Byte array.
private byte[] prepareResponse(IList response)
{
if (this.iSimEngine != null && this.iSimEngine.padData)
{
while (response.Count < 8)
{
response.Add(0x00);
}
}
byte[] rspBa = response.ToArray();
return rspBa;
}
///
/// Get (generate) response address.
///
/// Address part of test tool.
/// Response address.
private uint getRespAddr(uint testerAddr)
{
if (testerAddr == 0)
{
return this.physAddr | 0x08;
}
return (this.physAddr & 0xFFFF0000) | ((testerAddr & 0xff) << 8) | ((this.physAddr & 0xFF00) >> 8);
}
///
/// Check if received address matches the addresses of this engine.
///
/// Incoming address to test.
/// Out: Tester part of the address.
/// Out: If physical address did match.
/// 'true' if matching address.
private bool matchAddress(uint id, out uint testerAddr, out bool physicalAddress)
{
physicalAddress = id == this.physAddr;
testerAddr = 0;
if (this.funcAddr <= 0x7FF)
{
return id == this.funcAddr || physicalAddress;
}
testerAddr = id & 0xFF;
physicalAddress = (id & 0x18DFFF00) == (this.physAddr & 0x18DFFF00);
return ((id & 0x18DFFF00) == (this.funcAddr & 0x18DFFF00)) || physicalAddress;
}
///
/// Handle one Flow Control frame.
///
/// Byte containing flag state.
/// Payload data.
/// Payload data position.
private void handleFcFrame(uint zLen, byte[] data0, uint p0)
{
uint fcFlag = zLen & 0x0f;
switch (fcFlag)
{
case 0: // Clear to send.
lock (this.sendQueue)
{
this.blockSize = data0[p0++];
this.separationTime = data0[p0++];
if (this.blockSize == 0)
{
this.sendQueue.Clear();
this.iLogging.appendTextLn("Peer indicated BlockSize of zero - cancelling transfer. (See parameter ISO15765_BS on peer)");
}
else
{
Monitor.PulseAll(this.sendQueue);
}
}
break;
case 1: // Wait.
this.blockSize = 0;
break;
case 2: // Overflow/Abort.
lock (this.sendQueue)
{
this.sendQueue.Clear();
this.blockSize = 1;
}
break;
default:
break;
}
}
///
/// Handle continuation of a multi-frame message.
///
/// Response address (for handshake).
/// Target address.
/// Data length.
/// Payload data
/// Position of data in frame.
private void multiFrameHandler(uint outAddress, uint id, uint zLen, byte[] data0, uint p0)
{
if (this.multiFrameData != null)
{
this.multiFrameData.addData(zLen & 0x0f, data0, p0);
if (this.multiFrameData.error)
{
this.iLogging.appendTextLn("Multiframe data error, discarding rest.");
this.multiFrameData = null;
this.sendFlowAbort(outAddress);
}
else
{
if (this.multiFrameData.complete)
{
this.invokePresentation(id, outAddress, this.multiFrameData.length, this.multiFrameData.data);
}
else
{
this.rxBlockSize--;
if (this.rxBlockSize <= 0)
{
this.sendFlowCTS(outAddress);
}
}
}
}
}
///
/// Handle arrival of a single frame.
///
/// Functional addressing only permits single frames.
///
///
/// Target address.
/// Tester address part.
/// Data length.
/// Payload data
private void singleFrameHandler(uint id, uint outAddress, uint zLen, byte[] data0)
{
byte[] data = new byte[zLen];
Array.Copy(data0, 1, data, 0, zLen);
this.invokePresentation(id, outAddress, zLen, data);
}
///
/// Invoke the presentation layer.
///
/// Target address.
/// Tester address part.
/// Data length.
/// Payload data.
private void invokePresentation(uint id, uint outAddress, uint zLen, byte[] data)
{
string inData = Utils.arrayToString(data, 0);
if (this.iSimEngine != null)
{
this.iSimEngine.addRow(rowMode.session_in, id, 0, 0, zLen, inData);
}
foreach (IPresentationLayerToSession presentationLayer in this.presentationLayerList)
{
presentationLayer.processPayload(id, outAddress, zLen, data);
}
}
///
/// One item in send queue.
///
private class QueueItem
{
///
/// Gets destination address.
///
public uint outAddress { get; private set; }
///
/// Gets frame type, 0=Single, 1=First, 2=Consecutive, 3=Flow.
///
/// Flow frames shall usually not be present in queue.
///
///
public byte frameType { get; private set; }
///
/// Gets payload data.
///
public byte[] payload { get; private set; }
///
/// Initializes a new instance of the class.
///
/// Destination address.
/// Frame type.
/// Payload data.
public QueueItem(uint outAddress, byte frameType, byte[] payload)
{
this.outAddress = outAddress;
this.frameType = frameType;
this.payload = payload;
}
}
}
}