Files
DP44/DataPRO/ICommand/AbstractCommandBase.cs
2026-04-17 14:55:32 -04:00

834 lines
34 KiB
C#

//#define LOG_COMM
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using DTS.Common.DASResource;
using DTS.Common.ICommunication;
using DTS.Common.Utilities;
using DTS.Common.Utilities.Logging;
using DTS.Common.Utils;
using DTS.Common.Enums.Communication;
using DTS.Common.Interface.Communication;
using DTS.Common.Enums.DASFactory;
using DTS.Common.Events;
// ReSharper disable InconsistentNaming
namespace DTS.DASLib.Command
{
/// <summary>
/// Ribeye, SliceCommand, SliceDBCommand all contained Command objects which
/// implemented ICommand
/// I extracted as many common things as I could into one common class and formed
/// an abstract base class.
/// In general slice commands seemed to contain the most up to date code, and
/// slice db commands and ribeye seemed to contain older code, so the majority of the code
/// is actually from slice commands.
/// 6/14/10 - dtm
/// </summary>
public abstract class AbstractCommandBase : ICommand
{
// this is the buffer we use to accumulate the data coming in until we have a full
// response package.
protected SecureQueue<byte> CommandDataBuffer;
protected DTS.Common.Interface.DASFactory.ICommunication recorder;
protected ManualResetEvent SyncEvent;
protected CommandCallback UserCallback { get; set; }
protected object UserCallbackData { get; set; }
protected bool IsSynchronous { get; set; }
protected CommandPacketBase baseCommand;
protected CommandPacketBase baseResponse;
protected ICommunicationReport ComReport;
protected DFConstantsAndEnums.CommandStatus _status;
protected Type ClassType;
protected object _debuglock;
protected int current_thread_id;
protected int MinimumProtocolVersion { get; set; }
protected DateTime ExecuteTime { get; set; }
protected bool ExecuteIsBusy = false;
protected object ExecuteBusyLock = new object();
/// <summary>
/// whether to log commands or not
/// </summary>
public bool LogCommands { get; set; }
/// <summary>
/// Log Command or response
/// </summary>
/// <param name="sending">if true logs command, otherwise logs response</param>
protected virtual void LogCommand(bool sending)
{
try
{
if (!LogCommands) { return; }
APILogger.LogString(GetFormattedLogEntry(sending));
}
catch (Exception ex)
{
LogString($"threw an exception in {(sending ? "CommandToString()" : "ResponseToString()")}");
APILogger.LogException(ex);
}
}
protected virtual string MakeLogString(string msg)
{
var dasName = "<unknown>";
if (null != recorder) { dasName = recorder.ToString(); }
var cmdName = "<unknown>";
try
{
cmdName = GetType().FullName;
}
catch
{
// we use unknown
}
var sequenceNumber = "<unknown>";
try
{
sequenceNumber = baseCommand.SequenceNumber.ToString();
}
catch { }
//17686 add additional debug information into log
if (null != baseResponse)
{
sequenceNumber = $"{sequenceNumber}\\{baseResponse.SequenceNumber}";
}
return string.Format(Strings.CmdAbstractCommandGeneralFailure, dasName, cmdName, msg, sequenceNumber);
}
protected virtual void LogString(string msg)
{
APILogger.LogString(MakeLogString(msg));
}
/// <summary>
/// <see cref="CommandPacketBase.CommandStatus" /> status of command
/// </summary>
public DFConstantsAndEnums.CommandStatus Status => _status;
protected void EnqueueData(byte[] data)
{
CommandDataBuffer.Enqueue(data);
}
protected byte[] DequeueData(bool bResetEvent)
{
return CommandDataBuffer.Dequeue(bResetEvent);
}
#if LOG_COMM
private static readonly object _COMM_LOCK_ = new object();
#endif
/**
* Knuth-Morris-Pratt Algorithm for Pattern Matching
*/
class KMPMatch
{
/**
* Finds the first occurrence of the pattern in the text.
*/
public int indexOf(byte[] data, byte[] pattern, int startAddress = 0)
{
int[] failure = computeFailure(pattern);
int j = 0;
if (data.Length == 0) return -1;
for (int i = startAddress; i < data.Length; i++)
{
while (j > 0 && pattern[j] != data[i])
{
j = failure[j - 1];
}
if (pattern[j] == data[i]) { j++; }
if (j == pattern.Length)
{
return i - pattern.Length + 1;
}
}
return -1;
}
/**
* Computes the failure function using a boot-strapping process,
* where the pattern is matched against itself.
*/
private int[] computeFailure(byte[] pattern)
{
int[] failure = new int[pattern.Length];
int j = 0;
for (int i = 1; i < pattern.Length; i++)
{
while (j > 0 && pattern[j] != pattern[i])
{
j = failure[j - 1];
}
if (pattern[j] == pattern[i])
{
j++;
}
failure[i] = j;
}
return failure;
}
}
private static readonly byte[] StreamSignature = { 0xFA, 0x04, 0x07, 0x00, 0x00, 0x00, 0x10, 0xF8 };
private static readonly byte[] EndStreamSignature = { 0xFA, 0x04, 0x08, 0x00 };
private const int STREAM_PACKET_SIZE = 63518;
protected virtual CommandReceiveAction ReceiveBlockOK(ICommunicationReport report)
{
lock (_debuglock)
{
if (-1 != current_thread_id)
{
}
current_thread_id = Thread.CurrentThread.ManagedThreadId;
}
// use the buffer to assemble a bigger chunk
EnqueueData(report.Data);
var tempBuffer = DequeueData(false);
if (DFConstantsAndEnums.ExtraCommunicationLogging)
{
APILogger.Log($"ReceivedBlock {recorder?.ConnectString ?? "N/A"}", tempBuffer);
}
if (baseCommand.GetCommandDescription() == "GetNextDownloadStreamDataSamples")
{
var match = new KMPMatch();
var index = match.indexOf(tempBuffer, StreamSignature, 1);
if (index > 0)
{
var length = tempBuffer.Length - index;
var remainder = new byte[length];
Array.Copy(tempBuffer, index, remainder, 0, length);
EnqueueData(remainder);
var tmp = new byte[index];
Array.Copy(tempBuffer, 0, tmp, 0, index);
tempBuffer = tmp;
}
else
{
index = match.indexOf(tempBuffer, EndStreamSignature);
if (index >= 0)
{
var tmp = new byte[index];
Array.Copy(tempBuffer, 0, tmp, 0, index);
tempBuffer = tmp;
}
else
{
EnqueueData(tempBuffer);
return CommandReceiveAction.ContinueReceiving;
}
}
}
//there could be multiple packets in here, we have to
// a debug test
if (null != tempBuffer && tempBuffer.Length > 0 && tempBuffer[0] != 0xFA)
{
}
#if LOG_COMM
lock (_COMM_LOCK_)
{
System.IO.File.AppendAllText(@"Logs\COMM.log",
$"{DateTime.Now.Ticks} [IN] {BitConverter.ToString(tempBuffer).Replace("-", string.Empty)}\r\n");
}
#endif
// what's the state of this packet?
// - note I think the command object should be intialized and stable, but
// just incase maybe it's safer to check it before using it
// 6/14/2010 - dtm
var pState = baseCommand?.VerifyPacket(tempBuffer) ?? GetCommandPacket().VerifyPacket(tempBuffer);
if (DFConstantsAndEnums.ExtraCommunicationLogging)
{
APILogger.Log($"VerifyPacket {recorder?.ConnectString ?? "N/A"} {pState.ToString()}");
}
switch (pState)
{
case CommandPacketBase.PacketState.OK:
// perfect, a good complete packet
baseResponse = GetCommandPacket(tempBuffer);
#if LOG_COMM
lock (_COMM_LOCK_)
{
System.IO.File.AppendAllText(@"Logs\COMM.log",
$"{DateTime.Now.Ticks} [IN] [OK] seq:{baseResponse.SequenceNumber} {baseCommand.GetCommandDescription()}\r\n");
#endif
if (baseCommand.GetCommandDescription() == "EndRealtimeMode")
{
//if (baseCommand.SequenceNumber != baseResponse.SequenceNumber)
{
//look for the response
KMPMatch match = new KMPMatch();
//look for the command header for end realtime (0x08, 0x02)
//if we find it we have a response to end realtime, if not we have
//other data and should keep waiting
var target = new byte[] { 0xFA, 0x08, 0x02 };
//baseCommand.ToBytes();
var index = match.indexOf(tempBuffer, target);
if (index < 0)
{
#if LOG_COMM
System.IO.File.AppendAllText(@"Logs\COMM.log",
$"{DateTime.Now.Ticks} - NOT COMPLETE YET\r\n");
#endif
//this suggests the response we have is a realtime packet and NOT the end packet
return DataTooShort(tempBuffer);
}
else
{
#if LOG_COMM
System.IO.File.AppendAllText(@"Logs\COMM.log", $"{DateTime.Now.Ticks} - COMPLETE\r\n");
#endif
}
}
}
#if LOG_COMM
}
#endif
WholePackage();
if (IsSynchronous)
{
lock (_debuglock) { current_thread_id = -1; }
SyncEvent.Set();
}
else
{
// we're not doing a SyncExecute so we must flag the command done
recorder.ExecuteIsBusy = false;
WholePackagePost();
lock (_debuglock) { current_thread_id = -1; }
}
return CommandReceiveAction.StopReceiving;
case CommandPacketBase.PacketState.TooShort:
// not enough data, keep going
lock (_debuglock) { current_thread_id = -1; }
return DataTooShort(tempBuffer);
case CommandPacketBase.PacketState.Unknown:
// not good
if (!IsSynchronous)
{
// we're not doing a SyncExecute so we must flag the command done
recorder.ExecuteIsBusy = false;
lock (_debuglock) { current_thread_id = -1; }
return DataUnknown(report);
}
SyncEvent.Set();
return CommandReceiveAction.StopReceiving;
default:
Debug.Assert(false, "CommandBase.ReceiveBlockOK: Unhandled case " + pState.ToString());
return CommandReceiveAction.StopReceiving;
}
}
protected virtual CommandReceiveAction DataTooShort(byte[] dataSoFar)
{
// we need more data
CommandDataBuffer.Enqueue(dataSoFar);
return CommandReceiveAction.ContinueReceiving;
}
protected virtual CommandReceiveAction DataUnknown(ICommunicationReport report)
{
// a bad one
var cbReport = new CommandReport(CommandStatus.Failure, UserCallbackData);
LogString("DataUnknown: reporting failure " + report);
UserCallback(cbReport);
return CommandReceiveAction.StopReceiving;
}
protected virtual CommandReceiveAction WholePackage()
{
return CommandReceiveAction.StopReceiving;
}
protected virtual CommandReceiveAction WholePackagePost()
{
// we have a whole package, do a default response
var stat = CommandStatus.Success;
if (baseResponse.Status != DFConstantsAndEnums.CommandStatus.StatusNoError)
{
var s = (int)baseResponse.Status;
LogString("WholePackagePost: reporting failure, status==" + CommandPacketBase.StatusLabels[s] + " (0x" + s.ToString("X") + ")");
stat = CommandStatus.Failure;
}
var acr = new CommandReport(stat, UserCallbackData);
return UserCallback(acr);
}
protected virtual CommandReceiveAction SendReceiveError(ICommunicationReport report)
{
if (IsSynchronous)
{
string result = "null";
if (null != report) { result = report.Result.ToString(); }
LogString("SendReceiveError: SyncEvent.Set(), result=" + result);
SyncEvent.Set();
}
else
{
if (UserCallback == null)
{
throw new ApplicationException(Strings.Slice_CommandBase_SendReceiveError_Err1);
}
LogString("SendReceiveError: reporting failure, report==" + report);
var cbReport = new CommandReport(CommandStatus.Failure, UserCallbackData);
UserCallback(cbReport);
}
return CommandReceiveAction.StopReceiving;
}
/// <summary>
/// Status of response packet
/// </summary>
public DFConstantsAndEnums.CommandStatus ResponseStatus
{
get { if (null == baseResponse) return DFConstantsAndEnums.CommandStatus.StatusNoResponse; return baseResponse.Status; }
}
/// <summary>
/// string description of status in hex?
/// </summary>
public string StatusString => CommandPacketBase.StatusLabels[(int)_status] + " (0x" + _status.ToString("X") + ")";
/// <summary>
/// default I/O timeout in ms?
/// </summary>
public const int Default_IO_Timeout = 120000; //System.Threading.Timeout.Infinite; //1000;
/// <summary>
/// timeout for specific commands in ms?
/// </summary>
public int IO_Timeout { get; set; }
/// <summary>
/// note, this was internal, it's not designed to be accessed by more than 1 level of inheritance
/// </summary>
/// <param name="sock"></param>
/// <param name="TimeoutMillisec"></param>
internal void SetupThis(DTS.Common.Interface.DASFactory.ICommunication sock, int TimeoutMillisec)
{
ClassType = GetType();
CommandDataBuffer = new SecureQueue<byte>(SecureQueue<byte>.NullPolicy.SkipNull, "CommandBase.CommandDataBuffer");
recorder = sock;
SyncEvent = new ManualResetEvent(false);
UserCallback = null;
UserCallbackData = null;
IsSynchronous = false;
baseResponse = null;
ComReport = null;
LogCommands = true;
_status = DFConstantsAndEnums.CommandStatus.StatusUnimplemented;
IO_Timeout = TimeoutMillisec;
// The first protocol version was 1, so set that as the default here
MinimumProtocolVersion = 1;
}
protected AbstractCommandBase(DTS.Common.Interface.DASFactory.ICommunication sock)
{
_debuglock = new object();
lock (_debuglock)
{
current_thread_id = -1;
}
SetupThis(sock, Default_IO_Timeout);
}
protected AbstractCommandBase(DTS.Common.Interface.DASFactory.ICommunication sock, int TimeoutMillisec)
{
_debuglock = new object();
lock (_debuglock)
{
current_thread_id = -1;
}
SetupThis(sock, TimeoutMillisec);
}
/// <summary>
/// Both Execute and SyncExecute uses this function as their callback (since
/// both calls recorder.Execute).
/// </summary>
/// <param name="report"></param>
/// <returns>true if recorder should wait for more data, false otherwise
/// </returns>
protected virtual bool ExecuteCallback(ICommunicationReport report)
{
// we should only handle ExecuteIsBusy if we're NOT run thry SyncExecute
ComReport = report;
switch (report.Result)
{
case CommunicationConstantsAndEnums.CommunicationResult.ReceiveOK:
// ReceiveBlockOK takes care of the ExecuteIsBusy flag
return ReceiveBlockOK(report) == CommandReceiveAction.ContinueReceiving;
case CommunicationConstantsAndEnums.CommunicationResult.SendTimeout:
case CommunicationConstantsAndEnums.CommunicationResult.SendFailed:
case CommunicationConstantsAndEnums.CommunicationResult.ReceiveTimeout:
case CommunicationConstantsAndEnums.CommunicationResult.ReceiveFailed:
// this will always result in termination
LogString("ExecuteCallback: " + report.Result);
if (!IsSynchronous)
{
recorder.ExecuteIsBusy = false;
}
SendReceiveError(report);
return false;
case CommunicationConstantsAndEnums.CommunicationResult.Canceled:
if (IsSynchronous)
{
SyncEvent.Set();
}
else
{
recorder.ExecuteIsBusy = false;
var cbReport = new CommandReport(CommandStatus.Canceled, UserCallbackData);
UserCallback(cbReport);
}
return false;
}
Debug.Assert(false, "CommandBase.ExecuteCallback: unhandled case " + report.Result);
return true;
}
private void SurfaceError(string msg)
{
try
{
PageErrorEvent.SurfaceApplicationError(msg);
}
catch (Exception ex)
{
APILogger.Log(ex);
}
}
private Random _random = new Random(DateTime.Now.Second);
protected void InternalSyncExecute()
{
// this is a try/finally to handle the ExecuteIsBusy
try
{
// there can be only one!
recorder.ExecuteIsBusy = true;
if (recorder.ProtocolVersion < MinimumProtocolVersion)
{
throw new Exception(Strings.Slice_CommandBase_SyncExecute_Err6);
}
if (recorder.IsCanceled())
{
throw new CanceledException();
}
if (baseCommand.AlreadyRun)
{
baseCommand.GetNextSequenceNumber();
}
baseCommand.AlreadyRun = true;
baseCommand.ComputeCRCs();
var CommandBytes = baseCommand.ToBytes();
baseCommand.OriginalBytes = CommandBytes;
#if LOG_COMM
lock (_COMM_LOCK_)
{
System.IO.File.AppendAllText(@"Logs\COMM.log",
$"{DateTime.Now.Ticks} [OUT] seq:{baseCommand.SequenceNumber} {baseCommand.GetCommandDescription()} - {BitConverter.ToString(CommandBytes).Replace("-", string.Empty)}\r\n");
}
#endif
if (LogCommands)
{
LogCommand(true);
}
UserCallback = null;
UserCallbackData = null;
IsSynchronous = true;
SyncEvent.Reset();
ExecuteTime = DateTime.Now;
recorder.Execute(CommandBytes, ExecuteCallback, null, IO_Timeout);
var syncExecTimeout = IO_Timeout;
if (syncExecTimeout != Timeout.Infinite)
{
syncExecTimeout *= 3;
}
try
{
if (!WaitWithCondition.Wait(SyncEvent, syncExecTimeout, recorder.CancelEvent))
{
// timeout
LogString("SyncExecute: timeout");
throw new TimeoutException(MakeLogString("SyncExecute: timeout"));
}
}
catch (WaitWithCondition.ConditionMetException)
{
throw new CanceledException();
}
// we didn't timeout, check the result
switch (ComReport.Result)
{
case CommunicationConstantsAndEnums.CommunicationResult.Canceled:
throw new CanceledException();
case CommunicationConstantsAndEnums.CommunicationResult.ReceiveOK:
if (baseResponse == null)
{
LogString("SyncExecute: ReceiveOK but response==null!");
LogCommand(false);
throw new Exception(MakeLogString("SyncExecute: ReceiveOK but response==null!"));
}
if (baseResponse.Status == DFConstantsAndEnums.CommandStatus.StatusSlicebusNoResponse)
{
SurfaceError($"Possible hardware issue detected on {recorder.SerialNumber} [SliceBusNoResponse]. Please contact DTS support");
}
if (baseResponse.Status != DFConstantsAndEnums.CommandStatus.StatusNoError)
{
// didn't go well
var msg = MakeLogString("SyncExecute: response.Status = " + baseResponse.Status);
LogCommand(false);
if (baseResponse.Status == DFConstantsAndEnums.CommandStatus.StatusInvalidModeForCommand ||
baseResponse.Status == DFConstantsAndEnums.CommandStatus.StatusInvalidCommand)
{
throw new CommandException(CommandErrorReason.InvalidMode, msg);
}
if (baseResponse.Status == DFConstantsAndEnums.CommandStatus.StatusUnimplemented ||
baseResponse.Status == DFConstantsAndEnums.CommandStatus.StatusInvalidCommand ||
baseResponse.Status == DFConstantsAndEnums.CommandStatus.StatusInvalidCommandType)
{
throw new NotImplementedException(msg);
}
var ex = new Exception(msg);
ex.Data.Add("Status", baseResponse.Status);
throw ex;
}
// everything is fine, let it exit
if (LogCommands)
{
LogCommand(false);
}
break;
case CommunicationConstantsAndEnums.CommunicationResult.ReceiveFailed:
{
var msg = MakeLogString("SyncExecute: ComReport.Result == " + ComReport.Result);
APILogger.LogString(msg);
LogCommand(false);
throw new CommandException(CommandErrorReason.ReceiveFailed, msg);
}
case CommunicationConstantsAndEnums.CommunicationResult.ReceiveTimeout:
{
var msg = MakeLogString("SyncExecute: ComReport.Result == " + ComReport.Result);
APILogger.LogString(msg);
LogCommand(false);
throw new CommandException(CommandErrorReason.ReceiveFailed, msg);
}
case CommunicationConstantsAndEnums.CommunicationResult.SendFailed:
case CommunicationConstantsAndEnums.CommunicationResult.SendTimeout:
{
var msg = MakeLogString("SyncExecute: ComReport.Result == " + ComReport.Result);
APILogger.LogString(msg);
LogCommand(false);
throw new CommandException(CommandErrorReason.SendFailed, msg);
}
default:
{
var msg = MakeLogString("SyncExecute: Unknown ComReport.Result == " + ComReport.Result);
APILogger.LogString(msg);
LogCommand(false);
throw new Exception(msg);
}
}
}
finally
{
recorder.ExecuteIsBusy = false;
}
}
public virtual void SyncExecute()
{
try
{
InternalSyncExecute();
// if we get here, everything is fine
return;
}
catch (TimeoutException te)
{
// and again
LogString("SyncExecute: timeout");
//the retry here was causing a lot of issues
//seems better to just accept the death of the command and re-issue as needed
throw te;
}
}
public virtual void Execute(CommandCallback cb, object cbData)
{
// this try/catch is to only handle ExecuteIsBusy
try
{
// there can be only one!
recorder.ExecuteIsBusy = true;
if (!recorder.Connected)
{
// "Slice.CommandBase.Execute: No recorder connected"
throw new System.Exception(Strings.Slice_CommandBase_Execute_Err1);
}
if (cb == null)
{
// "Slice.CommandBase.Execute: Callback can't be null"
throw new ArgumentException(Strings.Slice_CommandBase_Execute_Err2);
}
if (recorder.ProtocolVersion < MinimumProtocolVersion)
{
// "Slice.CommandBase.Execute: The recorder's protocol version does not support this command"
throw new System.Exception(Strings.Slice_CommandBase_Execute_Err3);
}
if (recorder.IsCanceled())
{
throw new CanceledException();
}
// this is the tail end of execute, do some bookeeping first
//Debug.Assert(false == command.AlreadyRun);
if (baseCommand.AlreadyRun)
{
baseCommand.GetNextSequenceNumber();
}
baseCommand.AlreadyRun = true;
baseCommand.ComputeCRCs();
var CommandBytes = baseCommand.ToBytes();
baseCommand.OriginalBytes = CommandBytes;
UserCallback = cb;
UserCallbackData = cbData;
IsSynchronous = false;
LogCommand(true);
ExecuteTime = DateTime.Now;
recorder.Execute(CommandBytes, ExecuteCallback, null, IO_Timeout);
}
catch (System.Exception ex)
{
APILogger.Log(ex);
// if an exception has happend the execute is actually started and execute is therefore not busy.
// After execute starts the only responses we get is thru ExecuteCallback
recorder.ExecuteIsBusy = false;
throw;
}
}
/// <summary>
/// returns a commandpacket object
/// this is necessary because commandpackets are specific to inheritted classes
/// and can contain different packing of bytes and different verification schemes
/// an example would be <see cref="SliceCommandPacketBase" />
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
protected abstract CommandPacketBase GetCommandPacket(byte[] buffer);
protected abstract CommandPacketBase GetCommandPacket();
/// <summary>
/// allows inheriting classes to add their own log statements to entries
/// when overriding, be sure to call base class implementation
/// the first line of the two dimensional array is the header information
/// from the command (like sequence number, type of command, etc)
/// </summary>
/// <param name="lines"></param>
public virtual void CommandToString(ref List<List<string>> lines)
{
}
/// <summary>
/// allows inheriting classes to add their own log statements to log entries
/// when overriding, make sure to call the base class to pick up statements
/// added by parent classes
/// the base class will add a line if there is an error
/// </summary>
/// <param name="lines"></param>
public virtual void ResponseToString(ref List<List<string>> lines)
{
if (null == baseResponse || baseResponse.Status != DFConstantsAndEnums.CommandStatus.StatusNoError)
{
lines.Add(new List<string>()
{
$"XXXXX ERROR XXXXX {baseResponse.Status.ToString()}"
});
}
}
public const string RESPONSESTART_STRING = "<- ";
private const string SPACE_STRING = " ";
public const string COMMANDSTART_STRING = "-> ";
/// <summary>
/// outputs a two dimensional array of log information into a single string
/// using a two dimensional array means that subclasses don't need to care
/// about how much data was already put into the arrays, they can just add
/// whatever data they need to report
/// </summary>
/// <param name="lines">two dimensional array of log data</param>
/// <param name="bSending"></param>
/// <returns></returns>
private string FormatLogEntry(List<List<string>> lines, bool bSending)
{
StringBuilder sb = new StringBuilder(200);
for (var i = 0; i < lines.Count; i++)
{
if (0 == i)
{
sb.Append(bSending ? COMMANDSTART_STRING : RESPONSESTART_STRING);
}
else { sb.Append(SPACE_STRING); }
for (var j = 0; j < lines[i].Count; j++)
{
if (j > 0) { sb.Append(", "); }
sb.Append(lines[i][j]);
}
}
return sb.ToString();
}
/// <summary>
/// outputs a single string for logging
/// </summary>
/// <param name="bSending">log outgoing or ingoing command</param>
/// <returns></returns>
private string GetFormattedLogEntry(bool bSending)
{
var lines = new List<List<string>>();
if (bSending)
{
baseCommand.GetPacketLogHeader(ref lines);
CommandToString(ref lines);
}
else
{
if (null != baseResponse)
{
baseResponse.GetPacketLogHeader(baseCommand, ref lines, ExecuteTime);
ResponseToString(ref lines);
}
}
return FormatLogEntry(lines, bSending);
}
}
}