// Copyright (c) 2025 Metrolab Technology S.A., Geneva, Switzerland (www.metrolab.com)
// See the included file LICENSE.txt for the licensing conditions.

//////////////////////////////////////////////////////////////////////////
/// \file
/// \brief Interface definition for C++ API for Metrolab THM1176/TFM1186.

//////////////////////////////////////////////////////////////////////////
/// \if (thm1176_instrument_driver_doc)
///
/// \mainpage C++ API for Metrolab Three-axis Hall Magnetometer THM1176.
///
/// \section intro_sect Introduction
/// This C++ Application Programming Interface (API) controls a Metrolab magnetometer
/// in the Three-axis Hall Magnetometer THM1176 family (including the Three-axis Fluxgate Magnetometer TFM1186),
/// either via a USBTMC driver accessed by the VISA library (CVISAInstrument)
/// or via a libusb-based USBTMC instrument driver (CUSBTMCInstrument).
/// It provides access to the entire functionality of the THM1176 SCPI command set,
/// as documented in "Three-Axis Magnetometers: THM1176 and TFM1186 User's Manual".
///
/// \section using_sect Using the API
/// 1. Create and initialize a Resource Manager object
///		(\ref MTL::Instrument::CVISAResourceManager "CVISAResourceManager" or
///		\ref MTL::Instrument::CUSBTMCResourceManager "CUSBTMCResourceManager").
///
/// 2. Call the Resource Manager's FindResources method
///		(\ref MTL::Instrument::CVISAResourceManager::FindResources "CVISAResourceManager::FindResources" or
///		\ref MTL::Instrument::CUSBTMCResourceManager::FindResources "CUSBTMCResourceManager::FindResources")
///		with the appropriate filter ("USB[0-9]*::0x1BFA::0x0498::[0-9]+::INSTR" for VISA, "7162:1176" for USBTMC).
///
/// 3. Create a \ref MTL::Instrument::CTHM1176Instrument "CTHM1176Instrument" object,
///		with the Resource Manager and desired resource as arguments.
///
/// 4. Call \ref MTL::Instrument::CTHM1176Instrument::Connect "CTHM1176Instrument::Connect".
///
/// 5. Call whatever other methods you want to control the THM1176.
///
/// 6. Call \ref MTL::Instrument::CTHM1176Instrument::Disconnect "CTHM1176Instrument::Disconnect".
///
/// 7. Destroy the THM1176 %Instrument and Resource Manager objects.
///
/// \section examples_sect Examples
/// Any of the test cases will serve as an example.
/// The simplest functional test case is CTHM1176ConnectTest, which simply connects and disconnects from the instrument.
/// When reading the test code, note that the first steps of \ref using_sect are handled by the test setup method.
///
/// \endif

#pragma once

// Standard includes
#include <map>
#include <ctime>
#include <chrono>
#include <atomic>

// Eigen library for linear algebra
#include "Eigen/Core"

// Personal includes
#include "IEEE488Instrument.h"
#include "THM1176Types.h"

using namespace MTL::Instrument;

// Supported versions.
#define THM1176_SUPPORTED_VERSION_MIN_MAJOR		3
#define THM1176_SUPPORTED_VERSION_MIN_MINOR		0

using namespace MTL::Instrument::THM1176Types;
using namespace Eigen;

namespace MTL {
	namespace Instrument {
		
		//----------------------------------------------------------------------//
		// Constants
		/// \brief Warning that angle correction was not applied.
		///
		/// This error code is in addition to those returned by the instrument itself
		/// (see Section 9-3, "Execution Errors," of the User Manual).
		static const I32 THM1176_NO_ANGLE_CORRECTION_CODE = 208;
		
		//----------------------------------------------------------------------//
		// Class definition
		/// \brief THM1176 instrument class.
		///
		/// Abstraction of a Metrolab THM1176 Three-axis Hall Magnetometer or
		/// TFM1186 Three-axis Fluxgate Magnetometer.
		///
		/// \tparam InstrType	%Instrument class to use for communication, e.g.
		///						CVISAInstrument or CUSBTMCInstrument
		/// \tparam RsrcMgrType	Associated Resource Manager, e.g.
		///						CVISAResourceManager or CUSBTMCResourceManager
		///
	template <class InstrType, class RsrcMgrType>
	class CTHM1176Instrument: public InstrType
		{
			//----------------------------------------------------------------------//
			//							Private Attributes							//
			//----------------------------------------------------------------------//
		private:
			// General
			mutable CRecursiveMutex										m_Lock;
			U32															m_Timeout;
			std::string													m_Identification;
			sIdentifier													m_IdentificationStruct;
			CErrorList													m_ErrorList;
			CSCPIBuffer													m_ReadBuffer;
			CAbsoluteTimestamp											m_BootTime;
			CFluxList													m_Ranges;
			std::map<eUnits,U32>										m_Units;
			Matrix3f													m_RotationMatrix;
			std::string													m_ManufacturingDate;
			std::string													m_CalibrationDate;
			eUnits														m_UnitsParms;
			sAveraging<uParm>											m_AveragingParms;
			sInputTrigger<uParm>										m_TriggerParms;
			sRange<uParm>												m_RangeParms;
			eCommunicationFormat										m_Format;
			bool														m_Sleep;
			bool														m_UseCalibration;
			sAveraging<uParm>											m_AveragingParmsAtInit;
			sInputTrigger<uParm>										m_TriggerParmsAtInit;
			std::vector<std::chrono::high_resolution_clock::time_point>	m_TriggerTimes;
			std::atomic_bool											m_AbortRead;
			
			//----------------------------------------------------------------------//
			//								Utilities								//
			//----------------------------------------------------------------------//
		private:
			bool ReadIdentification(std::string & rIdentification);
			bool ParseIdentification(struct sIdentifier & rIdentification);
			bool GetErrorList(CErrorList & rList, const std::string & rContext);
			bool ReadBootTime(CAbsoluteTimestamp & rBootTime);
			bool ReadAllRanges(CFluxList & rRanges);
			bool ReadAllUnits(std::map<eUnits,U32> & rAllUnits);
			bool ReadUseCalibration(bool & rUseCal);
			bool ReadManufacturingDate(std::string & rDate);
			bool ReadRotationMatrix(Matrix3f & rMatrix, std::string & rCalibrationDate);
			bool ApplyRotationMatrix(tFlux & rBx, tFlux & rBy, tFlux & rBz);
			bool ApplyRotationMatrix(CFluxList & rBx, CFluxList & rBy, CFluxList & rBz);
			bool ParseMeasurements(CSCPIBufferParser & rBP, eUnits Units, CFluxList & rMeas, U32 NoMeasurements = 0);
			
			//----------------------------------------------------------------------//
			//							THM1176 Interface							//
			//----------------------------------------------------------------------//
		public:
			//----------------------------------------------------------------------//
			// Constructors / destructors
			/// \brief Constructor.
			/// \param[in]	rResourceManager	Resource Manager.
			/// \param[in]	ResourceName		Resource name.
			CTHM1176Instrument(RsrcMgrType & rResourceManager, tResourceName ResourceName);
			
			/// \brief Destructor.
			virtual ~CTHM1176Instrument();
			
			//----------------------------------------------------------------------//
			// General Information
			/// \brief Fetch current error list.
			/// \return List of errors.
			const CErrorList & CurrentErrorList();
			
			/// \brief Clear the error list.
			void ClearErrorList();
			
			//----------------------------------------------------------------------//
			// Open / Close
			/// \brief Open the connection to the instrument.
			/// \param[in]	InitialTimeout	Timeout on exclusive lock request.
			/// \param[in]	Exclusive		Whether or not to request an exclusive lock.
			/// \param[in]	pErrMsg			Optional error message.
			/// \return True if the connection was successful.
			bool Connect(U32 InitialTimeout, bool Exclusive = true, std::string *pErrMsg = nullptr);
			
			/// \brief Close the connection to the instrument.
			void Disconnect();
			
			//----------------------------------------------------------------------//
			// Parameters
			// Averaging
			/// \brief Set the averaging parameter.
			/// \param[in]	rAvg		Desired averaging setting.
			/// \return True if successful.
			bool ParmAveragingSet(const sAveraging<uParm> & rAvg);
			
			/// \brief Fetch the currently selected averaging parameter.
			/// \param[out]	rAvg		Current averaging parameters.
			/// \return True if successful.
			bool ParmAveragingGet(sAveraging<uParm> & rAvg);
		
			/// \brief Fetch current averaging parameter, incl. min/max/default.
			/// \param[out]	rAvg		Averaging parameters, incl. min/max/default.
			/// \return True if successful.
			bool ParmAveragingGet(sAveraging<sBoundedParm> & rAvg);
			
			// Sleep
			/// \brief Set parameter whether to sleep after each acquisition.
			/// \param[in]	Sleep		Whether or not to sleep.
			/// \return True if successful.
			bool ParmSleepSet(bool Sleep);
			
			/// \brief Fetch parameter whether to sleep after each acquisition.
			/// \param[out]	rSleep		Whether or not to sleep.
			/// \return True if successful.
			bool ParmSleepGet(bool & rSleep);

			// Trigger
			/// \brief Set trigger input parameters.
			/// \param[in]	rInputTrig		Trigger input parameters.
			/// \return True if successful.
			bool ParmTriggerInputSet(const sInputTrigger<uParm> & rInputTrig);
			
			/// \brief Fetch current trigger input parameters.
			/// \param[out]	rInputTrig		Trigger input parameters.
			/// \return True if successful.
			bool ParmTriggerInputGet(sInputTrigger<uParm> & rInputTrig);
			
			/// \brief Fetch current trigger input parameters, incl. min/max/default.
			/// \param[out]	rInputTrig		Trigger input parameters, incl. min/max/default.
			/// \return True if successful.
			bool ParmTriggerInputGet(sInputTrigger<sBoundedParm> & rInputTrig);

			// Units
			/// \brief Set measurement units.
			/// \param[in]	Units		Measurement units.
			/// \return True if successful.
			bool ParmUnitsSet(eUnits Units);
			
			/// \brief Fetch currently selected measurement units.
			/// \param[out]	rUnits		Measurement units.
			/// \return True if successful.
			bool ParmUnitsGet(eUnits & rUnits);

			// Use Calibration
			/// \brief Set whether to return calibrated results.
			/// \param[in]	UseCal		Whether to return calibrated results.
			/// \return True if successful.
			bool ParmUseCalibrationSet(bool UseCal);
			
			/// \brief Fetch parameter whether to return calibrated results.
			/// \param[out]	rUseCal		Whether to return calibrated results.
			/// \return True if successful.
			bool ParmUseCalibrationGet(bool	& rUseCal);

			// Range
			/// \brief Set measurement range.
			/// \param[in]	rRange		Desired measurement range.
			/// \return True if successful.
			bool ParmRangeSet(const sRange<uParm> & rRange);

			/// \brief Fetch currently selected measurement range.
			/// \param[out]	rRange		Measurement range.
			/// \return True if successful.
			bool ParmRangeGet(sRange<uParm> & rRange);

			/// \brief Fetch currently selected measurement range, incl. min/max/default.
			/// \param[out]	rRange		Measurement range, incl. min/max/default.
			/// \return True if successful.
			bool ParmRangeGet(sRange<sBoundedParm> & rRange);
			
			//----------------------------------------------------------------------//
			// Files
			/// \brief Read the instrument's file directory information.
			/// \param[out]	rUsedBytes		Number of bytes occupied by files.
			/// \param[out]	rAvailableBytes	Number of bytes still available.
			/// \param[out]	rFileList		List of files.
			/// \return True if successful.
			bool ReadFileDirectory(U32 & rUsedBytes, U32 & rAvailableBytes, tFileList & rFileList);

			/// \brief Read a file from the instrument's file system.
			/// \param[in]	Path			Path of file to be read.
			/// \param[out]	rContent		Contents of file.
			/// \return True if successful.
			bool ReadFile(std::string Path, std::string & rContent);
			
			//----------------------------------------------------------------------//
			// Initiate / Abort / Trigger
			/// \brief Initiate measurements.
			/// \param[in]	Continuous		Whether to measure continuously.
			/// \return True if successful.
			bool Initiate(bool Continuous = false);

			/// \brief Abort a measurement in progress.
			/// \return True if successful.
			bool Abort(void);

			/// \brief Send a USB bus trigger.
			/// \return True if successful.
			bool SendBusTrigger();
			
			//----------------------------------------------------------------------//
			// Measurements / Data
			/// \brief Retrieve measurements: short form.
			/// \param[in]	NoMeasurements			Number of measurements.
			/// \param[out]	rBx						Measured Bx values.
			/// \param[out]	rBy						Measured By values.
			/// \param[out]	rBz						Measured Bz values.
			/// \param[out]	rUnits					Units in which data is returned.
			/// \param[out]	rTemp					Returned temperature.
			/// \param[out]	rTimestampList			Timestamp for each measurement.
			/// \param[out]	pMeasurementConditions	Averaging, trigger and calibration parameters.
			/// \return True if successful.
			///
			/// *Note:*
			/// If pMeasurementConditions is NULL (the default), the measurement parameters
			/// are not returned.
			bool MeasurementsGet(U32 NoMeasurements,
								 CFluxList & rBx, CFluxList & rBy, CFluxList & rBz,
								 eUnits & rUnits, U16 & rTemp, CTimestampList & rTimestampList,
								 sMeasurementConditions * pMeasurementConditions = NULL);

			/// \brief Retrieve measurements: long form.
			/// \param[in]	ArbSelect		Select which data to return, and how much.
			/// \param[out]	rBx				Measured Bx values; empty if not selected.
			/// \param[out]	rBy				Measured By values; empty if not selected.
			/// \param[out]	rBz				Measured Bz values; empty if not selected.
			/// \param[out]	rUnits			Units in which data is returned.
			/// \param[out]	rTemp			Returned temperature; zero if not selected.
			/// \param[out]	rTimestampList	Timestamp for each measurement; empty if not selected.
			/// \param[out]	pMeasurementConditions	Averaging, trigger and calibration parameters.
			/// \return True if successful.
			///
			/// *Note:*
			/// If pMeasurementConditions is NULL (the default), the measurement parameters
			/// are not returned.
			bool MeasurementsGet(sArbitraryMeasurements ArbSelect,
								 CFluxList & rBx, CFluxList & rBy, CFluxList & rBz,
								 eUnits & rUnits, U16 & rTemp, CTimestampList & rTimestampList,
								 sMeasurementConditions * pMeasurementConditions = NULL);

			/// \brief Select whether data is returned as text or binary.
			/// \param[in]	Format		Select text or binary.
			/// \return True if successful.
			bool SetFormat(eCommunicationFormat Format);

			/// \brief Retrieve whether data is returned as text or binary.
			/// \param[out]	Format		Text or binary.
			/// \return True if successful.
			bool GetFormat(eCommunicationFormat & Format);
			
			//----------------------------------------------------------------------//
			// High Level Measurement
			/// \brief High-level measurement: single measurement.
			/// \param[out]	rBx				Measured Bx value.
			/// \param[out]	rBy				Measured By value.
			/// \param[out]	rBz				Measured Bz value.
			/// \param[in]	DefaultParms	Use default or currently selected parameters.
			/// \param[in]	Units			Measurement units.
			/// \param[in]	ExpectedField	Select range for this expected field.
			/// \param[in]	NoDigits		Number of digits to return.
			/// \return True if successful.
			bool Measure(tFlux & rBx, tFlux & rBy, tFlux & rBz,
						 bool DefaultParms = true, eUnits Units = kT,
						 tFlux ExpectedField = 0., unsigned int NoDigits = 0);

			/// \brief High-level measurement: multiple measurements.
			/// \param[out]	rBx				Measured Bx values.
			/// \param[out]	rBy				Measured By values.
			/// \param[out]	rBz				Measured Bz values.
			/// \param[in]	NoMeasurements	Number of measurements.
			/// \param[in]	DefaultParms	Use default or currently selected parameters.
			/// \param[in]	Units			Measurement units.
			/// \param[in]	ExpectedField	Select range for this expected field.
			/// \param[in]	NoDigits		Number of digits to return.
			/// \return True if successful.
			bool Measure(CFluxList & rBx, CFluxList & rBy, CFluxList & rBz, unsigned int NoMeasurements = 1,
						 bool DefaultParms = true, eUnits Units = kT,
						 tFlux ExpectedField = 0., unsigned int NoDigits = 0);
			
			//----------------------------------------------------------------------//
			// Status Handling
			/// \brief Reset OPERation and QUEStionable enable registers.
			/// \return True if successful.
			bool StatusPreset();

			/// \brief Fetch current value of a single status register.
			/// \param[in]	Reg			Register to fetch.
			/// \param[out]	rStatus		Register value; zero if error.
			/// \return True if successful.
			bool StatusGet(sStatusRegister Reg, U16 & rStatus);

			/// \brief Fetch current values of a list of status registers.
			/// \param[in]	Regs		Registers to fetch.
			/// \param[out]	rStatus		Register values; zero if error.
			/// \return True if successful.
			bool StatusGet(RegisterList Regs, StatusValues & rStatus);

			/// \brief Disable and enable bits in the given enable register.
			/// \param[in]	Set			Register set whose enable register is to be modified.
			/// \param[in]	DisableMask	Bits to be disabled.
			/// \param[in]	EnableMask	Bits to be enabled.
			/// \return True if successful.
			///
			/// *Note:*
			/// The DisableMask is applied before the EnableMask, so the EnableMask takes precedence.
			bool StatusSetEnableRegister(eStatusRegisterSet Set, U16 DisableMask, U16 EnableMask);
			
			//----------------------------------------------------------------------//
			// Utilities
			/// \brief Write an arbitrary command and read the result.
			/// \param[in]	rWriteStr			Command to write.
			/// \param[out]	rReadBuffer			Returned result.
			/// \return True if successful.
			bool WriteAndRead(const std::string & rWriteStr, CSCPIBuffer & rReadBuffer);

			/// \brief Fetch the intrument's identification string.
			/// \param[out]	rIdentification		Returned identification string.
			/// \return True if successful.
			bool GetIdentification(std::string & rIdentification);

			/// \brief Fetch the intrument's parsed identification string.
			/// \param[out]	rIdentification		Returned identification string.
			/// \return True if successful.
			bool GetIdentification(sIdentifier & rIdentification);

			/// \brief Fetch all the intrument's ranges.
			/// \param[out]	rRanges				List of ranges.
			/// \return True if successful.
			bool GetAllRanges(CFluxList & rRanges);

			/// \brief Convert a raw timestamp to UNIX Epoch time and nanoseconds.
			/// \param[in]	RawTimestamp		Raw timestamp returned by instrument.
			/// \param[out]	rTimestamp			Timestamp as UNIX Epoch time and nanoseconds.
			/// \return True if successful.
			inline bool ConvertTimestamp(const U64 RawTimestamp, CAbsoluteTimestamp & rTimestamp)
			{
				rTimestamp = m_BootTime + CAbsoluteTimestamp(0, RawTimestamp);
				return true;
			};

			/// \brief Fetch all units supported by instrument.
			/// \param[out]	rUnits				List of units.
			/// \return True if successful.
			bool GetAllUnits(CUnitsList & rUnits);

			/// \brief Fetch divisor to convert instrument's base units to given units.
			/// \param[in]	Units				Units for which we want the divisor.
			/// \param[out]	rDivisor			The returned divisor; zero if error.
			/// \return True if successful.
			bool GetDivisor(eUnits Units, U32 & rDivisor);

			/// \brief Fetch the intrument's rotation matrix, to correct angular error.
			/// \param[out]	Matrix				The returned rotation matrix.
			/// \return True if successful.
			bool GetRotationMatrix(Matrix3f & Matrix);

			/// \brief Compute measurement interval for Immediate Trigger, for a given averaging parameter.
			/// \param[in]	rAvg				Averaging setting.
			/// \param[in]  modelRev			THM1176 Model: kA for THM1176-A. kB for THM1176-B.
			/// \param[out]	rPeriod				Corresponding measurement period, in s.
			/// \return True if successful.
			bool GetImmediateMeasurementPeriod(const sAveraging<uParm> & rAvg, eModelRevision modelRev, F64 & rPeriod);

			/// \brief Perform the Zero Offset calibration procedure.
			/// \param[in]	ForceCalibration	Override warnings about models that should not be calibrated
			/// \return True if successful.
			///
			/// *Notes:*
			/// 1. The instrument must be placed in a zero-gauss chamber.
			/// 2. Should not be performed on a TFM1186.
			bool CalibrateZeroOffset(bool ForceCalibration = false);

			/// \brief Restore the factory values for the Zero Offset.
			/// \return True if successful.
			bool RestoreZeroOffset();

			/// \brief Reset the instrument to power-on configuration.
			/// \return True if successful.
			bool Reset();

			/// \brief Enter the Device Firmware Upgrade mode.
			/// \return True if successful.
			///
			/// *Note:*
			/// This function is intended for use by the manufacturer only.
			/// It can cause your instrument to become nonoperational.
			bool SwitchToDFUMode();

			/// \brief Fetch the intrument's date information.
			/// \param[out]	rSManufacturingDate	Manufacturing date as string.
			/// \param[out]	rManufacturingDate	Manufacturing date as time structure.
			/// \param[out]	rSCalibrationDate	Calibration date as string.
			/// \param[out]	rCalibrationDate	Calibration date as time structure.
			/// \return True if successful.
			bool ReadInformationDates(std::string & rSManufacturingDate, std::time_t & rManufacturingDate, std::string & rSCalibrationDate, std::time_t & rCalibrationDate);

			/// \brief Abort a read operation.
			/// \return True if successful.
			///
			/// *Note:*
			/// This operation is nonoperational for instruments with firmware version 3.1 or below.
			/// It will cause your instrument to lock up.
			bool AbortRead();
			
		};
		
	} // namespace Instrument
} // namespace MTL
