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