//----------------------------------------------------------------------- // // 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 Protocol.OBD { using System; using System.Collections.Generic; using System.IO; using System.IO.Ports; using System.Linq; using System.Threading; using System.Windows.Forms; using global::DataLogging; using global::SharedObjects; using global::SharedObjects.DataLogging; using global::SharedObjects.DataLogging.Objects; using global::SharedObjects.DataMgmt; using global::SharedObjects.GUI.OBD; using global::SharedObjects.Misc; using global::SharedObjects.Misc.Objects; using global::SharedObjects.Protocol; /// /// Class for managing data requests initiated by the GUI. /// public class DataRequester { /// /// Number of items per SSM request. /// /// Can be up to 75 (given by trial&error), but 64 seems to be a good value. /// /// private const int ITEMS_PER_SSM_REQUEST = 64; /// /// Gets or sets interval between plotting/logging polls. /// public int pollInterval { get; set; } /// /// Messaging interface instance. /// private IMessaging iMessaging; /// /// Gets data log interface instance. /// public IDataLogInstance iDataLog { get; private set; } /// /// Gets a value indicating whether logging status is active. /// public bool logging { get; private set; } /// /// Gets a value indicating whether plotting status is active. /// public bool plotting { get; private set; } /// /// List of requests to perform. /// private List requests = new List(); /// /// List of requests to perform. /// private Dictionary> ssmReadAddresses = new Dictionary>(); /// /// Current Mode parser. /// private ModeParser modeParser = null; /// /// Thread for regular polling of data. /// private Thread pollThread = null; /// /// Thread for reception of NMEA GPS data. /// private Thread gpsThread = null; /// /// Dictionary of value names when plotting. /// private StackedDictionary nodeValueDictionary = new StackedDictionary(); /// /// Logging instance. /// private ILogging iLogging; /// /// Preferences interface instance. /// private IPreferences iPreferences; /// /// Data source instance. /// private IDataSource iDataSource; /// /// Progress feedback instance. /// private IProgressFeedback iProgressFeedback; /// /// Number of items per SSM request, protocol dependent value. /// private int itemsPerSsmRequest = 1; /// /// Initializes a new instance of the class. /// /// Logging instance /// Preferences interface instance /// Messaging instance. /// Mode parser instance. /// Data source interface instance. /// Current protocol handler instance. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "5", Justification = "Reviewed, intentional.")] public DataRequester( ILogging iLogging, IPreferences iPreferences, IMessaging iMessaging, ModeParser modeParser, IDataSource iDataSource, IProtocolHandler protocolHandler) { this.iLogging = iLogging; this.iPreferences = iPreferences; this.iMessaging = iMessaging; this.modeParser = modeParser; this.iDataSource = iDataSource; this.itemsPerSsmRequest = 1; if (protocolHandler != null && protocolHandler.protocol != null) { // Only a J2534 interface with ISO15765 can do some good stuff. switch (protocolHandler.protocol.id) { case Protocols.ISO15765: this.itemsPerSsmRequest = ITEMS_PER_SSM_REQUEST; break; default: this.itemsPerSsmRequest = 1; break; } } this.iDataLog = new DataLogInstance(this.iLogging); this.pollInterval = 1000; this.plotting = false; this.logging = false; } /// /// Get the destination address from the source address. /// /// Current protocol handler instance. /// Source address to use. /// Prepared destination address. public static uint? getDestinationAddress(IProtocolHandler protocolHandler, uint sourceAddress) { uint? destinationAddress = null; if (protocolHandler != null && protocolHandler.protocol != null) { switch (protocolHandler.protocol.id) { case Protocols.SERIAL_AT_AGV: case Protocols.SERIAL_AT_ELM: destinationAddress = getSerialAtDestAddress(protocolHandler, sourceAddress); break; case Protocols.ISO15765: destinationAddress = getIso15765DestAddress(sourceAddress); break; default: // No action. break; } } return destinationAddress; } /// /// Start the Plot thread. /// /// Progress feedback interface. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "iProgressFeedback", Justification = "Reviewed, intentional.")] public void startPlotThread(IProgressFeedback iProgressFeedback) { this.iProgressFeedback = iProgressFeedback; this.pollThread = new Thread(new ThreadStart(this.pollThreadImplementation)); this.pollThread.Name = "DRQ Poll"; this.pollThread.Start(); } /// /// Start the GPS thread. /// public void startGpsThread() { this.gpsThread = new Thread(this.gpsThreadImplementation); this.gpsThread.Name = "GPS Poll"; this.gpsThread.Start(); } /// /// Prepare for plotting and/or logging of data. /// /// 'true' if plotting. /// 'true' if logging. public void preparePlotting(bool plotting, bool logging) { this.plotting = plotting; this.logging = logging; } /// /// Stop plotting/logging. /// public void stopPlotting() { this.plotting = false; this.logging = false; if (this.pollThread != null) { this.iLogging.appendText("Poll Thread stopping.\r\n"); this.pollThread.Abort(); this.pollThread.Interrupt(); this.pollThread.Join(4000); this.pollThread = null; this.iLogging.appendText("Poll Thread stopped.\r\n"); } } /// /// Prepare for data request by initializing the data stores. /// public void prepareRequestData() { this.requests = new List(); this.nodeValueDictionary = new StackedDictionary(); } /// /// Finalize the data request. /// public void finishPrepareDataRequest() { foreach (uint destinationAddress in this.ssmReadAddresses.Keys) { this.nextSsmChunk(destinationAddress); } if (this.logging && this.iDataLog.isLogOpen()) { this.iDataLog.initLog(this.nodeValueDictionary, this.gpsThread != null ? this.gpsThread.IsAlive : false); } } /// /// Handle one PID when building a request. /// /// Notice that Mode 0x02 uses same PID list as 0x01, which means that there /// are some tweaks built in for that. /// /// /// Destination addresses to send to. /// Mode byte. /// Freeze Frame number (Mode 0x02), ignored when current data. /// Mode by Pid Group. /// PID item. /// Actual PID id. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "3", Justification = "Reviewed, intentional.")] public void handlePid( IList destinationAddresses, byte actualMode, byte frameNumber, uint pidGroupMode, XmlClass.pidgroup.pidlist pid, uint actualPid) { if (destinationAddresses != null) { foreach (uint destinationAddress in destinationAddresses) { this.addValueNames(destinationAddress, pidGroupMode, pid, actualPid); switch (actualMode) { case 0x01: byte[] normalPidBa = new byte[] { actualMode, (byte)(actualPid & 0xff) }; this.requests.Add(new RequestData(destinationAddress, actualMode, pid, new TxMsg(normalPidBa))); break; case 0x02: byte[] freezeFrameBa = new byte[] { actualMode, (byte)(actualPid & 0xff), frameNumber }; this.requests.Add(new RequestData(destinationAddress, actualMode, pid, new TxMsg(freezeFrameBa))); break; case 0xA0: this.requests.Add(getSsmEntry(destinationAddress, actualMode, pid)); break; case 0xA8: // Do nothing here, see below. break; case 0x22: byte[] extendedPidBa = new byte[] { actualMode, (byte)((actualPid >> 8) & 0xff), (byte)(actualPid & 0xff) }; this.requests.Add(new RequestData(destinationAddress, actualMode, pid, new TxMsg(extendedPidBa))); break; default: byte[] defaultActionBa = new byte[] { actualMode, (byte)(actualPid & 0xff) }; this.requests.Add(new RequestData(destinationAddress, actualMode, pid, new TxMsg(defaultActionBa))); break; } } switch (actualMode) { case 0xA8: this.addSsmEntry(destinationAddresses, pid); break; } } } /// /// Perform a request for data. /// public void requestSelectedPids() { this.requestSelectedPids(this.requests); } /// /// If it's a serial AT device we can still figure out protocol at a second step. /// /// Current protocol handler instance. /// Source address to use. /// Created destination address or 'null' if not supported. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Reviewed.")] private static uint? getSerialAtDestAddress(IProtocolHandler protocolHandler, uint sourceAddress) { uint? destinationAddress = null; if (protocolHandler is Protocol_Serial_AT) { Protocol_Serial_AT psa = (Protocol_Serial_AT)protocolHandler; switch (psa.detectedProtocol) { case 6: case 7: case 8: case 9: destinationAddress = getIso15765DestAddress(sourceAddress); break; default: destinationAddress = 0x00; break; } } return destinationAddress; } /// /// Get the destination address from the source address for ISO 15765. /// /// Source address to use. /// Created destination address or 'null' if not supported. private static uint? getIso15765DestAddress(uint sourceAddress) { uint? destinationAddress = null; if (sourceAddress > 0x800) { destinationAddress = (sourceAddress & 0x18FF0000) | ((sourceAddress & 0xff) << 8) | ((sourceAddress & 0xff00) >> 8); } else { destinationAddress = (sourceAddress & 0x7F7); } return destinationAddress; } /// /// Build a request entry with SSM request data. /// /// Destination address to send to. /// Mode byte value. /// PID item for the entry. /// Request data instance. private static IRequestData getSsmEntry(uint? destinationAddress, uint mode, XmlClass.pidgroup.pidlist ssmPid) { uint addr = ssmPid.pid_int; int n1 = 0; byte[] ba; if (mode == 0xA8) { ba = new byte[(2 + (3 * ssmPid.pid_bytes))]; ba[n1++] = 0xA8; // SSM Read ba[n1++] = 0x00; // Pad int k = 0; while (ssmPid.pid_bytes > k) { ba[n1++] = (byte)((addr >> 16) & 0xff); ba[n1++] = (byte)((addr >> 8) & 0xff); ba[n1++] = (byte)(addr & 0xff); addr++; k++; } return new RequestData(destinationAddress, 0xA8, ssmPid, new TxMsg(ba)); } ba = new byte[6]; ba[n1++] = 0xA0; // SSM Read ba[n1++] = 0x00; // Pad ba[n1++] = (byte)((addr >> 16) & 0xff); ba[n1++] = (byte)((addr >> 8) & 0xff); ba[n1++] = (byte)(addr & 0xff); ba[n1++] = (byte)ssmPid.pid_bytes; return new RequestData(destinationAddress, 0xA0, ssmPid, new TxMsg(ba)); } /// /// Build a request entry with SSM request data. /// /// Destination addresses to send to. /// PID item for the entry. private void addSsmEntry(IList destinationAddresses, XmlClass.pidgroup.pidlist ssmPid) { // List foreach (uint destinationAddress in destinationAddresses) { List itemList; if (this.ssmReadAddresses.ContainsKey(destinationAddress)) { itemList = this.ssmReadAddresses[destinationAddress]; } else { itemList = new List(); this.ssmReadAddresses.Add(destinationAddress, itemList); } uint addr = ssmPid.pid_int; if ((itemList.Count + ssmPid.pid_bytes) > this.itemsPerSsmRequest) { this.nextSsmChunk(destinationAddress); } for (int i = 0; i < ssmPid.pid_bytes; i++) { byte[] ba = new byte[3]; int n1 = 0; ba[n1++] = (byte)((addr >> 16) & 0xff); ba[n1++] = (byte)((addr >> 8) & 0xff); ba[n1++] = (byte)(addr & 0xff); itemList.Add(new SsmItemWrapper(ba)); addr++; } } } /// /// Add next SSM chunk to the list of requests. /// /// Destination ECU. private void nextSsmChunk(uint destinationAddress) { if (this.ssmReadAddresses.ContainsKey(destinationAddress)) { List itemList = this.ssmReadAddresses[destinationAddress]; if (itemList.Count > 0) { byte[] ba = new byte[(2 + (itemList.Count * 3))]; int n1 = 0; ba[n1++] = 0xA8; // SSM Read ba[n1++] = 0x00; // Pad foreach (SsmItemWrapper ssmItemWrapper in itemList) { Array.Copy(ssmItemWrapper.data, 0, ba, n1, 3); n1 += 3; } this.requests.Add(new RequestData(destinationAddress, 0xA8, null, new TxMsg(ba))); itemList.Clear(); } } } /// /// Add names of values to a dictionary using the mode, PID and sensor number. /// /// Notice that this is only working for 16 bit PIDs, if they are longer - like some SSM PIDs /// there is a risk of some quirky behavior and failure to map correctly. /// /// /// Destination address to send to. /// Mode byte. /// PID information instance, used to get name of PID. /// Actual PID ID. Only the 16 lower bits are considered. private void addValueNames(uint? destinationAddress, uint pidGroupMode, XmlClass.pidgroup.pidlist pid, uint actualPid) { uint k = 0; foreach (XmlClass.pidgroup.pidlist.sensordata sensor in pid.sensors) { XmlClass.unititem thisunit = null; foreach (XmlClass.unititem unit in this.iDataSource.units) { if (sensor != null && unit != null && sensor.unit != null && sensor.unit.Equals(unit.name)) { thisunit = unit; break; } } if (this.iPreferences.selectedUnitCategory == 0 || (thisunit != null && (thisunit.category == 0 || (thisunit.category > 0 && thisunit.category == this.iPreferences.selectedUnitCategory)))) { uint da = (destinationAddress != null) ? (uint)destinationAddress : 0; uint[] keys = { da, pidGroupMode, actualPid, k }; this.nodeValueDictionary.Add(keys, "0x" + da.ToString("X2") + ": " + pid.name + " " + sensor.name + " " + sensor.unit); } k++; } } /// /// Perform a request for data. /// /// List of request items. private void requestSelectedPids(IList requestItems) { try { this.performRequest(requestItems); } catch (ThreadAbortException) { throw; } catch (Exception ex) { MessageBox.Show( ex.Message + "\r\n\r\n" + ex.StackTrace, "Error", MessageBoxButtons.OK, MessageBoxIcon.Stop); throw; } } /// /// Perform a request for data. /// /// List of request items. private void performRequest(IList requestItems) { IList pendingModeRequests = new List(); IRequestData[] rda = requestItems.ToArray(); foreach (IRequestData request in rda) { pendingModeRequests.Add(request); } if (this.modeParser != null) { this.modeParser.setPendingRequests(pendingModeRequests); } // Tell the message handler to send one message. this.iMessaging.sendMsg(); } /// /// Implementation of the GPS listen thread. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Reviewed.")] private void gpsThreadImplementation() { SerialPort sp = null; try { this.iLogging.appendText("Open GPS port: '" + this.iPreferences.gpsSerialPortName + "'\r\n"); sp = new SerialPort(this.iPreferences.gpsSerialPortName, 4800, Parity.None, 8, StopBits.One); sp.Handshake = Handshake.None; sp.Open(); GpsData gpsData = null; bool valid = false; while (this.plotting || this.logging) { try { string line = sp.ReadLine(); #if TRACE this.iLogging.appendText("line=" + line + "\r\n"); #endif if (line.StartsWith("$GPRMC")) { string[] sa = line.Split(new char[] { ',' }); valid = (sa[2] == "A"); if (valid) { gpsData = new GpsData(sa[1], sa[9], sa[3], sa[4], sa[5], sa[6], sa[7], sa[8]); } } if (valid && gpsData != null && line.StartsWith("$GPGGA")) { string[] sa = line.Split(new char[] { ',' }); gpsData.addGGA(sa[8], sa[9], sa[10]); this.iDataLog.logGps(gpsData, DateTime.Now.Ticks); } } catch { // Garbage on the line, try again. } } } catch (IOException ex) { this.iLogging.appendText("gpsThreadImplementation(): " + ex.GetType().ToString() + ", " + ex.Message + "\r\n" + ex.StackTrace + "\r\n"); Exception sub = ex.InnerException; while (sub != null) { this.iLogging.appendText("gpsThreadImplementation(sub): " + sub.GetType().ToString() + ", " + sub.Message + "\r\n" + sub.StackTrace + "\r\n"); sub = sub.InnerException; } } finally { if (sp != null) { try { sp.Close(); } catch { } try { sp.Dispose(); } catch { } } } } /// /// Implementation of the poll thread. /// private void pollThreadImplementation() { IList[] rqArr = this.buildRequestData(); this.pollLoop(rqArr); this.iLogging.appendText("Poll Thread ended.\r\n"); } /// /// Loop for polling for data and logging it. /// /// Array of requests to loop through. private void pollLoop(IList[] rqArr) { int nextBeforeMaxDivisor = rqArr.Length; uint ix = 0; long t2 = (DateTime.Now.Ticks / Utils.TICKS_PER_MILLISECOND); int delay = 0; while (this.plotting || this.logging) { this.iProgressFeedback.tickProgress(ix); long t3 = DateTime.Now.Ticks / Utils.TICKS_PER_MILLISECOND; // Adjust the time interval between each poll to cover for lost time in processing. delay = this.pollInterval - (int)(t3 - t2 - delay); t2 = t3; // Make sure that we at least have a reasonable delay in case the load is too large. /* if (delay < 50) { delay = 100; } */ if (delay < 0) { delay = 0; } Thread.Sleep(delay); if (this.logging) { this.iDataLog.performLog(); } // Build the request for the selected data slot. // This means we consider the configured poll interval // for each item and spread them over time to // get an easy load on vehicle and system. IList rqd = new List(); int ix1 = (int)(ix % nextBeforeMaxDivisor); foreach (IRequestData rd in rqArr[ix1]) { if (((ix - ix1) % rd.divisor) == 0) { rqd.Add(rd); } } this.requestSelectedPids(rqd); ix++; } } /// /// Build the request data to use when communicating with the vehicle. /// /// Array of requests to loop through. private IList[] buildRequestData() { SortedDictionary> intervalGroups = new SortedDictionary>(); SortedDictionary divisors = new SortedDictionary(); foreach (IRequestData rd in this.requests) { #if TRACE this.iLogging.appendText("rd.divisor=" + rd.divisor + "\r\n"); #endif int divisor = rd.divisor; if (!divisors.ContainsKey(divisor)) { divisors.Add(divisor, divisor); } IList partList; if (intervalGroups.ContainsKey(divisor)) { partList = intervalGroups[divisor]; } else { partList = new List(); intervalGroups.Add(divisor, partList); } partList.Add(rd); } int nextBeforeMaxDivisor = 1; int maxDivisor = 1; foreach (KeyValuePair kvp in divisors) { if (kvp.Key > maxDivisor) { nextBeforeMaxDivisor = maxDivisor; maxDivisor = kvp.Key; } } IList[] rqArr = new IList[nextBeforeMaxDivisor]; for (int i = 0; i < nextBeforeMaxDivisor; i++) { rqArr[i] = new List(); } foreach (KeyValuePair> kvp in intervalGroups) { int n = 999; // Arbitrary large value. int i1 = 0; // Index of first list with lowest count. for (int i = 0; i < nextBeforeMaxDivisor; i++) { if (n > rqArr[i].Count) { n = rqArr[i].Count; i1 = i; } } int sz = kvp.Key; IList rql = kvp.Value; for (int i = 0; i < nextBeforeMaxDivisor; i++) { if (((i - i1) % sz) == 0) { foreach (IRequestData rd in rql) { rqArr[i].Add(rd); } } } } return rqArr; } } }