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