//--------------------------------------------------------------------------------
// <copyright file="SBGPCTDController.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 the SBGPCTDController class, which implements the controller
//  role business logic for the SeabirdGPCTD program
// </summary>
//
// <owner>Mike Cookson</owner>
//--------------------------------------------------------------------------------

#include "LqrSMACByteArray.h"
#include "LqrSMACEscapeStr.h"
#include "LqrSMACLogger.h"
#include "LqrSMACTimeCounter.h"
#include "LqrSMACUptime.h"
#include "SBGPCTDController.h"
#include "SBGPCTDSensor.h"
#include "nsextend.h"
#include "SensorControllerRunFileCmd.h"
#include <boost/filesystem.hpp>

// --- command classes ---
#include "SBGPCTDAutoRunCmd.h"
#include "SBGPCTDExpertCmd.h"
#include "SBGPCTDGetStatusCmd.h"
#include "SBGPCTDLoggingCmd.h"
#include "SBGPCTDPowerCmd.h"
#include "SBGPCTDStartCmd.h"
#include "SBGPCTDStopCmd.h"

using namespace std;
using namespace LiquidRobotics::SMACFramework;

inline string absfilename(const string& filename)
{
	using namespace boost::filesystem;
	string result;

	try
	{
		result = absolute(filename).string();
	}
	catch(...)
	{
		result.clear();
	}

	return result;
}

namespace LiquidRobotics {
namespace SeabirdGPCTD {

SBGPCTDController::SBGPCTDController(const SBGPCTDControllerSettings& _settings, IObjectBroker& _broker, Logger& _logger, GPSSignalSubscriber::Ptr _gpsSubscriber, ShutdownSignalSubscriber::Ptr _shutdownSubscriber)
: SBGPCTDControllerBase(_broker, _logger, _gpsSubscriber, _shutdownSubscriber)
, sampleReport(sampleReportSignal, firingMutex)
, serialNumberReport(serialNumberReportSignal, firingMutex)
, sampleTelemetryChange(sampleTelemetryChangeSignal, firingMutex)
, internalSampleReport(internalSampleReportSignal, firingMutex)
, settings(_settings)
, lat(0)
, lon(0)
, hasGPS(false)
, sensorCommunicating(false)
, o2Installed(false)
, backgroundSem(0)
, backgroundSampling(false)
, backgroundRunning(false)
, csvSamples(settings.GetFileSetSettings())
, persistFileName(absfilename(settings.GetPersistentXMLFileName()))
{
}

SBGPCTDController::~SBGPCTDController()
{
}

void SBGPCTDController::Initialize()
{
	Logger::Locked(logger)->Info << "SBGPCTDController::Initialize()" << endl;

	bool shutdownBase = false;

	try
	{
		persist.reset(new SBGPCTDControllerPersistSettings);

		if (persistFileName.length())
		{
			try
			{
				persist->Load(persistFileName);
			}
			catch(exception& ex)
			{
				Logger::Locked(logger)->Warning << "Failed to load persistent settings; loading default values (" << ex.what() << ")" << endl;
				persist->SetDefaults();

				try
				{
					persist->Save(persistFileName);
				}
				catch(exception& ex)
				{
					Logger::Locked(logger)->Warning << "Failed to save persistent settings (ignored; " << ex.what() << ")" << endl;
				}
			}
		}
	}
	catch(exception& ex)
	{
		Logger::Locked(logger)->Error << "SBGPCTDController::Initialize: caught exception while creating/loading persistant settings (" << typeid(ex).name() << "/" << ex.what() << ")" << endl;

		if (!persist)
		{
			stringstream msg;
			msg << "Failed to allocate the persistent settings class (" << typeid(ex).name() << "/" << ex.what() << ")";
			throw runtime_error(msg.str());
		}

		persist->SetDefaults();

		if (persistFileName.length())
			try { persist->Save(settings.GetPersistentXMLFileName()); } catch(...) { }
	}

	try
	{
		o2Installed = false;
		SBGPCTDControllerBase::Initialize();
		shutdownBase = true;

		csvSamples.Initialize();

		CSVSample sample;
		sample += "Conductivity(S/m)";
		sample += "Temperature(C)";
		sample += "Pressure(decibars)";
		sample += "Oxygen(Hz)";
		csvSamples.RegisterSampleHeaders("CTDO", sample);

		CSVSample devInfo;
		devInfo += "SerialNumber";
		devInfo += "SoftwareVersion";
		csvSamples.RegisterSampleHeaders("DEVINFO", devInfo);

		CSVSample rawReceive;
		rawReceive += "Message";
		csvSamples.RegisterSampleHeaders("RECV", rawReceive);

		CSVSample rawSend;
		rawSend += "Message";
		csvSamples.RegisterSampleHeaders("SEND", rawSend);

		CSVSample startSet;
		startSet += "Period(sec)";
		startSet += "BlockSize";
		startSet += "FlushTime(sec)";
		startSet += "OffTime(sec)";
		csvSamples.RegisterSampleHeaders("START", startSet);

		SBGPCTDSensor::Ptr sens = GetSensorPtr(false);
	}
	catch(exception& ex)
	{
		Logger::Locked(logger)->Error << "SBGPCTDController::Initialize: caught exception while initializing base and getting sensor pointer (" << typeid(ex).name() << "/" << ex.what() << ")" << endl;

		// forget initialization here
		if (shutdownBase)
			try { Base::Shutdown() ; } catch(...) { }

		throw;
	}

	// items that depend on the board address from PayloadManagerClient
	// have to start a bit later after his function returns since
	// PayloadManagerClient isn't fully initialized yet
	Thread stage2(boost::bind(&SBGPCTDController::StartupStage2, this));
}

void SBGPCTDController::Shutdown()
{
	// if the sensor is allocated, then clear its events and shut it down
	if (sensorPtr)
	{
		// stop asynchonrous sampling
		StopSampling();

		// turn off the power to the sensor
		sensorPtr->SetPower(false);

		// release event subscriptions
		sensorPtr->asynchronousSample.Release(connectionToAsynchronousSample);
		sensorPtr->rawReceive.Release(connectionToRawReceive);
		sensorPtr->rawSend.Release(connectionToRawSend);

		// shut down the sensor and clear it
		try { sensorPtr->Shutdown(); } catch(...) { }
	}

	try { Base::Shutdown() ; } catch(...) { }
}

void SBGPCTDController::ConfigureBlockReporting()
{
	sampleTelemetryChangeSignal(*this, persist->GetReportBlockSize(), persist->GetSamplePeriodSec());
}

SBGPCTDSensor::Ptr SBGPCTDController::GetSensorPtr(bool turnOn)
{
	Logger::Locked(logger)->Info << "SBGPCTDController::GetSensorPtr(turnOn=" << BooleanToString(turnOn) << ")" << endl;

	// serializing access to the pointer
	ILockableConceptLock lock(getPtrMutex);
	SBGPCTDSensor::Ptr result = sensorPtr;
	bool justCreated = false;

	// if no pointer allocated, allocate the sensor and wire it up
	if (!result)
	{
		sensorCommunicating = false;

		try
		{
			// instantiate the sensor interface, then connect to call back events and finally call initialize
			sensorPtr.reset(new SBGPCTDSensor(settings.GetSensorSettings(), logger));
			result = sensorPtr;
			justCreated = true;
			connectionToAsynchronousSample = sensorPtr->asynchronousSample.Subscribe(boost::bind(&SBGPCTDController::OnAsynchronousSample, this, _1, _2));
			connectionToRawReceive = sensorPtr->rawReceive.Subscribe(boost::bind(&SBGPCTDController::OnRawReceive, this, _1, _2));
			connectionToRawSend = sensorPtr->rawSend.Subscribe(boost::bind(&SBGPCTDController::OnRawSend, this, _1, _2));
			sensorPtr->Initialize();
		}
		catch(...)
		{
			if (sensorPtr)
			{
				sensorPtr->asynchronousSample.Release(connectionToAsynchronousSample);
				sensorPtr->rawReceive.Release(connectionToRawReceive);
				sensorPtr->rawSend.Release(connectionToRawSend);
			}

			sensorPtr.reset();
			throw;
		}
	}

	// if not communicating with the sensor yet ...
	if (sensorPtr && (!sensorCommunicating || !sensorPtr->IsPowered()) && turnOn)
	{
		try
		{
			// attempt to establish communication, and if that's successful ...
			if (!!(sensorCommunicating = sensorPtr->EstablishComms(true)))
			{
				// default state is not sampling and pump off
				sensorPtr->StopAutonomousSampling();
				sensorPtr->SetPump(SBGPCTDSensor::PUMP_OFF);

				// determine if there's an oxygen sensor installed and get serial number
				SBGPCTDSensor::HardwareData hardware = sensorPtr->GetHarwareData();
				o2Installed = IsO2Installed(hardware);
				const string newSerialNumber = GetSerialNumber(hardware);
				const string newSoftwareVersion = hardware.firmwareVersion+" "+hardware.firmwareDate;

				if (persist && persist->GetLoggingEnabled())
				{
					CSVSample devInfo;
					devInfo += newSerialNumber;
					devInfo += newSoftwareVersion;

					timeval tv = SBGPCTDSensor::Sample::GetTimestamp();
					csvSamples.WriteSample("DEVINFO", tv, lat, lon, devInfo);
				}

				// forward serial number to subscribers
				if (!boost::equals(serialNumber, newSerialNumber) || !boost::equals(newSoftwareVersion, deviceSoftwareVersion))
				{
					serialNumber = newSerialNumber;
					ILockableConceptLock lock(firingMutex);
					serialNumberReportSignal(*this, newSerialNumber, newSoftwareVersion);
				}

#if 0
				sensorPtr->GetConfigurationData();
				sensorPtr->GetDisplayCalibration();
				sensorPtr->GetDisplayStatus();
				sensorPtr->GetHarwareData();
				sensorPtr->GetLastPressureSample();
				sensorPtr->GetLastSample();
				sensorPtr->GetStatusData();
				sensorPtr->UploadHeaders();
				sensorPtr->SetPump(SBGPCTDSensor::PUMP_FAST);
				sleep(5);
				sensorPtr->SetPump(SBGPCTDSensor::PUMP_SLOW);
				sleep(5);
				sensorPtr->SetPump(SBGPCTDSensor::PUMP_OFF);
				sleep(5);
				sensorPtr->SetAutoSamplingInterval(1);
				sensorPtr->StartAutonomousSampling();
				sleep(10);
				sensorPtr->StopAutonomousSampling();
#endif
			}
		}
		catch(exception& ex)
		{
			Logger::Locked(logger)->Error << "SBGPCTDController::GetSensorPtr: caught " << typeid(ex).name() << ": " << ex.what() << endl;

			try
			{
				SetPower(false);
			}
			catch(exception& ex)
			{
				Logger::Locked(logger)->Error << "SBGPCTDController::GetSensorPtr: caught exception while turning power off (ignored; " << typeid(ex).name() << "/" << ex.what() << ")" << endl;
			}
		}
	}
	else if (sensorPtr && !sensorCommunicating && justCreated)
	{
		try
		{
			SetPower(false);
		}
		catch(exception& ex)
		{
			Logger::Locked(logger)->Error << "SBGPCTDController::GetSensorPtr: caught exception while turning power off (ignored; " << typeid(ex).name() << "/" << ex.what() << ")" << endl;
		}
	}

	Logger::Locked(logger)->Info << "SBGPCTDController::GetSensorPtr: " << BooleanToString(!!result) << endl;
	return result;
}

SBGPCTDControllerPersistSettings& SBGPCTDController::GetPersistSettings() const
{
	return *persist;
}

const SBGPCTDControllerSettings& SBGPCTDController::GetSettings() const
{
	return settings;
}

void SBGPCTDController::OnGPSSampleEvent(const GPSSampleEvent& sample)
{
	if (sample.HasGPS())
	{
		sample.GetGPS(lat, lon);
		hasGPS = true;
	}
}

void SBGPCTDController::OnShutdownEvent()
{
	Base::OnShutdownEvent();
}

SBGPCTDController::CommandPtr SBGPCTDController::ParseCommand(ParsingContext& context, bool internalCmd)
{
	CmdLineParser parser(context.GetCmd());
	CommandPtr result;
	const string serverVerb = "CTD";

	if (!!(result = EventLib::SensorControllerRunFileCmd<TCon,TCmd,TCID,TEx>::Parse(serverVerb, "--run", *this, parser, context, logger))
	 || !!(result = SBGPCTDAutoRunCmd::Parse(serverVerb, "--autorun", *this, parser, context, logger))
	 || !!(result = SBGPCTDExpertCmd::Parse(serverVerb, "--expert", *this, parser, context, logger))
	 || !!(result = SBGPCTDLoggingCmd::Parse(serverVerb, "--log-raw", *this, parser, context, logger, true))
	 || !!(result = SBGPCTDLoggingCmd::Parse(serverVerb, "--log-samples", *this, parser, context, logger, false))
	 || !!(result = SBGPCTDPowerCmd::Parse(serverVerb, "--power", *this, parser, context, logger))
	 || !!(result = SBGPCTDStartCmd::Parse(serverVerb, "--start", *this, parser, context, logger))
	 || !!(result = SBGPCTDGetStatusCmd::Parse(serverVerb, "--status", *this, parser, context, logger))
	 || !!(result = SBGPCTDStopCmd::Parse(serverVerb, "--stop", *this, parser, context, logger))
		 // add more command initializations here
	)

		;

	else
	{
		stringstream msg;
		msg << "Unrecognized command '" << context.GetCmd() << "'";

		throw invalid_argument(msg.str());
	}

	return result;
}

void SBGPCTDController::OnAsynchronousSample(SBGPCTDSensor& sender, const SBGPCTDSensor::Sample& sample)
{
	// making a copy of the event and adding latitude and longitude
	SBGPCTDSensor::Sample temp = sample;
	temp.latitude = lat;
	temp.longitude = lon;

	// logging the reading
	if (persist->GetLoggingEnabled())
	{
		Logger::Locked(logger)->Debug << "Logging sample to file set" << endl;

		// put the sample columns into a sample
		CSVSample csvSample;
		csvSample += temp.condSmStr;
		csvSample += temp.tempCStr;
		csvSample += temp.pressureStr;
		csvSample += temp.disOxyHzStr;

		// write the sample
		csvSamples.WriteSample("CTDO", temp.tv, temp.latitude, temp.longitude, csvSample);
	}
	else
		Logger::Locked(logger)->Debug << "Sample logging is disabled (nothing logged)" << endl;

	// forwarding the event to any local subscriber
	ILockableConceptLock lock(firingMutex);
	internalSampleReportSignal(sender, temp);
}

string SBGPCTDController::GetStatus()
{
	stringstream msg;

	if (!sensorPtr)
		msg << "NotInitialized";
	else if (!sensorCommunicating)
		msg << "NotCommunicating";
	else if (!backgroundThread)
		msg << "Idle";
	else if (backgroundSampling)
		msg << "Sampling";
	else
		msg << "NotSampling";

	msg << " Port=" << settings.GetSensorSettings().GetSerialPortSettings().GetPortName()
		<< " Enable=" << BooleanToString(true)
		<< " Pwr=" << (sensorPtr->IsPowerStateKnown() ? BooleanToString(sensorPtr->IsPowered()) : "unknown");

	if (persist)
		msg << " Intervl=" << persist->GetSamplePeriodSec() << " sec"
		    << " Size=" << persist->GetReportBlockSize() << " smpl/pkt"
		    << " Off-Tm=" << persist->GetDutyCycleSec()
		    << " Flush-Tm=" << persist->GetFlushTimeSec();

	if (sensorPtr && sensorCommunicating)
		msg << " Oxygen=" << BooleanToString(o2Installed);

	if (persist)
	{
		stringstream logmsg;

		if (persist->GetLoggingEnabled())
			logmsg << "samples";

		if (persist->GetRawLoggingEnabled())
		{
			if (logmsg.tellp())
				logmsg << "+";

			logmsg << "raw";
		}

		if (!logmsg.tellp())
			logmsg << "disabled";

		msg << " AutoRun=" << BooleanToString(persist->GetAutoRun())
		    << " Logging=" << logmsg.str();
	}

	const string result = msg.str();
	return result;
}

bool SBGPCTDController::IsOxygenInstalled() const
{
	return o2Installed;
}

void SBGPCTDController::SavePersistentSettings()
{
	if (persist && persistFileName.length())
	{
		Logger::Locked(logger)->Debug << "Saving persistent settings to " << persistFileName << endl;
		persist->Save(persistFileName);
	}
	else
		Logger::Locked(logger)->Warning << "Not saving persistent settings (persist=" << BooleanToString(!!persist) << " filename=" << persistFileName << ")" << endl;
}

void SBGPCTDController::SetPower(bool on)
{
	SBGPCTDSensor::Ptr sens = GetSensorPtr(on);

	if (!sens->IsPowerStateKnown() || (!!on != sens->IsPowered()))
	{
		sens->SetPower(on);
		if (!on) sensorCommunicating = false;
	}
}

void SBGPCTDController::StartSampling()
{
	StopSampling();

	backgroundSem.SetCount(0);
	backgroundRunning = true;
	backgroundThread.reset(new Thread(boost::bind(&SBGPCTDController::Background, this,
			persist->GetFlushTimeSec(), persist->GetSamplePeriodSec(), persist->GetReportBlockSize(),
			persist->GetDutyCycleSec())));
}

void SBGPCTDController::StopSampling()
{
	// if the background thread exists ...
	ThreadPtr temp = backgroundThread;
	if (temp)
	{
		// mark the background thread as not running and signal
		// it; it will then notice the flag is false and exit
		backgroundRunning = false;
		backgroundSem.SetCount(1);

		// wait for the thread to exit
		temp->join();

		// just resetting the background thread var to be sure it's
		// nil (this is also done by the background thread)
		backgroundThread.reset();
	}

	// calling this function releases any waiting samples
	ConfigureBlockReporting();
}

// implementing this function allows the controller to configure which
// formatter is used: the default is "xml", plain text is "basic"
string SBGPCTDController::GetDefaultOutputFormatter() const
{
	return settings.GetDefaultOutputFormatter();
}

/*! \brief Help class that provides a thread-safe queue of samples
 *  \sa SBGPCTDController, SBGPCTDController::Background()
 *
 * The Background() member function of SBGPCTDController, which performs background sample
 * polling, uses this class to temporarily hook the internal sample signal and create
 * a queue that Background() can read from.  A separate Semaphore is used since Background()
 * is checking a semaphore anyway to determine if it should exit.
 *
 * This class is used internally to SBGPCTDController
 */
struct BackgroundSampleCollector
{
	/*! \brief Class constructor
	 *
	 * Saves references to the command arguments and subscribes to the asynchronous sample signal.
	 */
	BackgroundSampleCollector(
			SignalSubscriber<SBGPCTDSensor::AsynchronousSampleSignalType>& _subscriber,		/**< The signal to subscribe to */
			Semaphore& _sem																	/**< A semaphore that is posted whenever a new sample is written to the queue */
			)
	: subscriber(_subscriber)
	, sem(_sem)
	{
		subscription = subscriber.Subscribe(boost::bind(&BackgroundSampleCollector::OnSample, this, _1, _2));
	}

	/*! \brief Class destructor
	 *
	 * Releases the subscription to asynchronous sample signal.
	 */
	~BackgroundSampleCollector()
	{
		subscriber.Release(subscription);
	}

	/*! \brief Asynchornous sample handler
	 *
	 * Writes a sample to the thread safe queue and signals that there is something
	 * new to be read.
	 */
	void OnSample(
			SBGPCTDSensor& sender,					/**< Reference of the sender of the event (the sensor interface) */
			const SBGPCTDSensor::Sample& sample		/**< The sample that was received */
			)
	{
		samples.WriteExpand(sample);
		sem.Post();
	}

	SignalSubscriber<SBGPCTDSensor::AsynchronousSampleSignalType>& subscriber;	//!<  References the subscriber for asynchronous samples
	Semaphore& sem;																//!<  References the semaphore to be signaled when samples come in
	ScopedConnectionPtr subscription;											//!<  Subscription to the asynchronous sample event
	ThreadSafeQueue<SBGPCTDSensor::Sample> samples;								//!<  Queue of samples received
};

//! Shortcut for a smart pointer to a TimerCounter instance
typedef boost::shared_ptr<TimeCounter> TimeCounterPtr;


inline bool WaitSec(Semaphore& sem, bool& running, unsigned sec, TimeCounterPtr& tc, bool breakOnSem = false)
{
	if (!tc)
		tc.reset(new TimeCounter);

	unsigned timeLeft;

	while(running && !!(timeLeft = tc->Remaining(1000*sec)))
		if (sem.TimedWait(timeLeft, false) && breakOnSem)
			break;

	return running && timeLeft;
}

void SBGPCTDController::Background(unsigned flushSec, unsigned periodSec, unsigned blockSize, unsigned offSec)
{
	//const unsigned cycleTime = periodSec*blockSize+((offSec <= flushSec) ? flushSec : (flushSec+offSec));
	Logger::Locked(logger)->Info << "(B) Starting background polling thread with flushSec="
			<< flushSec << " periodSec=" << periodSec << " blockSize=" << blockSize << " offSec=" << offSec << endl;

	CSVSample startSet;
	startSet += utos(periodSec);
	startSet += utos(blockSize);
	startSet += utos(flushSec);
	startSet += utos(offSec);
	timeval stamp = SBGPCTDSensor::Sample::GetTimestamp();
	csvSamples.WriteSample("START", stamp, lat, lon, startSet);

	//double nextTarget = GetSystemUptimeDouble();
	bool pumpOn = false;
	bool first = true;
	unsigned cycles = 0;
	TimeCounter sinceClockSetting(0,false);

	backgroundSampling = false;

	while(backgroundRunning)
	{
		TimeCounter cycleCounter;
		SBGPCTDSensor::Ptr sens;

		try
		{
			// turn on the pump fast to clear the line if a flush time was specified
			sens = GetSensorPtr();

			if (first)
			{
				Logger::Locked(logger)->Info << "(B) Turning off sampling since and setting the interval since it's the first time through" << endl;
				first = false;
				sens->StopAutonomousSampling();
				backgroundSampling = false;
				pumpOn = false;
				sens->SetAutoSamplingInterval(periodSec);
			}

			if (flushSec && !pumpOn)
			{
				Logger::Locked(logger)->Info << "(B) Starting flush (" << flushSec << " sec)" << endl;
				sens->SetPump(SBGPCTDSensor::PUMP_FAST);
				pumpOn = true;
				TimeCounterPtr tcp;
				WaitSec(backgroundSem, backgroundRunning, flushSec, tcp);
			}

			// now start accumulating samples asynchronously
			if (backgroundRunning)
			{
				BackgroundSampleCollector collector(internalSampleReport, backgroundSem);
				unsigned countLeft = blockSize;

				if (!backgroundSampling)
				{
					if (!sinceClockSetting.IsRunning() || !sinceClockSetting.RemainingSec(12*3600))
					{
						Logger::Locked(logger)->Info << "(B) Setting the clock" << endl;
						sens->SetTime();
						sinceClockSetting.Reset(0,true);
					}

					Logger::Locked(logger)->Info << "(B) Starting sampling" << endl;
					sens->StartAutonomousSampling();
					backgroundSampling = true;
					pumpOn = true;
				}
				else
					Logger::Locked(logger)->Debug << "(B) Sampling already enabled" << endl;

				TimeCounterPtr tcp;
				SBGPCTDSensor::Sample sample;

				while(countLeft && WaitSec(backgroundSem, backgroundRunning, periodSec+30, tcp, true))
				{
					while(countLeft && collector.samples.ReadTest(sample))
					{
						--countLeft;
						tcp->Reset(0,true);
						{ ILockableConceptLock lock(firingMutex); sampleReportSignal(*this, sample, o2Installed && (periodSec < 15)); }
						Logger::Locked(logger)->Debug << "(B) Sample noted, waiting for " << countLeft << " more" << endl;
					}
				}

				if (backgroundRunning && countLeft)
					throw runtime_error("(B) Failed to accumulate the expected number of samples");

				if (backgroundRunning && offSec)
				{
					sens->StopAutonomousSampling();
					backgroundSampling = false;

					unsigned waitTimeSec;

					if (offSec < flushSec)
					{
						sens->SetPump(SBGPCTDSensor::PUMP_FAST);
						pumpOn = true;
						waitTimeSec = flushSec;
					}
					else
					{
						sens->SetPump(SBGPCTDSensor::PUMP_OFF);
						pumpOn = false;
						waitTimeSec = offSec;
					}

					Logger::Locked(logger)->Info << "(B) Waiting for " << waitTimeSec << " sec between blocks" << endl;
					TimeCounterPtr tcp;
					WaitSec(backgroundSem, backgroundRunning, waitTimeSec, tcp);
				}
			}
		}
		catch(exception& ex)
		{
			Logger::Locked(logger)->Warning << "(B) Caught exception during sample polling loop (sensor will be powered off and restarted after delay): " << ex.what() << endl;
			first = false;
			backgroundSampling = false;

			// try turning the sensor off due to error
			if (sens) try { sens->StopAutonomousSampling(); sens->SetPump(SBGPCTDSensor::PUMP_OFF); } catch(...) { }
			if (sens) try { SetPower(false); } catch(...) { }

			// wait for 1 minute with power off
			TimeCounterPtr tcp;
			WaitSec(backgroundSem, backgroundRunning, 60, tcp);
		}

		if (backgroundRunning)
			Logger::Locked(logger)->Info << "Cycle #" << ++cycles << " took " << cycleCounter.GetCountSec() << " sec" << endl;
	}

	backgroundThread.reset();
	Logger::Locked(logger)->Info << "SBGPCTDController exiting background polling thread" << endl;
}

void SBGPCTDController::OnRawReceive(SBGPCTDSensor& sender, const string& message)
{
	if (persist->GetRawLoggingEnabled())
	{
		Logger::Locked(logger)->Debug << "Logging raw receive to file set" << endl;

		// there's no timestamp in the sample, so I'm taking the system time
		timeval tv;
		if (gettimeofday(&tv, NULL))
		{
			tv.tv_sec = time(NULL);
			tv.tv_usec = 0;
		}

		// put the sample columns into a sample
		CSVSample csvSample;
		csvSample += message;

		// write the sample
		csvSamples.WriteSample("RECV", tv, lat, lon, csvSample);
	}
	else
		Logger::Locked(logger)->Debug << "Raw receive logging is disabled (nothing logged)" << endl;
}

void SBGPCTDController::OnRawSend(SBGPCTDSensor& sender, const string& message)
{
	if (persist->GetRawLoggingEnabled())
	{
		Logger::Locked(logger)->Debug << "Logging raw send to file set" << endl;

		// there's no timestamp in the sample, so I'm taking the system time
		timeval tv;
		if (gettimeofday(&tv, NULL))
		{
			tv.tv_sec = time(NULL);
			tv.tv_usec = 0;
		}

		// put the sample columns into a sample
		CSVSample csvSample;
		csvSample += message;

		// write the sample
		csvSamples.WriteSample("SEND", tv, lat, lon, csvSample);
	}
	else
		Logger::Locked(logger)->Debug << "Raw send logging is disabled (nothing logged)" << endl;
}

bool SBGPCTDController::IsO2Installed(const SBGPCTDSensor::HardwareData& hw)
{
	bool result = false;

	for (SBGPCTDSensor::HardwareID::Vector::const_iterator ci = hw.externalSensors.begin(); !result && (ci != hw.externalSensors.end()); ++ci)
		if (boost::icontains(ci->id, "oxygen"))
			result = true;

	return result;
}

struct SNItem
{
	string id;
	string prefix;
};

string SBGPCTDController::GetSerialNumber(const SBGPCTDSensor::HardwareData& hw)
{
	map<string,string> lookup;

	for (SBGPCTDSensor::HardwareID::Vector::const_iterator ci = hw.internalSensors.begin(); ci != hw.internalSensors.end(); ++ci)
		lookup[boost::to_lower_copy(ci->id)] = ci->serialNumber;

	for (SBGPCTDSensor::HardwareID::Vector::const_iterator ci = hw.externalSensors.begin(); ci != hw.externalSensors.end(); ++ci)
		lookup[boost::to_lower_copy(ci->id)] = ci->serialNumber;

	const SNItem snitems[] = { { "conductivity", "C" }, { "temperature", "T" }, { "pressure", "P" }, { "oxygen", "O" }, { "", "" } };
	stringstream msg;
	map<string,string>::const_iterator ci;

	msg << hw.mainSensor.serialNumber;

	for (const SNItem* snip = snitems; !snip->id.empty(); ++snip)
		if ((ci = lookup.find(snip->id)) != lookup.end())
			msg << ":" << snip->prefix << "=" << ci->second;

	string result = msg.str();
	return result;
}

void SBGPCTDController::StartupStage2()
{
	Logger::Locked(logger)->Info << "Starting stage 2 construction thread" << endl;

	// probably not necessary, but just to be sure
	sleep(10);

	// if the persist file exists ...
	if (persist)
	{
		// send a message configuring how many messages per report and how
		// long to wait before reporting anyway
		ConfigureBlockReporting();

		if (persist->GetAutoRun())
			StartSampling();
	}

	Logger::Locked(logger)->Info << "Exiting stage 2 construction thread" << endl;
}

}
}
