//---------------------------------------------------------------------------------------------------
// <copyright file="SBGPCTDSensor.cpp" company="LiquidRobotics">
//
// Copyright (c) 2014 Liquid Robotics, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// This software is associated with a release.  Liquid Robotics makes no
// representation that the design, interfaces or component distribution will
// remain unchanged in future releases.
//
// </copyright>
//
// <summary>
// 	Implementation of SBGPCTDSensor, a boundary interface to a Seabird GPCTD.  The idea is to provide
//  interface functions to every valid command, but certain functions are omitted since the
//  class needs to be able to control some aspects in order to interact the sensor.  Some of these
//  are:
//
//		BaudRate (will change to the configured rate)
//		OutputExecutedTag (always false)
//		OutputFormat (always 1, decimal engineering units)
//		RS232ForceOn (always false since the sensor expects to be able to send commands)
//		TxRealTime (always true)
//
// </summary>
//
// <owner>Mike Cookson</owner>
//---------------------------------------------------------------------------------------------------

#include "SBGPCTDSensor.h"
#include "SBGPCTDSensorSettings.h"
#include "LqrSMACBooleanParser.h"
#include "LqrSMACEscapeStr.h"
#include "LqrSMACSerialListener.h"
#include "LqrSMACTimeCounter.h"
#include "LqrSMACWaitableThreadSafeQueue.h"
#include "nsextend.h"
#include "pugixml.hpp"
#include <boost/algorithm/string.hpp>
#include "boost/date_time/posix_time/posix_time.hpp"

using namespace std;
using namespace LiquidRobotics::SMACFramework;

static const unsigned MaxTries = 5;
static const unsigned ReactionTimeoutMS = 1000;
static const unsigned ShortResponseTimeoutMS = 5000;
static const unsigned LongResponseTimeoutMS = 10000;
static const unsigned MaxAccumulatedBuffer = 4*1024*1024;

namespace LiquidRobotics {
namespace SeabirdGPCTD {

struct CommandBuffer
{
	CommandBuffer(
			SignalSubscriber<SBGPCTDSensor::DataReceiveSignalType>& _originalSignalSubscriber,
			SBGPCTDSensor::DataReceiveSignalType& _forwardingSignal,
			LockableConceptMutex& _firingMutex,
			LockableConceptMutex& _commandMutex,
			Logger& _logger)

	: originalSignalSubscriber(_originalSignalSubscriber)
	, forwardingSignal(_forwardingSignal)
	, firingMutex(_firingMutex)
	, commandMutex(_commandMutex)
	, logger(_logger)
	{
		Logger::Locked(logger)->Debug << "(C) Buffer count = " << ++count << endl;
		connectionToOriginalSignal = originalSignalSubscriber.Subscribe(boost::bind(&CommandBuffer::OnDataRecieved, this, _1));
	}

	~CommandBuffer()
	{
		originalSignalSubscriber.Release(connectionToOriginalSignal);
		ForwardAllData();
		Logger::Locked(logger)->Debug << "(D) Buffer count = " << --count << endl;
	}

	void ForwardAllData()
	{
		string blob;
		while(blobs.ReadTest(blob))
			ForwardData(blob);
	}

	void ForwardData(string& data)
	{
		if (data.length())
		{
			ILockableConceptLock lock(firingMutex);
			forwardingSignal(data);
		}
	}

	void OnDataRecieved(string& data)
	{
		blobs.WriteExpand(data);
		data.clear();
	}

	SignalSubscriber<SBGPCTDSensor::DataReceiveSignalType>& originalSignalSubscriber;
	SBGPCTDSensor::DataReceiveSignalType& forwardingSignal;
	LockableConceptMutex& firingMutex;
	LockableConceptMutex& commandMutex;
	Logger& logger;
	ScopedConnectionPtr connectionToOriginalSignal;
	WaitableThreadSafeQueue<string> blobs;
	static unsigned count;
};

unsigned CommandBuffer::count = 0;

SBGPCTDSensor::SBGPCTDSensor(const SBGPCTDSensorSettings& _settings, Logger& _logger)
: asynchronousSample(asynchronousSampleSignal, firingMutex)
, rawSend(rawSendSignal, firingMutex)
, rawReceive(rawReceiveSignal, firingMutex)
, settings(_settings)
, logger(_logger)
, initialized(false)
, powered(false)
, powerStateKnown(false)
, sampling(false)
, asynchronousData(asynchronousDataSignal, firingMutex)
, commandData(commandDataSignal, firingMutex)
, expectedEnd0("S>")
, expectedEnd("\r\nS>")
, qsCounter(0,false)
{
}

SBGPCTDSensor::~SBGPCTDSensor()
{
	Shutdown();
}

void SBGPCTDSensor::EnterQuiescentMode()
{
	Logger::Locked(logger)->Debug << "EnterQuiescentMode()" << endl;
	ILockableConceptLock commandLock(commandMutex);

	if (!sampling)
		VerifyReacting();
	else
		EstablishCommsIfPortClosed();

	SendCommand("qs", false);

	ILockableConceptLock qsLock(qsMutex);
	qsCounter.Pause();
}

void SBGPCTDSensor::EstablishCommsIfPortClosed()
{
	ILockableConceptLock commandLock(commandMutex);

	if (!serialListener
	 || !connectionToSerialListener
	 || !serialPort
	 || !serialPort->IsOpen())

		EstablishComms(true);
}

bool SBGPCTDSensor::EstablishComms(bool throwOnFailure)
{
	Logger::Locked(logger)->Debug << "EstablishComms(" << BooleanToString(throwOnFailure) << ")" << endl;
	ILockableConceptLock commandLock(commandMutex);
	bool result = false;
	set<unsigned> possibleBaudRates;
	possibleBaudRates.insert(9600);
	possibleBaudRates.insert(38400);
	possibleBaudRates.insert(115200);

	unsigned desiredBaudRate = SerialPort::BaudRate2Num(settings.GetSerialPortSettings().GetBaudRate());

	if (possibleBaudRates.find(desiredBaudRate) == possibleBaudRates.end())
	{
		Logger::Locked(logger)->Warning << "Desired baud rate (" << desiredBaudRate << ") is invalid: selecting 9600 baud instead" << endl;
		desiredBaudRate = 9600;
	}

	if (!(result = TryComms(desiredBaudRate)))
		for (set<unsigned>::const_iterator ci = possibleBaudRates.begin(); ci != possibleBaudRates.end(); ++ci)
			if (*ci != desiredBaudRate)
			{
				if (!!(result = TryComms(*ci)))
				{
					try
					{
						ChangeBaudRates(desiredBaudRate);
					}
					catch(exception& ex)
					{
						Logger::Locked(logger)->Warning << "Failed to change baud rates to " << desiredBaudRate << ": " << ex.what() << endl;
						result = false;
						ClosePort();
					}
					break;
				}
			}

	Logger::Locked(logger)->Debug << "EstablishComms: " << BooleanToString(result) << endl;
	return result;
}

SBGPCTDSensor::ConfigData SBGPCTDSensor::GetConfigurationData()
{
	Logger::Locked(logger)->Debug << "GetConfigurationData()" << endl;
	const string content = SendCommand("getcd", "<ConfigurationData", "</ConfigurationData>");
	ConfigData result(content);
	if (logger.LogEnabled(Logger::LT_DEBUG))
		Logger::Locked(logger)->Debug << "GetConfigurationData: " << result.ToString() << endl;
	return result;
}

string SBGPCTDSensor::GetDisplayStatus()
{
	Logger::Locked(logger)->Debug << "GetDisplayStatus()" << endl;
	const string result = SendCommand("ds");
	return result;
}

string SBGPCTDSensor::GetDisplayCalibration()
{
	Logger::Locked(logger)->Debug << "GetDisplayCalibration()" << endl;
	const string result = SendCommand("getcc", "<CalibrationCoefficients", "</CalibrationCoefficients>");
	return result;
}

SBGPCTDSensor::HardwareData SBGPCTDSensor::GetHarwareData()
{
	Logger::Locked(logger)->Debug << "GetHarwareData()" << endl;
	const string content = SendCommand("gethd", "<HardwareData", "</HardwareData>");
	HardwareData result(content);
	if (logger.LogEnabled(Logger::LT_DEBUG))
		Logger::Locked(logger)->Debug << "GetHardwareData: " << result.ToString() << endl;
	return result;
}

SBGPCTDSensor::Sample SBGPCTDSensor::GetLastSample()
{
	Logger::Locked(logger)->Debug << "GetLastSample()" << endl;
	const string line = SendCommand("sl");
	Sample result(line);
	return result;
}

SBGPCTDSensor::Sample SBGPCTDSensor::GetLastPressureSample()
{
	Logger::Locked(logger)->Debug << "GetLastPressureSample()" << endl;
	const string line = SendCommand("slp");
	Sample result(line);
	return result;
}

string SBGPCTDSensor::GetStatusData()
{
	Logger::Locked(logger)->Debug << "GetStatusData()" << endl;
	const string result = SendCommand("getsd", "<StatusData", "</StatusData>");
	return result;
}

void SBGPCTDSensor::Initialize()
{
	Logger::Locked(logger)->Debug << "Initialize()" << endl;
	connectionToAsynchronousData = asynchronousData.Subscribe(boost::bind(&SBGPCTDSensor::OnAsynchronousData, this, _1));
	initialized = true;
}

bool SBGPCTDSensor::IsInitialized() const
{
	return initialized;
}

bool SBGPCTDSensor::IsPowered() const
{
	return powered;
}

bool SBGPCTDSensor::IsPowerStateKnown() const
{
	return powerStateKnown;
}

void SBGPCTDSensor::ResetLogging()
{
	Logger::Locked(logger)->Debug << "ResetLogging()" << endl;
	SendCommand("resetlogging");
}

SBGPCTDSensor::Sample SBGPCTDSensor::SampleNow(bool withPump)
{
	Logger::Locked(logger)->Debug << "SampleNow(" << BooleanToString(withPump) << ")" << endl;
	const string line = SendCommand(withPump ? "pts" : "ts");
	Sample result(line);
	return result;
}

SBGPCTDSensor::Sample::VectorPtr SBGPCTDSensor::SampleNowNoPump(unsigned sampleCount)
{
	Logger::Locked(logger)->Debug << "SampleNowNoPump(" << sampleCount << ")" << endl;
	// read the set of lines as a blob
	stringstream cmdstm;
	cmdstm << "tsn:" << sampleCount;
	const string content = SendCommand(cmdstm.str());

	// split the blob into lines
	vector<string> lines;
	boost::split(lines, content, boost::is_any_of("\r\n"));

	// create the array to be returned and reserve enough samples for each line
	Sample::VectorPtr result;
	result.reset(new Sample::Vector);
	result->reserve(lines.size());

	// for each line, push back the sample output
	for (vector<string>::const_iterator ci = lines.begin(); ci != lines.end(); ++ci)
	{
		try
		{
			Sample sample(*ci);
			result->push_back(sample);
		}
		catch(exception& ex)
		{
			Sample sample;
			sample.line = *ci;
			result->push_back(sample);
		}
	}

	return result;
}

SBGPCTDSensor::Sample SBGPCTDSensor::SamplePressureNow()
{
	Logger::Locked(logger)->Debug << "SamplePressureNow()" << endl;
	const string line = SendCommand("tp");
	Sample result(line);
	return result;
}

string SBGPCTDSensor::SendLowLevelCommand(const string& command)
{
	Logger::Locked(logger)->Debug << "SendLowLevelCommand(" << EscapeStr(command) << ")" << endl;
	ILockableConceptLock commandLock(commandMutex);

	string result;

	if (boost::iequals(command, "getcd")
	 || boost::iequals(command, "getsd")
	 || boost::iequals(command, "getcc")
	 || boost::iequals(command, "getec")
	 || boost::iequals(command, "gethd")
	 || boost::iequals(command, "ds")
	 || boost::iequals(command, "dc")
	 || boost::iequals(command, "sl")
	 || boost::iequals(command, "slp"))

		result = SendCommand(command);

	else if (boost::iequals(command, "qs"))
	{
		SendCommand(command, false);
		ILockableConceptLock qsLock(qsMutex);
		qsCounter.Pause();
	}

	else
	{
		string escaped = EscapeStr(command);
		boost::replace_all(escaped, "\"", "\\\"");

		stringstream msg;
		msg << "SendLowLevelCommand: '" << escaped << "' is not a supported command";
		throw invalid_argument(msg.str());
	}

	return result;
}

void SBGPCTDSensor::SetAutoRun(bool autoRun)
{
	Logger::Locked(logger)->Debug << "SetAutoRun(" << BooleanToString(autoRun) << ")" << endl;
	stringstream cmd;
	cmd << "autorun=" << (autoRun ? "y" : "n");
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetAutoSamplingInterval(unsigned seconds)
{
	Logger::Locked(logger)->Debug << "SetAutoSamplingInterval(" << seconds << ")" << endl;
	stringstream cmd;
	cmd << "interval=" << seconds;
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetAutoSamplingMinConductivity(float frequency)
{
	Logger::Locked(logger)->Debug << "SetAutoSamplingMinConductivity(" << frequency << ")" << endl;
	stringstream cmd;
	cmd << "mincondfreq=" << frequency;
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetAutoSamplingRealtimeTransmit(bool transmitRealtime)
{
	Logger::Locked(logger)->Debug << "SetAutoSamplingRealtimeTransmit(" << BooleanToString(transmitRealtime) << ")" << endl;
	stringstream cmd;
	cmd << "txrealtime=" << (transmitRealtime ? "y" : "n");
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetCustomPumpMode(bool customMode)
{
	Logger::Locked(logger)->Debug << "SetCustomPumpMode(" << BooleanToString(customMode) << ")" << endl;
	stringstream cmd;
	cmd << "custompumpmode=" << (customMode ? "y" : "n");
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetOxygenInstalled(bool installed)
{
	Logger::Locked(logger)->Debug << "SetOxygenInstalled(" << BooleanToString(installed) << ")" << endl;
	stringstream cmd;
	cmd << "oxygeninstalled=" << (installed ? "y" : "n");
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetPump(PumpStatus newStatus)
{
	stringstream msg;

	switch(newStatus)
	{
	case PUMP_OFF:	msg << "PUMP_OFF";						break;
	case PUMP_FAST:	msg << "PUMP_FAST";						break;
	case PUMP_SLOW:	msg << "PUMP_SLOW";						break;
	default:		msg << (unsigned) newStatus << "?";	break;
	}

	Logger::Locked(logger)->Debug << "SetPump(" << msg.str() << ")" << endl;

	switch(newStatus)
	{
	case PUMP_OFF:	SendCommand("pumpoff");		break;
	case PUMP_FAST:	SendCommand("pumpfast");	break;
	case PUMP_SLOW:	SendCommand("pumpslow");	break;

	default:
	{
		stringstream def;
		def << "SetPump: no such pump status (" << newStatus << ")";
		throw invalid_argument(def.str());
	}

	}
}

void SBGPCTDSensor::SetSampleNumber(unsigned sampleNumber)
{
	stringstream cmd;
	cmd << "setsamplenum=" << sampleNumber;
	SendCommand(cmd.str());
}

void SBGPCTDSensor::SetTime()
{
	using namespace boost::posix_time;
	using namespace boost::gregorian;
	ptime now = second_clock::local_time();
	date d = now.date();
	time_duration t = now.time_of_day();
	SetTime(d.year(), d.month(), d.day(), t.hours(), t.minutes(), t.seconds());
}

void SBGPCTDSensor::SetTime(unsigned year, unsigned month, unsigned day, unsigned hour, unsigned minute, unsigned second)
{
	stringstream stm;
	stm << "datetime=" << utos(month,10,2) << utos(day,10,2) << utos(year,10,4) << utos(hour,10,2) << utos(minute,10,2) << utos(second,10,2);
	SendCommand(stm.str());
}

void SBGPCTDSensor::SetPower(bool on)
{
	const string verb = boost::trim_copy(on ? settings.GetPowerOnVerb() : settings.GetPowerOffVerb());
	system(verb.c_str());
	powerStateKnown = true;
	powered = on;
}

void SBGPCTDSensor::Shutdown()
{
	Logger::Locked(logger)->Debug << "Shutdown()" << endl;
	ClosePort();
}

void SBGPCTDSensor::StartAutonomousSampling(bool ignoreNotWhileLogging)
{
	Logger::Locked(logger)->Debug << "StartAutonomousSampling()" << endl;
	try
	{
		SendCommand("start");
	}
	catch(exception& ex)
	{
		if (!ignoreNotWhileLogging || !boost::icontains(ex.what(), "cmd not allowed while logging"))
			throw;
	}
}

void SBGPCTDSensor::StopAutonomousSampling(bool ignoreOnlyWhileLoggingMessage)
{
	Logger::Locked(logger)->Debug << "StopAutonomousSampling()" << endl;

	try
	{
		SendCommand("stop");
	}
	catch(exception& ex)
	{
		if (!ignoreOnlyWhileLoggingMessage || !boost::icontains(ex.what(), "cmd only allowed while logging"))
			throw;
	}
}

SBGPCTDSensor::Header::VectorPtr SBGPCTDSensor::UploadHeaders()
{
	// read the set of lines as a blob
	const string content = SendCommand("uh");

	// split the blob into lines
	vector<string> lines;
	const string trimVer = boost::trim_copy(content);
	boost::split(lines, trimVer, boost::is_any_of("\r\n"), boost::token_compress_on);

	// create the array to be returned and reserve enough samples for each line
	Header::VectorPtr result;
	result.reset(new Header::Vector);
	result->reserve(lines.size());

	// for each line, push back the sample output
	stringstream msg;

	for (vector<string>::const_iterator ci = lines.begin(); ci != lines.end(); ++ci)
	{
	  if (ci->length())
	  {
		try
		{
			Header sample(*ci);
			result->push_back(sample);
		}
		catch(exception& ex)
		{
			Header sample;
			sample.line = *ci;
			result->push_back(sample);
		}
		if (logger.LogEnabled(Logger::LT_DEBUG))
		  msg << "{ " << result->back().ToString() << " }" << endl;
	  }
	}

	if (logger.LogEnabled(Logger::LT_DEBUG))
	    Logger::Locked(logger)->Debug << "UploadHeaders: {" << msg.str() << "}" << endl;

	return result;
}

SBGPCTDSensor::Sample::VectorPtr SBGPCTDSensor::UploadSamples(unsigned castNumber)
{
	// read the set of lines as a blob
	stringstream cmdstm;
	cmdstm << "uc" << castNumber;
	const string content = SendCommand(cmdstm.str());

	// split the blob into lines
	vector<string> lines;
	boost::split(lines, content, boost::is_any_of("\r\n"));

	// create the array to be returned and reserve enough samples for each line
	Sample::VectorPtr result;
	result.reset(new Sample::Vector);
	result->reserve(lines.size());

	// for each line, push back the sample output
	for (vector<string>::const_iterator ci = lines.begin(); ci != lines.end(); ++ci)
	{
		try
		{
			Sample sample(*ci);
			result->push_back(sample);
		}
		catch(exception& ex)
		{
			Sample sample;
			sample.line = *ci;
			result->push_back(sample);
		}
	}

	return result;
}

string SBGPCTDSensor::SendCommand(const string& command, bool expectPrompt)
{
	return SendCommand(command, "", "", expectPrompt);
}

string SBGPCTDSensor::SendCommand(const string& command, const string& startLine, const string& endLine, bool expectPrompt)
{
	ILockableConceptLock commandLock(commandMutex);

	if (!sampling)
		VerifyReacting();

	CommandBuffer commandBuffer(commandData, asynchronousDataSignal, firingMutex, commandMutex, logger);
	string blob;
	unsigned timeLeft;

	// send the message
	const string command2 = command+"\r";
	int commandTriesLeft = MaxTries;
	string response;
	bool responded = false;

	while(!responded && commandTriesLeft--)
	{
		DoWrite(command2);
		TimeCounter tc;
		bool gotEcho = false;
		bool gotBlank = false;
		while(!responded && (timeLeft = tc.Remaining(LongResponseTimeoutMS), timeLeft && commandBuffer.blobs.ReadTest(blob, timeLeft)))
		{
			if (!gotEcho)
			{
				size_t echo = blob.find(command+"\r\n");
				string fwd;

				if (echo != string::npos)
				{
					gotEcho = true;
					blob.erase(0,echo+command.length());
					if (echo) fwd = blob.substr(0,echo);
				}
				else if (boost::equals(blob, command))
				{
					gotEcho = true;
					blob.clear();
				}

				if (gotEcho)
				{
					Logger::Locked(logger)->Debug << "found echo" << endl;
					if (fwd.length()) commandBuffer.ForwardData(fwd);
				}
			}

			while(!gotBlank && blob.length())
			{
				size_t nl = blob.find("\r\n");
				if (!nl)
				{
					gotEcho = gotBlank = true;
					Logger::Locked(logger)->Debug << "found blank" << endl;
					blob.erase(0,2);

					if (!expectPrompt)
						responded = true;
				}
				else
				{
					string fwd = blob.substr(0,nl+2);
					commandBuffer.ForwardData(fwd);
					blob.erase(0,nl+2);
				}
			}
			if (gotBlank && !responded && boost::ends_with(blob, expectedEnd0))
			{
				Logger::Locked(logger)->Debug << "Response received after " << tc.GetCountSec() << " sec" << endl;
				blob.erase(blob.length()-expectedEnd0.length(), expectedEnd0.length());
				response = blob;
				responded = true;
			}
			else
				commandBuffer.ForwardData(blob);
		}

		if (responded)
		{
			// == find out what the new sampling state is and save it ==
			size_t fnd0 = response.rfind("<!--start logging-->");
			size_t fnd1 = response.rfind("<logging stopped/>");
			bool changed = true;

			if (fnd0 == fnd1)
			{
				if (fnd0 != string::npos)
					sampling = fnd0 > fnd1;
				else
					changed = false;
			}
			else if (fnd0 != string::npos)
				sampling = true;
			else
				sampling = false;


			if (changed)
				Logger::Locked(logger)->Info << "Note: sampling " << (sampling ? "STARTED" : "STOPPED") << endl;

			// look for "<Error" tag in the file and throw an exception if found
			const string ucase = boost::to_lower_copy(response);
			const size_t efind = ucase.find("<error");

			if (efind != string::npos)
			{
				size_t efind2 = (efind == string::npos) ? string::npos : ucase.find("\r", efind+1);
				pugi::xml_document doc;
				doc.load_buffer(response.c_str()+efind, ((efind2 == string::npos) ? response.length() : efind2)-efind);

				for (pugi::xml_node n = doc.first_child(); n; n = n.next_sibling())
					if (boost::iequals(n.name(), "error"))
					{
						string typeStr;
						string msgStr;

						for (pugi::xml_attribute a = n.first_attribute(); a; a = a.next_attribute())
						{
							if (boost::iequals(a.name(), "type"))
								typeStr = a.value();
							else if (boost::iequals(a.name(), "msg"))
								msgStr = a.value();
						}

						stringstream msg;
						msg << "Sensor returned " << typeStr << ": " << msgStr;
						Logger::Locked(logger)->Error << msg.str() << endl;

						if (boost::iequals(typeStr, "INVALID ARGUMENT"))
							throw invalid_argument(msg.str());
						else
							throw runtime_error(msg.str());
					}
			}

			// if the first tag was specified, then find it in the string and throw away everything before that
			if (startLine.length())
			{
				size_t fnd = response.find(startLine);

				if (fnd == string::npos)
					responded = false;
				else if (fnd != 0)
				{
					string sub = response.substr(0,fnd);
					commandBuffer.ForwardData(sub);
					response.erase(0,fnd);
				}
			}

			// if the end tag was specified, then find it and throw away everything after that
			if (responded && endLine.length())
			{
				size_t fnd = response.find(endLine);

				if (fnd == string::npos)
					responded = false;
				else
				{
					size_t fnd2 = response.find("\r\n", fnd+endLine.length());

					if (fnd2 != string::npos)
						response.erase(fnd2, response.length()-fnd2);
				}
			}
		}
	}

	// if no response, throw exception
	if (!responded)
	{
		stringstream msg;
		msg << "SendCommand: no response to '" << command << "' after " << MaxTries << " attempts";
		DoThrow(msg.str());
	}

	Logger::Locked(logger)->Debug << "SendCommand: response='" << EscapeStr(response) << "'" << endl;
	return response;
}

bool SBGPCTDSensor::TryComms(unsigned baudRate)
{
	Logger::Locked(logger)->Debug << "TryComms(" << baudRate << ")" << endl;
	bool communicating = false;
	ILockableConceptLock commandLock(commandMutex);

	// power up the device if not powered
	if (!powered)
	{
		const string trimmedCommand = boost::trim_copy(settings.GetPowerOnVerb());
		if (trimmedCommand.length())
			system(trimmedCommand.c_str());
		powerStateKnown = powered = true;
	}

	try
	{
		// if the listener isn't allocated or there's no connection to its callbacks or
		// the port is not allocated or its closed or if the baud rate doesn't match,
		// clean up the resources and re-open using the target baud rate
		if (!serialListener
		 || !connectionToSerialListener
		 || !serialPort
		 || !serialPort->IsOpen()
		 || (SerialPort::BaudRate2Num(serialPort->GetBaudRate()) != baudRate))
		{
			ClosePort();
			OpenPort(baudRate);
		}

		// now hit enter a few times and see if there's a response
		communicating = false;
		{ // (I'm using this indent so that the command buffer is deallocated afterward)
			const string wakeup = "\x8\x8\r";
			CommandBuffer commandBuffer(commandData, asynchronousDataSignal, firingMutex, commandMutex, logger);
			unsigned triesLeft = MaxTries;

			while(!communicating && triesLeft--)
			{
				DoWrite(wakeup);
				TimeCounter tc;
				string blob;
				bool gotResponse = false;
				unsigned timeLeft;

				while(!gotResponse && (timeLeft = tc.Remaining(ShortResponseTimeoutMS), timeLeft && commandBuffer.blobs.ReadTest(blob, timeLeft)))
				{
					if (!!(gotResponse = boost::ends_with(blob, expectedEnd)))
					{
						communicating = true;
						Logger::Locked(logger)->Debug << "Response received after " << tc.GetCountSec() << " sec" << endl;
					}
				}
			}
		}

		// determine if currently logging
		if (communicating)
		{
			try { sampling = CheckLogging(); }
			catch(...) { communicating = false; }
		}

		// if now communicating, send some initialization strings
		if (communicating)
		{
			if (sampling)
				SendCommand("stop");
			SendCommand("autorun=n");
			SendCommand("outputexecutedtag=n");
			SendCommand("outputformat=1");
			SendCommand("rs232forceon=n");
			SendCommand("txrealtime=y");
		}
	}
	catch(exception& ex)
	{
		Logger::Locked(logger)->Warning << "TryComms: failed to communicate at " << baudRate << " baud (" << ex.what() << ")" << endl;
		ClosePort();
		communicating = false;
	}

	Logger::Locked(logger)->Debug << "TryComms(" << baudRate << "): " << BooleanToString(communicating) << endl;
	return communicating;
}

bool SBGPCTDSensor::CheckLogging()
{
	Logger::Locked(logger)->Debug << "CheckLogging()" << endl;
	CommandBuffer commandBuffer(commandData, asynchronousDataSignal, firingMutex, commandMutex, logger);
	unsigned timesLeft = MaxTries;
	bool gotResponse = false;
	bool logging = false;

	while(!gotResponse && timesLeft--)
	{
		string dsCommand = "ds\r";
		DoWrite(dsCommand);

		TimeCounter tc;
		unsigned timeLeft;

		while(!gotResponse && !!(timeLeft = tc.Remaining(LongResponseTimeoutMS)))
		{
			string blob;
			bool handled = false;

			if (commandBuffer.blobs.ReadTest(blob, timeLeft) && boost::ends_with(blob, expectedEnd))
			{
				if (boost::icontains(blob, "logging"))
				{
					gotResponse = true;
					logging = !boost::icontains(blob, "not logging");
					handled = true;
					Logger::Locked(logger)->Debug << "Response received after " << tc.GetCountSec() << " sec" << endl;
				}
			}
			else if (boost::iequals(blob, "ds\r\n"))
				blob.clear();

			if (!handled)
				commandBuffer.ForwardData(blob);
		}
	}

	if (!gotResponse)
		DoThrow("CheckLogging: no response to DS command");

	Logger::Locked(logger)->Debug << "CheckLogging(): " << BooleanToString(logging) << endl;
	return logging;
}

void SBGPCTDSensor::OnAsynchronousData(string& data)
{
	if (logger.LogEnabled(Logger::LT_DEBUG))
		Logger::Locked(logger)->Debug << "OnAsynchronousData: '" << EscapeStr(data) << "'" << endl;

	if (data.length())
	{
		Sample sample(data);
		if (sample.HaveReading(sample.condSm) && sample.HaveReading(sample.tempC) && sample.HaveReading(sample.pressure))
		{
			Logger::Locked(logger)->Debug << "Converted sample: " << sample.ToString() << endl;
			ILockableConceptLock lock(firingMutex);
			asynchronousSampleSignal(*this, sample);
		}
	}
}

void SBGPCTDSensor::OnSerialReceive(SerialListener& sender, const ByteArray& receivedData)
{
	Logger::Locked(logger)->Debug << "OnSerialReceive: " << receivedData.size() << " bytes" << endl;
	{ ILockableConceptLock qsLock(qsMutex); qsCounter.Reset(0, true); }

	// if data received, append it to the local buffer
	if (receivedData.size())
	{
		const string temp = receivedData.ToASCIIString();
		receivedDataBuffer.write(temp.data(), temp.length());
	}

	// when the end of the contiguous data is found and the buffer isn't empty,
	// fire data to any commands that are running (should be no more than one)
	// and forward anything left to the handlers of asynchronous data
	if ((!receivedData.size() && receivedDataBuffer.tellp()) || (receivedDataBuffer.tellp() >= MaxAccumulatedBuffer))
	{
		string completedData = receivedDataBuffer.str();
		receivedDataBuffer.str("");

		if (logger.LogEnabled(Logger::LT_DEBUG))
			Logger::Locked(logger)->Debug << "Completed receive (" << completedData.length() << " bytes) " << EscapeStr(completedData) << endl;

		if (completedData.length())
		{
			// echo received data to subscribers
			{
				ILockableConceptLock lock(firingMutex);
				rawReceiveSignal(*this, completedData);
			}

			{
				ILockableConceptLock lock(firingMutex);

				// echo command data to internal subscribers waiting for command responses, buffer may be modified
				commandDataSignal(completedData);

				// if there's anything left, echo to other internal subscribers (assumed data is not related to a command)
				if (completedData.length())
					asynchronousDataSignal(completedData);
			}
		}
	}
}

void SBGPCTDSensor::ChangeBaudRates(unsigned baudRate)
{
	if (!sampling)
		VerifyReacting();

	stringstream cmd;
	cmd << "baudrate=" << baudRate;
	stringstream check;
	check << "setting baud rate to " << baudRate;

	CommandBuffer commandBuffer(commandData, asynchronousDataSignal, firingMutex, commandMutex, logger);
	string blob;
	unsigned timeLeft;
	bool success = true;
	string failMessage;

	for (unsigned pass = 1; success && (pass <= 2); ++pass)
	{
		// first send the change command and wait for the expected response; if successful, close the port
		// open at the new rate
		if (success)
		{
			DoWrite(cmd.str()+"\r");

			TimeCounter tc;
			bool responded = false;
			while(!responded && (timeLeft = tc.Remaining(ShortResponseTimeoutMS), timeLeft && commandBuffer.blobs.ReadTest(blob, timeLeft)))
			{
				if (boost::icontains(blob, check.str()))
				{
					Logger::Locked(logger)->Debug << "Response received after " << tc.GetCountSec() << " sec" << endl;
					responded = true;
					if (pass == 1)
					{
						ClosePort();
						OpenPort(baudRate);
					}
				}
			}

			if (!responded)
			{
				stringstream msg;
				msg << "No response to change baud rate command (" << pass << ")";
				failMessage = msg.str();
				success = false;
			}
		}

		// step 2 is to wait for the end prompt at the new baud rate (don't care if we dont' get it
		// on the first pass, but it needs to be consumed so it doesn't interfere with the second
		// pass command)
		if (success)
		{
			TimeCounter tc;
			bool responded = false;
			while(!responded && (timeLeft = tc.Remaining(ShortResponseTimeoutMS), timeLeft && commandBuffer.blobs.ReadTest(blob, timeLeft)))
			{
				if (boost::ends_with(blob, expectedEnd0))
				{
					Logger::Locked(logger)->Debug << "Response received after " << tc.GetCountSec() << " sec (" << pass << ")" << endl;
					responded = true;
				}
			}

			if (!responded && (pass > 1))
			{
				stringstream msg;
				msg << "End prompt not received (" << pass << ")";
				failMessage = msg.str();
				success = false;
			}
		}
	}

	if (!success)
		DoThrow(failMessage);
}

void SBGPCTDSensor::ClosePort()
{
	ILockableConceptLock lock(commandMutex);

	if (serialListener)
	{
		{ ILockableConceptLock lock(serialListener->FiringMutex()); connectionToSerialListener.reset(); }
		serialListener->Shutdown();
		serialListener.reset();
	}

	if (serialPort && serialPort->IsOpen())
		serialPort->Close();

	serialPort.reset();
	{ ILockableConceptLock qsLock(qsMutex); qsCounter.Reset(0,false); }
}

void SBGPCTDSensor::OpenPort(unsigned baudRate)
{
	ILockableConceptLock lock(commandMutex);
	string portName;
	ClosePort();

	try
	{
		const SerialPortSettings& spSettings = settings.GetSerialPortSettings();
		portName = spSettings.GetPortName();
		serialPort.reset(new SerialPort(portName));
		serialPort->Open(SerialPort::NumToBaudRate(baudRate),
				settings.GetSerialPortSettings().GetCharacterSize(),
				settings.GetSerialPortSettings().GetParity(),
				settings.GetSerialPortSettings().GetStopBits(),
				settings.GetSerialPortSettings().GetFlowControl());
		serialPort->FlushAll();

		serialListener.reset(new SerialListener(*serialPort, settings.GetSerialListenerSettings()));
		connectionToSerialListener = serialListener->Subscribe(boost::bind(&SBGPCTDSensor::OnSerialReceive, this, _1, _2));
		serialListener->Initialize();
	}
	catch(exception& ex)
	{
		Logger::Locked(logger)->Error << "Caught " << typeid(ex).name() << " while opening " << portName << " at " << baudRate << " baud: " << ex.what() << endl;
		ClosePort();
		throw;
	}

	{ ILockableConceptLock qsLock(qsMutex); qsCounter.Reset(0,false); }
}

void SBGPCTDSensor::VerifyReacting()
{
	double remaining;

	{
		ILockableConceptLock qsLock(qsMutex);
		remaining = qsCounter.IsRunning() ? qsCounter.RemainingSec(110) : -1.0;
	}

	Logger::Locked(logger)->Debug << "VerifyReacting() -- remaining=" << remaining << endl;

	// don't bother checking unless there's no time left on the clock
	if (remaining > 0.0) return;

	int triesLeft = MaxTries;
	const string prompt = "\r";
	TimeCounter tc;
	bool reacting = false;
	CommandBuffer commandBuffer(commandData, asynchronousDataSignal, firingMutex, commandMutex, logger);

	while(!reacting && triesLeft--)
	{
		DoWrite(prompt);

		unsigned timeLeft;
		TimeCounter tc;
		string blob;

		while(!reacting && (timeLeft = tc.Remaining(ReactionTimeoutMS), timeLeft && commandBuffer.blobs.ReadTest(blob, timeLeft)))
		{
			 if (boost::ends_with(blob, expectedEnd))
				 reacting = true;
			 else
				 commandBuffer.ForwardData(blob);
		}
	}

	if (!reacting)
		DoThrow("Sensor is not reacting");
}

void SBGPCTDSensor::DoWrite(const string& msg)
{
	if (!serialPort)
		DoThrow("DoWrite: no serial port");

	if (logger.LogEnabled(Logger::LT_DEBUG))
		Logger::Locked(logger)->Debug << "Sending " << EscapeStr(msg) << endl;

	serialPort->Write(msg);

	ILockableConceptLock lock(firingMutex);
	rawSendSignal(*this, msg);
}

void SBGPCTDSensor::DoThrow(const string& msg)
{
	Logger::Locked(logger)->Error << "Throwing " << msg << endl;
	throw runtime_error(msg);
}

}
}



