//-----------------------------------------------------------------------
//
// 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 SharedObjects.CAN.Objects
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using global::SharedObjects;
using global::SharedObjects.CAN;
using global::SharedObjects.DataMgmt;
using global::SharedObjects.Misc;
///
/// Session layer for CAN communication.
///
/// This class manages communication with multiple peers, which means that
/// it has to consider multi-frame communication with more than one peer
/// at the same time.
///
///
public class SessionLayer : IDataReceiver, ISessionLayerToPresentation, IDisposable
{
///
/// 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 ICanCommunicator iCanCommunicator;
///
/// Instance of multi-frame data.
///
private Dictionary multiFrameDataDictionary = new Dictionary();
///
/// Default functional address.
///
private uint addressMask;
///
/// Default physical address.
///
private uint physAddr;
///
/// Presentation layer instance.
///
private IList presentationLayerList = new List();
///
/// Queue for sending of data.
///
private Queue sendQueue = new Queue();
///
/// Flag for send thread.
///
private bool doRun = false;
///
/// Number of frames sent before next CTS expected.
///
/// Only applicable for multi-frame.
///
///
private uint txBlockSize = 1;
///
/// Number of frames received before sending next CTS.
///
/// Only applicable for multi-frame.
///
///
private uint rxBlockSize = 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;
///
/// Gets a value indicating whether CAN data shall be padded.
///
private bool padData;
///
/// Preferences instance.
///
private IPreferences iPreferences;
///
/// Initializes a new instance of the class.
///
/// Event log appender instance.
/// Preferences instance.
/// Physical address.
/// Address mask.
/// 'true' if data shall be padded to full CAN frame.
public SessionLayer(ILogging iLogging, IPreferences iPreferences, uint physAddr, uint addressMask, bool padData)
{
this.iLogging = iLogging;
this.iPreferences = iPreferences;
this.addressMask = addressMask;
this.physAddr = physAddr;
this.padData = padData;
this.doRun = true;
this.sendThread = new Thread(this.sendThreadImpl);
this.sendThread.Name = "0x" + physAddr.ToString("X2");
this.sendThread.Start();
}
///
/// Clean up.
///
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Add presentation layer instance.
///
/// Presentation layer to add
public void addPresentationLayer(IPresentationLayer iPresentationLayer)
{
if (!this.presentationLayerList.Contains(iPresentationLayer))
{
this.presentationLayerList.Add(iPresentationLayer);
}
}
///
/// Remove presentation layer instance.
///
/// Presentation layer to remove.
public void removePresentationLayer(IPresentationLayer iPresentationLayer)
{
if (this.presentationLayerList.Contains(iPresentationLayer))
{
this.presentationLayerList.Remove(iPresentationLayer);
}
}
///
/// Set communicator instance.
///
/// Communicator instance.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "iCanCommunicator", Justification = "Reviewed, intentional")]
public void setCanCommunicator(ICanCommunicator iCanCommunicator)
{
if (iCanCommunicator == null && this.iCanCommunicator != null)
{
this.iCanCommunicator.removeDataReceiver(this);
}
this.iCanCommunicator = iCanCommunicator;
if (iCanCommunicator != null)
{
this.iCanCommunicator.addDataReceiver(this);
}
}
///
/// Enqueue data to send on CAN.
///
/// Target address.
/// Payload data.
/// Payload data length.
public void sendData(uint outAddress, IList response, uint dataLen)
{
if (response != null)
{
this.iLogging.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);
#if TRACE
this.iLogging.appendTextLn("Datalen=" + maskedDataLen);
#endif
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.");
}
}
}
else
{
this.iLogging.appendTextLn("Trying to send 'null' data.");
}
}
///
/// Receive one data packet.
///
/// Address value.
/// Payload data.
/// Flags value.
/// RES1 Value.
/// RES2 Value.
public void receiveData(uint inAddress, byte[] data0, ushort flags, ulong res1, ulong res2)
{
if (data0 != null)
{
if (this.matchAddress(inAddress))
{
uint outAddress = getRespAddr(inAddress);
this.iLogging.addRow(rowMode.transport_in, inAddress, 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;
switch (frameType)
{
case 0:
this.singleFrameHandler(inAddress, zLen, data0);
break;
case 1:
this.firstFrameHandler(outAddress, inAddress, zLen, data0, p0);
break;
case 2:
this.multiFrameHandler(outAddress, inAddress, 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
{
if (this.iLogging != null)
{
this.iLogging.appendTextLn("Received less than 2 bytes of data, ignoring.");
}
}
}
}
else
{
if (this.iLogging != null)
{
this.iLogging.appendTextLn("'null' data received!");
}
}
}
///
/// Clean up.
///
/// 'true' if disposing.
protected virtual void Dispose(bool disposing)
{
this.doRun = false;
this.sendThread.Abort();
this.sendThread.Interrupt();
}
///
/// Get (generate) response address.
///
/// Source address to use as template.
/// Response address.
private static uint getRespAddr(uint address)
{
if (address < 0x800)
{
return address & 0x7F7;
}
return (address & 0xFFFF0000) | ((address & 0xff) << 8) | ((address & 0xFF00) >> 8);
}
///
/// 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))
{
if (this.sendQueue.Count > 0)
{
this.iLogging.appendTextLn("SessionLayer.sendThreadImpl(): Timeout with data present. this.sendQueue.Count=" + this.sendQueue.Count);
}
}
}
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.txBlockSize > 0)
|| (timeoutCounter > TIMEOUT_CYCLES))
{
if (queueItem.frameType == 0x01)
{
this.txBlockSize = 1;
}
queueItem = this.sendQueue.Dequeue();
timeoutCounter = 0;
if (this.txBlockSize > 0 && (queueItem.frameType == 0x01 || queueItem.frameType == 0x02))
{
this.txBlockSize--;
}
}
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)
{
this.iLogging.addRow(rowMode.transport_out, outAddress, 0x00, 0, 0, Utils.arrayToString(payload, 0));
this.iCanCommunicator.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.iPreferences.blockSize;
List response = new List();
response.Add(0x30); // CTS.
response.Add(this.iPreferences.blockSize);
response.Add(this.iPreferences.frameSpacing);
// 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.iPreferences.blockSize;
List response = new List();
response.Add(0x32); // Abort.
response.Add(this.iPreferences.blockSize);
response.Add(this.iPreferences.frameSpacing);
// 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.padData)
{
while (response.Count < 8)
{
response.Add(0x00);
}
}
byte[] rspBa = response.ToArray();
return rspBa;
}
///
/// Check if received address matches the addresses of this engine.
///
/// Incoming address to test.
/// 'true' if matching address.
private bool matchAddress(uint id)
{
return ((id & this.addressMask) == (this.physAddr & this.addressMask));
}
///
/// 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.txBlockSize = data0[p0++];
this.separationTime = data0[p0++];
if (this.txBlockSize == 0)
{
this.sendQueue.Clear();
this.iLogging.appendTextLn("Peer indicated BlockSize of zero - cancelling transfer.");
}
else
{
Monitor.PulseAll(this.sendQueue);
}
}
break;
case 1: // Wait.
this.txBlockSize = 0;
break;
case 2: // Overflow/Abort.
lock (this.sendQueue)
{
this.sendQueue.Clear();
this.txBlockSize = 1;
}
break;
default:
break;
}
}
///
/// Handle first frame in a multi-frame message.
///
/// Response address (for handshake).
/// Target address.
/// Data length.
/// Payload data
/// Position of data in frame.
private void firstFrameHandler(uint outAddress, uint inAddress, uint zLen, byte[] data0, uint p0)
{
if (this.multiFrameDataDictionary.ContainsKey(inAddress))
{
this.multiFrameDataDictionary.Remove(inAddress);
}
this.multiFrameDataDictionary.Add(inAddress, new MultiFrameData(zLen, data0, p0));
this.sendFlowCTS(outAddress);
}
///
/// 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 inAddress, uint zLen, byte[] data0, uint p0)
{
if (this.multiFrameDataDictionary.ContainsKey(inAddress))
{
this.multiFrameDataDictionary[inAddress].addData((zLen & 0x0f), data0, p0);
if (this.multiFrameDataDictionary[inAddress].error)
{
this.iLogging.appendTextLn("Multiframe data error, discarding rest.");
this.multiFrameDataDictionary.Remove(inAddress);
this.sendFlowAbort(outAddress);
}
else
{
if (this.multiFrameDataDictionary[inAddress].complete)
{
this.invokePresentation(inAddress, this.multiFrameDataDictionary[inAddress].length, this.multiFrameDataDictionary[inAddress].data);
this.multiFrameDataDictionary.Remove(inAddress); // Clean up for next packet.
}
else
{
this.rxBlockSize--;
if (this.rxBlockSize <= 0)
{
this.sendFlowCTS(outAddress);
}
}
}
}
}
///
/// Handle arrival of a single frame.
///
/// Functional addressing only permits single frames.
///
///
/// Target address.
/// Data length.
/// Payload data
private void singleFrameHandler(uint inAddress, uint zLen, byte[] data0)
{
// Discard any old multi-frame data from the given node.
if (this.multiFrameDataDictionary.ContainsKey(inAddress))
{
this.multiFrameDataDictionary.Remove(inAddress);
}
byte[] data = new byte[zLen];
Array.Copy(data0, 1, data, 0, zLen);
this.invokePresentation(inAddress, zLen, data);
}
///
/// Invoke the presentation layer.
///
/// Target address.
/// Data length.
/// Payload data.
private void invokePresentation(uint id, uint zLen, byte[] data)
{
string inData = Utils.arrayToString(data, 0);
this.iLogging.addRow(rowMode.session_in, id, 0, 0, zLen, inData);
foreach (IPresentationLayer presentationLayer in this.presentationLayerList)
{
presentationLayer.processPayload(id, 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;
}
}
}
}