From 1960e661c95631a5d38dde72cff52283643ddb1d Mon Sep 17 00:00:00 2001 From: Irina Stefan <144726379+stefi07@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:50:15 -0400 Subject: [PATCH] Dev dev fsm (#1) * Added fsm telemetry app connecting via socket * Cleaned up logging across all fsm header files. Added ADC & DAC query commands. * Added INDI params to send DACs. Fixed state progression. * Adding voltage to dac & angles to actuator displacement conversion. Diagrams for the INDI commands. * Added shmim commands. Made block diagrams for the INDI & shmim logic. * Documentation * Added toggle between indi & shmim * Added ADC 'current' params that show the ADC values queried on connection and after a DAC value update. * Added status.query INDI param to query 'adc' or 'dac' values. * WIP test writing --- apps/fsmCtrl/IUart.h | 44 + apps/fsmCtrl/Makefile | 10 + apps/fsmCtrl/binaryUart.hpp | 317 +++++ apps/fsmCtrl/cGraphPacket.hpp | 223 +++ apps/fsmCtrl/conversion.hpp | 119 ++ apps/fsmCtrl/fsmCommands.hpp | 346 +++++ apps/fsmCtrl/fsmCtrl.cpp | 16 + apps/fsmCtrl/fsmCtrl.hpp | 1300 ++++++++++++++++++ apps/fsmCtrl/fsmCtrl_on_PinkyVM.md | 128 ++ apps/fsmCtrl/fsm_conversion_factors.ipynb | 378 +++++ apps/fsmCtrl/iPacket.hpp | 25 + apps/fsmCtrl/linux_pinout_client_socket.hpp | 241 ++++ apps/fsmCtrl/readme.md | 57 + apps/fsmCtrl/set_vals_INDI.drawio.svg | 1034 ++++++++++++++ apps/fsmCtrl/set_vals_shmim.drawio.svg | 1289 +++++++++++++++++ apps/fsmCtrl/socket.hpp | 69 + apps/fsmCtrl/tests/Makefile | 9 + apps/fsmCtrl/tests/binaryUart_test.cpp | 674 +++++++++ apps/fsmCtrl/tests/fsmCtrl_test.cpp | 70 + libMagAOX/Makefile | 83 +- libMagAOX/logger/logCodes.dat | 3 +- libMagAOX/logger/types/schemas/telem_fsm.fbs | 20 + libMagAOX/logger/types/telem.cpp | 3 +- libMagAOX/logger/types/telem_fsm.hpp | 120 ++ tests/tests.list | 2 + 25 files changed, 6537 insertions(+), 43 deletions(-) create mode 100644 apps/fsmCtrl/IUart.h create mode 100644 apps/fsmCtrl/Makefile create mode 100644 apps/fsmCtrl/binaryUart.hpp create mode 100644 apps/fsmCtrl/cGraphPacket.hpp create mode 100644 apps/fsmCtrl/conversion.hpp create mode 100644 apps/fsmCtrl/fsmCommands.hpp create mode 100644 apps/fsmCtrl/fsmCtrl.cpp create mode 100644 apps/fsmCtrl/fsmCtrl.hpp create mode 100644 apps/fsmCtrl/fsmCtrl_on_PinkyVM.md create mode 100644 apps/fsmCtrl/fsm_conversion_factors.ipynb create mode 100644 apps/fsmCtrl/iPacket.hpp create mode 100755 apps/fsmCtrl/linux_pinout_client_socket.hpp create mode 100644 apps/fsmCtrl/readme.md create mode 100644 apps/fsmCtrl/set_vals_INDI.drawio.svg create mode 100644 apps/fsmCtrl/set_vals_shmim.drawio.svg create mode 100644 apps/fsmCtrl/socket.hpp create mode 100644 apps/fsmCtrl/tests/Makefile create mode 100644 apps/fsmCtrl/tests/binaryUart_test.cpp create mode 100644 apps/fsmCtrl/tests/fsmCtrl_test.cpp create mode 100644 libMagAOX/logger/types/schemas/telem_fsm.fbs create mode 100644 libMagAOX/logger/types/telem_fsm.hpp diff --git a/apps/fsmCtrl/IUart.h b/apps/fsmCtrl/IUart.h new file mode 100644 index 000000000..578961f2f --- /dev/null +++ b/apps/fsmCtrl/IUart.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +class IUart +{ +public: + IUart() { } + virtual ~IUart() { } + + static const bool OddParity = true; + static const bool NoParity = false; + static const bool IUartOK = 0x00; + static const bool UseRTSCTS = true; + static const bool NoRTSCTS = false; + + + //All the following pinout functions are very hardware & device dependant, so they are declared in another class to preserve the abstraction + //virtual int init() { return(InitOK); } //note: this functiuon usually gets overloaded with different params, so it's no longer virtual anyway - probably not an issue, since it usually gets called from code with the full object, not just the base class. + //~ virtual bool dataready() { printf("\nIUart::dataready() stub!\n"); return(false); } + //~ virtual char getcqq() { printf("\nIUart::getcqq() stub!\n"); return('\0'); } + //~ virtual char putcqq(char c) { printf("\nIUart::putcqq() stub!\n"); return('\0'); } + //~ virtual void flushoutput() { printf("\nIUart::flushoutput() stub!\n"); } + //~ virtual void purgeinput() { printf("\nIUart::purgeinput() stub!\n"); } + //~ virtual bool isopen() { printf("\nIUart::isopen() stub!\n"); return(false); } + virtual bool dataready() const = 0; + virtual char getcqq() = 0; + virtual char putcqq(char c) = 0; + virtual void flushoutput() = 0; + virtual void purgeinput() = 0; + virtual bool isopen() const = 0; + + void puts(const char* s, const size_t len) + { + for (size_t i = 0; i < len; i++) + { + if ('\0' == s[i]) { break; } + + putcqq(s[i]); + } + } +}; diff --git a/apps/fsmCtrl/Makefile b/apps/fsmCtrl/Makefile new file mode 100644 index 000000000..95bde8c8b --- /dev/null +++ b/apps/fsmCtrl/Makefile @@ -0,0 +1,10 @@ + +allall: all + +debug_opt: all +debug_opt: CXXFLAGS += -g + +OTHER_HEADERS= +TARGET=fsmCtrl +include ../../Make/magAOXApp.mk + diff --git a/apps/fsmCtrl/binaryUart.hpp b/apps/fsmCtrl/binaryUart.hpp new file mode 100644 index 000000000..204d1ccd0 --- /dev/null +++ b/apps/fsmCtrl/binaryUart.hpp @@ -0,0 +1,317 @@ +// +/// Copyright (c)2007 by Franks Development, LLC +// +// This software is copyrighted by and is the sole property of Franks +// Development, LLC. All rights, title, ownership, or other interests +// in the software remain the property of Franks Development, LLC. This +// software may only be used in accordance with the corresponding +// license agreement. Any unauthorized use, duplication, transmission, +// distribution, or disclosure of this software is expressly forbidden. +// +// This Copyright notice may not be removed or modified without prior +// written consent of Franks Development, LLC. +// +// Franks Development, LLC. reserves the right to modify this software +// without notice. +// +// Franks Development, LLC support@franks-development.com +// 500 N. Bahamas Dr. #101 http://www.franks-development.com +// Tucson, AZ 85710 +// USA +// + +#pragma once + +#include +#include +#include + +#include // for stringstreams + +#include "IUart.h" + +#include "iPacket.hpp" + +#include "cGraphPacket.hpp" +#include "fsmCommands.hpp" + +namespace MagAOX +{ +namespace app +{ + +struct BinaryUartCallbacks +{ + BinaryUartCallbacks() { } + virtual ~BinaryUartCallbacks() { } + + //Malformed/corrupted packet handler: + virtual void InvalidPacket(const uint8_t* Buffer, const size_t& BufferLen) { } + + //Packet with no matching command handler: + virtual void UnHandledPacket(const IPacket* Packet, const size_t& PacketLen) { } + + //In case we need to look at every packet that goes by... + virtual void EveryPacket(const IPacket* Packet, const size_t& PacketLen) { } + + //Seems like someone, sometime might wanna handle this... + virtual void BufferOverflow(const size_t& BufferLen) { } +}; + + +struct BinaryUart +{ + //Default values + static const uint64_t RxCountInit = 0; + static const uint64_t PacketStartInit = 0; + static const uint64_t PacketLenInit = 0; + static const bool InPacketInit = false; + static const bool debugDefault = false; + static const char EmptyBufferChar = '\0'; + + static const size_t RxBufferLenBytes = 4096; + static const size_t TxBufferLenBytes = 4096; + uint8_t RxBuffer[RxBufferLenBytes]; //This is where the received characters go while we are building a line up from the input + uint16_t RxCount; + IUart& Pinout; + IPacket& Packet; + BinaryUartCallbacks& Callbacks; + bool debug; + bool InPacket; + size_t PacketStart; + size_t PacketLen; + size_t packetEnd = 0; + //~ const void* Argument; + uint64_t SerialNum; + static const uint64_t InvalidSerialNumber = 0xFFFFFFFFFFFFFFFFULL; + + BinaryUart(struct IUart& pinout, struct IPacket& packet, struct BinaryUartCallbacks& callbacks, const uint64_t serialnum = InvalidSerialNumber) + : + RxCount(RxCountInit), + Pinout(pinout), + Packet(packet), + Callbacks(callbacks), + debug(debugDefault), + //~ debug(true), + InPacket(InPacketInit), + PacketStart(PacketStartInit), + PacketLen(PacketLenInit), + SerialNum(serialnum) + + { + Init(serialnum); + } + + void Debug(bool dbg) + { + debug = dbg; + } + + const uint8_t* GetRxBuffer() const + { + return RxBuffer; + } + + int Init(uint64_t serialnum) + { + SerialNum = serialnum; + RxCount = RxCountInit; + PacketStart = PacketStartInit; + PacketLen = PacketLenInit; + InPacket = InPacketInit; + memset(RxBuffer, EmptyBufferChar, RxBufferLenBytes); + + std::ostringstream oss; + oss << "Binary Uart: Init(PktH " << Packet.HeaderLen() << ", PktF " << Packet.FooterLen() << ")."; + MagAOXAppT::log(oss.str()); + + return(0); + } + + bool Process(MagAOX::app::PZTQuery* pztQuery) + { + //New char? + if ( !(Pinout.dataready()) ) { return(false); } + + //pull it off the hardware + uint8_t c = Pinout.getcqq(); + + ProcessByte(c); + CheckPacketStart(); + CheckPacketEnd(pztQuery); + + return(true); //We just want to know if there's chars in the buffer to put threads to sleep or not... + } + + void ProcessByte(const char c) + { + //Put the current character into the buffer + if (RxCount < RxBufferLenBytes) + { + RxCount++; + RxBuffer[RxCount - 1] = c; + } + else + { + if (debug) + { + std::ostringstream oss; + oss << "BinaryUart: Buffer(" << RxBuffer <<") overflow; this packet will not fit (" << RxCount << "b), flushing buffer."; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + Callbacks.BufferOverflow(RxCount); + + Init(SerialNum); + } + } + + void CheckPacketStart() + { + //Packet Start? + if ( (!InPacket) && (RxCount >= Packet.HeaderLen()) ) + { + if (Packet.FindPacketStart(RxBuffer, RxCount, PacketStart)) //This is wasteful, we really only need to look at the 4 newest bytes every time... + { + if (debug) { MagAOXAppT::log({__FILE__, __LINE__, "BinaryUart: Packet start detected! Buffering."}); } + + InPacket = true; + } + } + } + + bool CheckPacketEnd(MagAOX::app::PZTQuery* pztQuery) + { + packetEnd = 0; + bool Processed = false; + + if (!InPacket || RxCount < (Packet.HeaderLen() + Packet.FooterLen())) + return false; + + //This is wasteful, we really only need to look at the 4 newest bytes every time... + if (!Packet.FindPacketEnd(RxBuffer, RxCount, packetEnd)) { + if (debug) { + MagAOXAppT::log({__FILE__, __LINE__, "BinaryUart: Still waiting for packet end..."}); + } + return false; + } + + if (debug) { + MagAOXAppT::log({__FILE__, __LINE__, "BinaryUart: Packet end detected; Looking for matching packet handlers."}); + } + + const size_t payloadLen = Packet.PayloadLen(RxBuffer, RxCount, PacketStart); + + if (RxCount < payloadLen + Packet.HeaderLen() + Packet.FooterLen()) + { + if ( (payloadLen > RxBufferLenBytes) || (payloadLen > Packet.MaxPayloadLength()) ) + { + if (debug) + { + std::ostringstream oss; + oss << "BinaryUart: Short packet (" << RxCount << " bytes) with unrealistic payload len; ignoring corrupted packet (should have been header() + payload(" << payloadLen << ") + footer()."; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + Callbacks.InvalidPacket(reinterpret_cast(RxBuffer), RxCount); + + Init(SerialNum); + + return false; + } + else + { + if (debug) + { + std::ostringstream oss; + oss << "BinaryUart: Short packet (" << RxCount << " bytes); we'll assume the packet footer was part of the payload data and keep searching for the packet end (should have been header() + payload(" << payloadLen << ") + footer()."; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + return false; + } + } + + if (Packet.IsValid(RxBuffer, RxCount, PacketStart)) + { + if ( (SerialNum == InvalidSerialNumber) || (SerialNum == Packet.SerialNum() ) ) + { + + //strip the part of the line with the arguments to this command (chars following command) for compatibility with the parsing code, the "params" officially start with the s/n + const char* Params = reinterpret_cast(&(RxBuffer[PacketStart + Packet.PayloadOffset()])); + + //call the actual command + pztQuery->processReply(Params, payloadLen); + + Processed = true; + } + else + { + if (debug) + { + std::ostringstream oss; + oss << "BinaryUart: Packet received, but SerialNumber comparison failed (expected: 0x" << SerialNum << "; got: 0x" << Packet.SerialNum() << ")."; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + Callbacks.UnHandledPacket(reinterpret_cast(&RxBuffer[PacketStart]), packetEnd - PacketStart); + } + + //Now just let the user do whatever they want with it... + Callbacks.EveryPacket(reinterpret_cast(&RxBuffer[PacketStart]), packetEnd - PacketStart); + } + else + { + if (debug) { MagAOXAppT::log({__FILE__, __LINE__, "BinaryUart: Packet received, but invalid."}); } + + Callbacks.InvalidPacket(reinterpret_cast(RxBuffer), RxCount); + } + + InPacket = false; + + if (RxCount > (packetEnd + 4) ) + { + size_t pos = 0; + size_t clr = 0; + for (; pos < (RxCount - (packetEnd + 4)); pos++) + { + RxBuffer[pos] = RxBuffer[(packetEnd + 4) + pos]; + } + for (clr = pos; clr < RxCount; clr++) + { + RxBuffer[clr] = 0; + } + RxCount = pos; + } + else + { + Init(SerialNum); + } + + return Processed; + } + + void TxBinaryPacket(const uint16_t PayloadType, const void* PayloadData, const size_t PayloadLen) const + { + uint8_t TxBuffer[TxBufferLenBytes]; + size_t PacketLen = Packet.MakePacket(TxBuffer, TxBufferLenBytes, PayloadData, PayloadType, PayloadLen); + + std::ostringstream oss; + oss << "Packet length: " << PacketLen; + MagAOXAppT::log(oss.str()); + + for (size_t i = 0; i < PacketLen; i++) { Pinout.putcqq(TxBuffer[i]); } + + // Log packet sent + oss.str(""); + oss << "Binary Uart: Sending packet(" << PayloadType << ", " << PayloadLen << "): "; + MagAOXAppT::log(oss.str()); + oss.str(""); + for(size_t i = 0; i < PacketLen; i++) { oss << std::setw(2) << std::setfill('0') << std::hex << static_cast(TxBuffer[i]) << ":"; } + MagAOXAppT::log(oss.str()); + + } +}; + +} //namespace app +} //namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/cGraphPacket.hpp b/apps/fsmCtrl/cGraphPacket.hpp new file mode 100644 index 000000000..febc2a85e --- /dev/null +++ b/apps/fsmCtrl/cGraphPacket.hpp @@ -0,0 +1,223 @@ +/// \file +/// $Source: /raincloud/src/projects/include/Packet/IPacket.h,v $ +/// $Revision: 1.5 $ +/// $Date: 2009/01/06 07:14:18 $ +/// $Author: steve $ +/// The functionality for the Packet hardware when polled on the Atmel AVR processor + +#pragma once + +#include +#include +#include +#include // for stringstreams + +#include "iPacket.hpp" + +namespace MagAOX +{ +namespace app +{ + +uint32_t CRC32(const uint8_t* data, const size_t length); + +static const uint32_t CGraphMagikPacketStartToken = 0x1BADBABEUL; + +struct CGraphPacketHeader +{ + uint32_t PacketStartToken; + uint16_t PayloadType; + uint16_t PayloadLen; + + CGraphPacketHeader() : PacketStartToken(CGraphMagikPacketStartToken), PayloadType(0), PayloadLen(0) { } + + CGraphPacketHeader(uint16_t packettype, uint16_t payloadtype, uint16_t payloadlen) : PacketStartToken(packettype), PayloadType(payloadtype), PayloadLen(payloadlen) { } + + const void* PayloadData() const { return(reinterpret_cast(&(this[1]))); } + + void* PayloadDataNonConst() { return(reinterpret_cast(&(this[1]))); } + + void formatf() const { + std::ostringstream oss; + oss << "CGraphPacketHeader: StartToken: 0x" << (long)PacketStartToken << ", PayloadType: " << (unsigned long)PayloadType << ", PayloadLen: " << (unsigned long)PayloadLen; + MagAOXAppT::log(oss.str()); + } + +} __attribute__((__packed__)); + +static const uint32_t CGraphMagikPacketEndToken = 0x0A0FADEDUL; //\n(0a) goes in high-byte to terminate serial stream in le arch + +struct CGraphPacketFooter +{ + uint32_t CRC32; + uint32_t PacketEndToken; + + CGraphPacketFooter() : CRC32(0), PacketEndToken(CGraphMagikPacketEndToken) { } + + //~ void formatf() const { ::formatf("CGraphPacketFooter: CRC: 0x%.8lX; PacketEndToken(0x%.8lX): 0x%.8lX", CRC32, CGraphMagikPacketEndToken, PacketEndToken); } + +} __attribute__((__packed__)); + + +class CGraphPacket: public IPacket +{ +public: + CGraphPacket() { } + virtual ~CGraphPacket() { } + + virtual bool FindPacketStart(const uint8_t* Buffer, const size_t BufferLen, size_t& Offset) const + { + for (size_t i = 0; i < (BufferLen - sizeof(uint32_t)); i++) { if (CGraphMagikPacketStartToken == *((uint32_t*)&(Buffer[i]))) { Offset = i; return(true); } } + return(false); + } + + virtual bool FindPacketEnd(const uint8_t* Buffer, const size_t BufferLen, size_t& Offset) const + { + for (size_t i = 0; i <= (BufferLen - sizeof(uint32_t)); i++) { if (CGraphMagikPacketEndToken == *((uint32_t*)&(Buffer[i]))) { Offset = i; return(true); } } + return(false); + } + + virtual size_t HeaderLen() const { return(sizeof(CGraphPacketHeader)); } + virtual size_t FooterLen() const { return(sizeof(CGraphPacketHeader)); } + virtual size_t PayloadOffset() const { return(sizeof(CGraphPacketHeader)); } + virtual size_t MaxPayloadLength() const { return(0xFFFFU); } + virtual uint64_t SerialNum() const { return(0); } + + virtual size_t PayloadLen(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const + { + if ((PacketStartPos + sizeof(CGraphPacketHeader)) > BufferCount) { return(0); } + const CGraphPacketHeader* Packet = reinterpret_cast(&(Buffer[PacketStartPos])); + return(Packet->PayloadLen); + } + + virtual uint64_t PayloadType(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const + { + if ((PacketStartPos + sizeof(CGraphPacketHeader)) > BufferCount) { return(0); } + const CGraphPacketHeader* Packet = reinterpret_cast(&(Buffer[PacketStartPos])); + return(Packet->PayloadType); + } + + virtual bool DoesPayloadTypeMatch(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos, const uint32_t CmdType) const + { + if ( ((PacketStartPos + sizeof(CGraphPacketHeader)) > BufferCount) ) { return(false); } + const CGraphPacketHeader* Packet = reinterpret_cast(&(Buffer[PacketStartPos])); + if (CmdType == Packet->PayloadType) { return(true); } + return(false); + } + + virtual bool IsValid(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const + { + if ((PacketStartPos + sizeof(CGraphPacketHeader) + sizeof(CGraphPacketFooter)) > BufferCount) { return(false); } + const CGraphPacketHeader* Header = reinterpret_cast(&(Buffer[PacketStartPos])); + if ((PacketStartPos + sizeof(CGraphPacketHeader) + Header->PayloadLen + sizeof(CGraphPacketFooter)) > BufferCount) { return(false); } + if (CGraphMagikPacketStartToken != Header->PacketStartToken) { return(false); } + const CGraphPacketFooter* Footer = reinterpret_cast(&(Buffer[PacketStartPos + sizeof(CGraphPacketHeader) + Header->PayloadLen])); + if (CGraphMagikPacketEndToken != Footer->PacketEndToken) { return(false); } + uint32_t CRC = CRC32((uint8_t*)Header, sizeof(CGraphPacketHeader) + Header->PayloadLen); + if (CRC != Footer->CRC32) { return(false); } + return(true); + } + + virtual size_t MakePacket(uint8_t* Buffer, const size_t BufferCount, const void* Payload, const uint16_t PayloadType, const size_t PayloadLen) const + { + if ( (NULL == Buffer) || ((NULL == Payload) && (0 != PayloadLen)) || (BufferCount < (sizeof(CGraphPacketHeader) + PayloadLen + sizeof(CGraphPacketFooter))) ) { return(0); } + CGraphPacketHeader Header; + Header.PayloadType = PayloadType; + Header.PayloadLen = PayloadLen; + memcpy(Buffer, &Header, sizeof(CGraphPacketHeader)); + if (NULL != Payload) { memcpy(&(Buffer[sizeof(CGraphPacketHeader)]), Payload, PayloadLen); } + CGraphPacketFooter* Footer = reinterpret_cast(&(Buffer[sizeof(CGraphPacketHeader) + PayloadLen])); + uint32_t CRC = CRC32(Buffer, sizeof(CGraphPacketHeader) + PayloadLen); + Footer->CRC32 = CRC; + Footer->PacketEndToken = CGraphMagikPacketEndToken; + return(sizeof(CGraphPacketHeader) + PayloadLen + sizeof(CGraphPacketFooter)); + } +}; + + +//This technically is a "BZIP2CRC32", not an "ANSICRC32"; seealso: https://crccalc.com/ +uint32_t CRC32(const uint8_t* data, const size_t length) +{ + static const uint32_t table[256] = + { + 0x00000000UL,0x04C11DB7UL,0x09823B6EUL,0x0D4326D9UL, + 0x130476DCUL,0x17C56B6BUL,0x1A864DB2UL,0x1E475005UL, + 0x2608EDB8UL,0x22C9F00FUL,0x2F8AD6D6UL,0x2B4BCB61UL, + 0x350C9B64UL,0x31CD86D3UL,0x3C8EA00AUL,0x384FBDBDUL, + 0x4C11DB70UL,0x48D0C6C7UL,0x4593E01EUL,0x4152FDA9UL, + 0x5F15ADACUL,0x5BD4B01BUL,0x569796C2UL,0x52568B75UL, + 0x6A1936C8UL,0x6ED82B7FUL,0x639B0DA6UL,0x675A1011UL, + 0x791D4014UL,0x7DDC5DA3UL,0x709F7B7AUL,0x745E66CDUL, + 0x9823B6E0UL,0x9CE2AB57UL,0x91A18D8EUL,0x95609039UL, + 0x8B27C03CUL,0x8FE6DD8BUL,0x82A5FB52UL,0x8664E6E5UL, + 0xBE2B5B58UL,0xBAEA46EFUL,0xB7A96036UL,0xB3687D81UL, + 0xAD2F2D84UL,0xA9EE3033UL,0xA4AD16EAUL,0xA06C0B5DUL, + 0xD4326D90UL,0xD0F37027UL,0xDDB056FEUL,0xD9714B49UL, + 0xC7361B4CUL,0xC3F706FBUL,0xCEB42022UL,0xCA753D95UL, + 0xF23A8028UL,0xF6FB9D9FUL,0xFBB8BB46UL,0xFF79A6F1UL, + 0xE13EF6F4UL,0xE5FFEB43UL,0xE8BCCD9AUL,0xEC7DD02DUL, + 0x34867077UL,0x30476DC0UL,0x3D044B19UL,0x39C556AEUL, + 0x278206ABUL,0x23431B1CUL,0x2E003DC5UL,0x2AC12072UL, + 0x128E9DCFUL,0x164F8078UL,0x1B0CA6A1UL,0x1FCDBB16UL, + 0x018AEB13UL,0x054BF6A4UL,0x0808D07DUL,0x0CC9CDCAUL, + 0x7897AB07UL,0x7C56B6B0UL,0x71159069UL,0x75D48DDEUL, + 0x6B93DDDBUL,0x6F52C06CUL,0x6211E6B5UL,0x66D0FB02UL, + 0x5E9F46BFUL,0x5A5E5B08UL,0x571D7DD1UL,0x53DC6066UL, + 0x4D9B3063UL,0x495A2DD4UL,0x44190B0DUL,0x40D816BAUL, + 0xACA5C697UL,0xA864DB20UL,0xA527FDF9UL,0xA1E6E04EUL, + 0xBFA1B04BUL,0xBB60ADFCUL,0xB6238B25UL,0xB2E29692UL, + 0x8AAD2B2FUL,0x8E6C3698UL,0x832F1041UL,0x87EE0DF6UL, + 0x99A95DF3UL,0x9D684044UL,0x902B669DUL,0x94EA7B2AUL, + 0xE0B41DE7UL,0xE4750050UL,0xE9362689UL,0xEDF73B3EUL, + 0xF3B06B3BUL,0xF771768CUL,0xFA325055UL,0xFEF34DE2UL, + 0xC6BCF05FUL,0xC27DEDE8UL,0xCF3ECB31UL,0xCBFFD686UL, + 0xD5B88683UL,0xD1799B34UL,0xDC3ABDEDUL,0xD8FBA05AUL, + 0x690CE0EEUL,0x6DCDFD59UL,0x608EDB80UL,0x644FC637UL, + 0x7A089632UL,0x7EC98B85UL,0x738AAD5CUL,0x774BB0EBUL, + 0x4F040D56UL,0x4BC510E1UL,0x46863638UL,0x42472B8FUL, + 0x5C007B8AUL,0x58C1663DUL,0x558240E4UL,0x51435D53UL, + 0x251D3B9EUL,0x21DC2629UL,0x2C9F00F0UL,0x285E1D47UL, + 0x36194D42UL,0x32D850F5UL,0x3F9B762CUL,0x3B5A6B9BUL, + 0x0315D626UL,0x07D4CB91UL,0x0A97ED48UL,0x0E56F0FFUL, + 0x1011A0FAUL,0x14D0BD4DUL,0x19939B94UL,0x1D528623UL, + 0xF12F560EUL,0xF5EE4BB9UL,0xF8AD6D60UL,0xFC6C70D7UL, + 0xE22B20D2UL,0xE6EA3D65UL,0xEBA91BBCUL,0xEF68060BUL, + 0xD727BBB6UL,0xD3E6A601UL,0xDEA580D8UL,0xDA649D6FUL, + 0xC423CD6AUL,0xC0E2D0DDUL,0xCDA1F604UL,0xC960EBB3UL, + 0xBD3E8D7EUL,0xB9FF90C9UL,0xB4BCB610UL,0xB07DABA7UL, + 0xAE3AFBA2UL,0xAAFBE615UL,0xA7B8C0CCUL,0xA379DD7BUL, + 0x9B3660C6UL,0x9FF77D71UL,0x92B45BA8UL,0x9675461FUL, + 0x8832161AUL,0x8CF30BADUL,0x81B02D74UL,0x857130C3UL, + 0x5D8A9099UL,0x594B8D2EUL,0x5408ABF7UL,0x50C9B640UL, + 0x4E8EE645UL,0x4A4FFBF2UL,0x470CDD2BUL,0x43CDC09CUL, + 0x7B827D21UL,0x7F436096UL,0x7200464FUL,0x76C15BF8UL, + 0x68860BFDUL,0x6C47164AUL,0x61043093UL,0x65C52D24UL, + 0x119B4BE9UL,0x155A565EUL,0x18197087UL,0x1CD86D30UL, + 0x029F3D35UL,0x065E2082UL,0x0B1D065BUL,0x0FDC1BECUL, + 0x3793A651UL,0x3352BBE6UL,0x3E119D3FUL,0x3AD08088UL, + 0x2497D08DUL,0x2056CD3AUL,0x2D15EBE3UL,0x29D4F654UL, + 0xC5A92679UL,0xC1683BCEUL,0xCC2B1D17UL,0xC8EA00A0UL, + 0xD6AD50A5UL,0xD26C4D12UL,0xDF2F6BCBUL,0xDBEE767CUL, + 0xE3A1CBC1UL,0xE760D676UL,0xEA23F0AFUL,0xEEE2ED18UL, + 0xF0A5BD1DUL,0xF464A0AAUL,0xF9278673UL,0xFDE69BC4UL, + 0x89B8FD09UL,0x8D79E0BEUL,0x803AC667UL,0x84FBDBD0UL, + 0x9ABC8BD5UL,0x9E7D9662UL,0x933EB0BBUL,0x97FFAD0CUL, + 0xAFB010B1UL,0xAB710D06UL,0xA6322BDFUL,0xA2F33668UL, + 0xBCB4666DUL,0xB8757BDAUL,0xB5365D03UL,0xB1F740B4UL, + }; + + uint32_t crc = 0xffffffff; + + size_t len = length; + while (len > 0) + { + crc = table[*data ^ ((crc >> 24) & 0xff)] ^ (crc << 8); + data++; + len--; + } + return crc ^ 0xffffffff; +} + + +} //namespace app +} //namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/conversion.hpp b/apps/fsmCtrl/conversion.hpp new file mode 100644 index 000000000..4f3a64446 --- /dev/null +++ b/apps/fsmCtrl/conversion.hpp @@ -0,0 +1,119 @@ +/** \file conversion.hpp + * \brief Conversion maths from alpha/beta/z to DACs & back + * + * \ingroup fsmCtrl_files + */ + +#pragma once + +namespace MagAOX +{ + namespace app + { + + // (dac1, dac2, dac3) ---> (alpha, beta, z) + + double get_alpha(double dac1, double dac2, double dac3, double a) + { + return 1. / a * (dac1 - 0.5 * (dac2 + dac3)); + } + + double get_beta(double dac2, double dac3, double b) + { + return 1. / b * (dac2 - dac3); + } + + double get_z(double dac1, double dac2, double dac3) + { + return 1. / 3. * (dac1 + dac2 + dac3); + } + + // (alpha, beta, z) ---> (dac1, dac2, dac3) + + double angles_to_dac1(double alpha, double z, double a) + { + return z + 2. / 3. * a * alpha; + } + + double angles_to_dac2(double alpha, double beta, double z, double a, double b) + { + return 0.5 * b * beta + z - 1. / 3. * a * alpha; + } + + double angles_to_dac3(double alpha, double beta, double z, double a, double b) + { + return z - 1. / 3. * a * alpha - 1. / 2. * b * beta; + } + + // vectors + + double DAC_to_angles(double dac1, double dac2, double dac3, double a, double b) + { + return get_alpha(dac1, dac2, dac3, a), get_beta(dac2, dac3, b), get_z(dac1, dac2, dac3); + } + + double angles_to_DAC(double alpha, double beta, double z, double a, double b) + { + return angles_to_dac1(alpha, z, a), angles_to_dac2(alpha, beta, z, a, b), angles_to_dac3(alpha, beta, z, a, b); + } + + // constraints + + double get_alpha_min(double dac1_min, double dac2_max, double dac3_max, double a) + { + return 1. / a * (dac1_min - 0.5 * (dac2_max + dac3_max)); + } + + double get_alpha_max(double dac1_max, double dac2_min, double dac3_min, double a) + { + return 1. / a * (dac1_max - 0.5 * (dac2_min + dac3_min)); + } + + double get_beta_min(double dac2_min, double dac3_max, double b) + { + return 1. / b * (dac2_min - dac3_max); + } + + double get_beta_max(double dac2_max, double dac3_min, double b) + { + return 1. / b * (dac2_max - dac3_min); + } + + //////////////////////////////////////////////////////// + //////////////////////////////////////////////////////// + + // (v1, v2, v3) ---> (dac1, dac2, dac3) + double v1_to_dac1(double v1, double v) + { + return v1 / v; + } + + double v2_to_dac2(double v2, double v) + { + return v2 / v; + } + + double v3_to_dac3(double v3, double v) + { + return v3 / v; + // ((4.096 / pow(2.0, 24)) * 60); + } + + // (dac1, dac2, dac3) ---> (v1, v2, v3) + double get_v1(double dac1, double v) + { + return dac1 * v; + } + + double get_v2(double dac2, double v) + { + return dac2 * v; + } + + double get_v3(double dac3, double v) + { + return dac3 * v; + } + + } // namespace app +} // namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/fsmCommands.hpp b/apps/fsmCtrl/fsmCommands.hpp new file mode 100644 index 000000000..c964b0b25 --- /dev/null +++ b/apps/fsmCtrl/fsmCommands.hpp @@ -0,0 +1,346 @@ +/** \file fsmCommands.hpp + * \brief Utility file for the fsmCtrl app with structre and class definitions for commands to be sent to the fsm + * + * \ingroup fsmCtrl_files + */ + +#pragma once + +#include +#include // for stringstreams +#include // for nullptr +using namespace std; + +namespace MagAOX +{ + namespace app + { + /** + * @brief Payload types for the commands + * + * The payload type is sent with the command packet and + * tells the fsm what command is being sent. + */ + static const uint16_t CGraphPayloadTypePZTDacs = 0x0002U; // Payload: 3 uint32's + static const uint16_t CGraphPayloadTypePZTDacsFloatingPoint = 0x0003U; // Payload: 3 double-precision floats + static const uint16_t CGraphPayloadTypePZTAdcs = 0x0004U; // Payload: 3 AdcAcumulators + static const uint16_t CGraphPayloadTypePZTAdcsFloatingPoint = 0x0005U; // Payload: 3 double-precision floats + static const uint16_t CGraphPayloadTypePZTStatus = 0x0006U; + + /** + * @brief Structure for the response payload of the Status command + */ + struct CGraphPZTStatusPayload + { + double P1V2; + double P2V2; + double P24V; + double P2V5; + double P3V3A; + double P6V; + double P5V; + double P3V3D; + double P4V3; + double N5V; + double N6V; + double P150V; + + bool operator==(const CGraphPZTStatusPayload *p /**< [in] the pointer to the struct to compare to*/) + { + return (P1V2 == p->P1V2 && P2V2 == p->P2V2 && P24V == p->P24V && P2V5 == p->P2V5 && P3V3A == p->P3V3A && P6V == p->P6V && P5V == p->P5V && + P3V3D == p->P3V3D && P4V3 == p->P4V3 && N5V == p->N5V && N6V == p->N6V && P150V == p->P150V); + } + + bool operator==(const CGraphPZTStatusPayload p /**< [in] the struct to compare to*/) + { + return (P1V2 == p.P1V2 && P2V2 == p.P2V2 && P24V == p.P24V && P2V5 == p.P2V5 && P3V3A == p.P3V3A && P6V == p.P6V && P5V == p.P5V && + P3V3D == p.P3V3D && P4V3 == p.P4V3 && N5V == p.N5V && N6V == p.N6V && P150V == p.P150V); + } + + CGraphPZTStatusPayload &operator=(const CGraphPZTStatusPayload *p /**< [in] the pointer to the struct to be copied*/) + { + this->P1V2 = p->P1V2; + this->P2V2 = p->P2V2; + this->P24V = p->P24V; + this->P2V5 = p->P2V5; + this->P3V3A = p->P3V3A; + this->P6V = p->P6V; + this->P5V = p->P5V; + this->P3V3D = p->P3V3D; + this->P4V3 = p->P4V3; + this->N5V = p->N5V; + this->N6V = p->N6V; + this->P150V = p->P150V; + return *this; + } + + CGraphPZTStatusPayload &operator=(const CGraphPZTStatusPayload &p /**< [in] struct to be copied*/) + { + this->P1V2 = p.P1V2; + this->P2V2 = p.P2V2; + this->P24V = p.P24V; + this->P2V5 = p.P2V5; + this->P3V3A = p.P3V3A; + this->P6V = p.P6V; + this->P5V = p.P5V; + this->P3V3D = p.P3V3D; + this->P4V3 = p.P4V3; + this->N5V = p.N5V; + this->N6V = p.N6V; + this->P150V = p.P150V; + return *this; + } + }; + + /** + * @brief Structure for the response payload of the ADC query command + */ + union AdcAccumulator + { + uint64_t all; + struct + { + int64_t Samples : 24; + int64_t reserved : 24; + uint16_t NumAccums; + + } __attribute__((__packed__)); + + AdcAccumulator() { all = 0; } + + void log() const + { + std::ostringstream oss; + oss << "AdcAccumulator: Samples: " << (double)Samples << " (x" << (unsigned long)(all >> 32) << (unsigned long)(all) << "), NumAccums: " << (unsigned long)NumAccums << "(0x" << (unsigned long)NumAccums << ")"; + MagAOXAppT::log(oss.str()); + } + + } __attribute__((__packed__)); + + /** + * @brief Base class for all the fsm queries + * + * PZTQuery is the class from which all the query classes inherit. + * It ensures that the all implement a minimal interfaces that includes + * processReply, logReply and errorLogString. + */ + class PZTQuery + { + public: + std::string startLog = ""; + std::string endLog = ""; + + virtual ~PZTQuery() = default; + virtual void errorLogString(const size_t ParamsLen) = 0; + virtual void processReply(char const *Params, const size_t ParamsLen) = 0; + virtual void logReply() = 0; + virtual uint16_t getPayloadType() const + { + return PayloadType; + } + virtual void *getPayloadData() const + { + return PayloadData; + } + virtual uint16_t getPayloadLen() const + { + return PayloadLen; + } + virtual void setPayload(void *newPayloadData, uint16_t newPayloadLen) + { + PayloadData = newPayloadData; + PayloadLen = newPayloadLen; + } + virtual void resetPayload() + { + PayloadData = DefaultPayloadData; + PayloadLen = DefaultPayloadLen; + } + + protected: + uint16_t PayloadType = -1; + void *DefaultPayloadData = NULL; + size_t DefaultPayloadLen = 0; + void *PayloadData = DefaultPayloadData; + size_t PayloadLen = DefaultPayloadLen; + }; + + // Derived classes + /** + * @brief Child query class that handles sending a status query to the fsm + */ + class StatusQuery : public PZTQuery + { + public: + StatusQuery() + { + PayloadType = CGraphPayloadTypePZTStatus; + startLog = "PZTStatus: Querying status."; + endLog = "PZTStatus: Finished querying status."; + } + + const CGraphPZTStatusPayload *ParamsPtr = nullptr; + CGraphPZTStatusPayload Status; + + void processReply(char const *Params, const size_t ParamsLen) override + { + if ((NULL != Params) && (ParamsLen >= (3 * sizeof(double)))) + { + ParamsPtr = reinterpret_cast(Params); + Status = *ParamsPtr; + } + else + { + errorLogString(ParamsLen); + } + } + + void errorLogString(const size_t ParamsLen) override + { + std::ostringstream oss; + oss << "PZTStatus: Short packet: " << ParamsLen << " (expected " << (3 * sizeof(double)) << " bytes): "; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + void logReply() override + { + std::ostringstream oss; + oss << "BinaryPZTStatus Command: Values with corrected units follow:\n"; + oss << "P1V2: " << std::fixed << std::setprecision(6) << Status.P1V2 << " V\n"; + oss << "P2V2: " << std::fixed << std::setprecision(6) << Status.P2V2 << " V\n"; + oss << "P24V: " << std::fixed << std::setprecision(6) << Status.P24V << " V\n"; + oss << "P2V5: " << std::fixed << std::setprecision(6) << Status.P2V5 << " V\n"; + oss << "P3V3A: " << std::fixed << std::setprecision(6) << Status.P3V3A << " V\n"; + oss << "P6V: " << std::fixed << std::setprecision(6) << Status.P6V << " V\n"; + oss << "P5V: " << std::fixed << std::setprecision(6) << Status.P5V << " V\n"; + oss << "P3V3D: " << std::fixed << std::setprecision(6) << Status.P3V3D << " V\n"; + oss << "P4V3: " << std::fixed << std::setprecision(6) << Status.P4V3 << " V\n"; + oss << "N5V: " << std::fixed << std::setprecision(6) << Status.N5V << " V\n"; + oss << "N6V: " << std::fixed << std::setprecision(6) << Status.N6V << " V\n"; + oss << "P150V: " << std::fixed << std::setprecision(6) << Status.P150V << " V"; + MagAOXAppT::log(oss.str()); + } + }; + + /** + * @brief Child query class that handles querying the fsm for the ADC values + */ + class AdcsQuery : public PZTQuery + { + public: + AdcsQuery() + { + PayloadType = CGraphPayloadTypePZTAdcs; + startLog = "PZTAdcs: Querying ADCs."; + endLog = "PZTAdcs: Finished querying ADCs."; + } + + const AdcAccumulator *ParamsPtr = nullptr; + AdcAccumulator AdcVals[3]; + + void processReply(char const *Params, const size_t ParamsLen) override + { + if ((NULL != Params) && (ParamsLen >= (3 * sizeof(AdcAccumulator)))) + { + ParamsPtr = reinterpret_cast(Params); + std::copy(ParamsPtr, ParamsPtr + 3, AdcVals); + } + else + { + errorLogString(ParamsLen); + } + } + + void errorLogString(const size_t ParamsLen) override + { + std::ostringstream oss; + oss << "BinaryPZTAdcsCommand: Short packet: " << ParamsLen << " (expected " << (3 * sizeof(AdcAccumulator)) << " bytes): "; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + void logReply() override + { + MagAOXAppT::log("BinaryPZTAdcsCommand: "); + AdcVals[0].log(); + AdcVals[1].log(); + AdcVals[2].log(); + } + }; + + /** + * @brief Child query class that handles querying the fsm for the DAC values + * and sending new DAC values to the fsm + */ + class DacsQuery : public PZTQuery + { + public: + DacsQuery() + { + PayloadType = CGraphPayloadTypePZTDacs; + startLog = "PZTDacs: Querying DACs."; + endLog = "PZTDacs: Finished querying DACs."; + } + + const uint32_t *ParamsPtr = nullptr; + uint32_t DacSetpoints[3]; + + virtual void setPayload(const void *Setpoints, uint16_t SetpointsLen) + { + PayloadData = const_cast(Setpoints); + PayloadLen = SetpointsLen; + } + + void processReply(char const *Params, const size_t ParamsLen) override + { + if ((NULL != Params) && (ParamsLen >= (3 * sizeof(uint32_t)))) + { + ParamsPtr = reinterpret_cast(Params); + std::copy(ParamsPtr, ParamsPtr + 3, DacSetpoints); + + // 1. Currently the fsm appends 58 to query (not set) dac response packages + // (24 vs 32 bytes -> there are 8 free bytes that currently are + // filled with 58; can replace with another meaningful value if helpful, + // details from Summer) - need to remove that. + // 2. Also, the returned value is half of the actual value (bit shift operation + // on the fsm side) - so will need to multiply by two for the forseeable + // future. + // NOTE: This is only the case for the queries that 'get' the dac values, not + // for the ones that 'set' them, so we need to first check which type of query + // we're dealing with. 'Set' queries have an empty payload + + if (PayloadLen == 0) + { + // Drop the first two hex digits + DacSetpoints[0] = DacSetpoints[0] & 0x00FFFFFF; // Mask out the first two bytes + DacSetpoints[1] = DacSetpoints[1] & 0x00FFFFFF; // Mask out the first two bytes + DacSetpoints[2] = DacSetpoints[2] & 0x00FFFFFF; // Mask out the first two bytes + + // Double + DacSetpoints[0] *= 2; + DacSetpoints[1] *= 2; + DacSetpoints[2] *= 2; + } + } + else + { + errorLogString(ParamsLen); + } + } + + void errorLogString(const size_t ParamsLen) override + { + std::ostringstream oss; + oss << "BinaryPZTDacsCommand: Short packet: " << ParamsLen << " (expected " << (3 * sizeof(uint32_t)) << " bytes): "; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + + void logReply() override + { + std::ostringstream oss; + oss << "BinaryPZTDacsCommand: 0x" << std::hex << DacSetpoints[0] << " | 0x" << std::hex << DacSetpoints[1] << " | 0x" << std::hex << DacSetpoints[2]; + MagAOXAppT::log(oss.str()); + } + }; + + } // namespace app +} // namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/fsmCtrl.cpp b/apps/fsmCtrl/fsmCtrl.cpp new file mode 100644 index 000000000..479b61177 --- /dev/null +++ b/apps/fsmCtrl/fsmCtrl.cpp @@ -0,0 +1,16 @@ +/** \file fsmCtrl.cpp + * \brief The MagAO-X xxxxx main program source file. + * + * \ingroup fsmCtrl_files + */ + +#include "fsmCtrl.hpp" + + +int main(int argc, char **argv) +{ + MagAOX::app::fsmCtrl xapp; + + return xapp.main(argc, argv); + +} diff --git a/apps/fsmCtrl/fsmCtrl.hpp b/apps/fsmCtrl/fsmCtrl.hpp new file mode 100644 index 000000000..7923d698e --- /dev/null +++ b/apps/fsmCtrl/fsmCtrl.hpp @@ -0,0 +1,1300 @@ +/** \file fsmCtrl.hpp + * \brief The MagAO-X XXXXXX header file + * + * \ingroup fsmCtrl_files + */ + +#pragma once + +#include "../../libMagAOX/libMagAOX.hpp" //Note this is included on command line to trigger pch +#include "../../magaox_git_version.h" + +#include +#include + +#include +using namespace std; + +#include + +typedef MagAOX::app::MagAOXApp MagAOXAppT; // This needs to be before the other header files for logging to work in other headers + +#include "conversion.hpp" +#include "fsmCommands.hpp" +#include "binaryUart.hpp" +#include "cGraphPacket.hpp" +#include "linux_pinout_client_socket.hpp" +#include "socket.hpp" + +/** \defgroup fsmCtrl + * \brief Application to interface with ESC FSM + * + * Application Documentation + * + * \ingroup apps + * + */ + +/** \defgroup fsmCtrl_files + * \ingroup fsmCtrl + */ + +namespace MagAOX +{ + namespace app + { + + /// The MagAO-X ESC FSM interface + /** + * \ingroup fsmCtrl + */ + class fsmCtrl : public MagAOXApp, public dev::telemeter, public dev::shmimMonitor + { + + // Give the test harness access. + friend class fsmCtrl_test; + + friend class dev::telemeter; + typedef dev::telemeter telemeterT; + + friend class dev::shmimMonitor; + + protected: + /** \name Configurable Parameters + *@{ + */ + std::string type; + std::string PortName; + int nHostPort; + int period_s; + double m_a; + double m_b; + double m_v; + double m_dac1_min; + double m_dac1_max; + double m_dac2_min; + double m_dac2_max; + double m_dac3_min; + double m_dac3_max; + + // input parameters + std::string m_inputType; + std::string m_inputToggle; + + // here add parameters which will be config-able at runtime + ///@} + + char Buffer[4096]; + CGraphPacket SocketProtocol; + linux_pinout_client_socket LocalPortPinout; + BinaryUart UartParser; + PZTQuery *statusQuery = new StatusQuery(); + PZTQuery *adcsQuery = new AdcsQuery(); + PZTQuery *dacsQuery = new DacsQuery(); + uint32_t targetSetpoints[3]; + + double m_dac1{0}; + double m_dac2{0}; + double m_dac3{0}; + + double m_adc1{0}; + double m_adc2{0}; + double m_adc3{0}; + + const std::string DACS = "dacs"; + const std::string VOLTAGES = "voltages"; + const std::string ANGLES = "angles"; + const std::string SHMIM = "shmim"; + const std::string INDI = "indi"; + + protected: + // INDI properties + pcf::IndiProperty m_indiP_val1; + pcf::IndiProperty m_indiP_val2; + pcf::IndiProperty m_indiP_val3; + pcf::IndiProperty m_indiP_dac1; + pcf::IndiProperty m_indiP_dac2; + pcf::IndiProperty m_indiP_dac3; + pcf::IndiProperty m_indiP_adc1; + pcf::IndiProperty m_indiP_adc2; + pcf::IndiProperty m_indiP_adc3; + pcf::IndiProperty m_indiP_conversion_factors; + pcf::IndiProperty m_indiP_input; + pcf::IndiProperty m_indiP_query; + + public: + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_val1); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_val2); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_val3); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_dac1); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_dac2); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_dac3); + // INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_adc1); + // INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_adc2); + // INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_adc3); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_conversion_factors); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_input); + INDI_NEWCALLBACK_DECL(fsmCtrl, m_indiP_query); + + public: + /// Default c'tor. + fsmCtrl(); + + /// D'tor, declared and defined for noexcept. + ~fsmCtrl() noexcept + { + } + + virtual void setupConfig(); + + /// Implementation of loadConfig logic, separated for testing. + /** This is called by loadConfig(). + */ + int loadConfigImpl(mx::app::appConfigurator &_config /**< [in] an application configuration from which to load values*/); + + virtual void loadConfig(); + + /// Startup function + /** Set up INDI props & other startup prep + * + */ + virtual int appStartup(); + + /// Implementation of the logic for fsmCtrl. + /** + * \returns 0 on no critical error + * \returns -1 on an error requiring shutdown + */ + virtual int appLogic(); + + /// Shutdown the app. + /** + * + */ + virtual int appShutdown(); + + /// TODO: Test the connection to the fsm + int testConnection(); + + /// Connect to fsm via Socket + /** + * + * \returns 0 if connection successful + * \returns -1 on an error + */ + int socketConnect(); + + /** + * @brief Request fsm status + * + * Wrapper that calls query() with instance of StatusQuery. + * Response is stored in instance's Status member. + * It returns fsm telemetry that is logged every 10s by the telemeter. + * Output in /opt/telem/fsmCtrl_xxxxx.bintel + */ + void queryStatus(); + + /** + * @brief Request fsm's ADC values + * + * Wrapper that calls query() with instance of AdcsQuery. + * Response is stored in instance's AdcVals member. + * Response is also logged in /opt/tele/fsmCtrl_xxxxxx.binlog + */ + void queryAdcs(); + + /** + * @brief Request fsm's DAC values + * + * Wrapper that calls query() with instance of DacsQuery. + * Response is stored in instance's DacSetpoints member. + * Response is also logged in /opt/tele/fsmCtrl_xxxxxx.binlog + */ + void queryDacs(); + + /** + * @brief Set fsm's DACs values to those in the argument. + * + * Wrapper that calls query() with instance of DacsQuery and + * three new values for the DACs. + * Response is stored in instance's DacSetpoints member. + * Response is also logged in /opt/tele/fsmCtrl_xxxxxx.binlog + * + * @param Setpoints pointer to an array of three uint32_t values + */ + int setDacs(uint32_t *); + + /** + * @brief Query interface for the fsm + * + * Function that sends a command packet to the fsm and waits for a response. + * If a response it received it processes the response as appropriate for the + * command sent. + * + * @param pztQuery pointer to a class inheriting from PZTQuery (see fsmCommands.hpp) + */ + void query(PZTQuery *); + + /** + * @brief Utility function that sets 'current' INDI values, if updated + * + * Function that takes the values in m_dac1, m_dac2 and m_dac3, transforms them + * (if necessary) to the type specified by m_inputType and updates the corresponding + * INDI parameter's 'current' value. + */ + void updateINDICurrentParams(); + + /** \name Telemeter Interface + * + * @{ + */ + /** + * @brief Required by Telemeter Interface + * + * \returns 0 on succcess + * \returns -1 on error + */ + int checkRecordTimes(); + + /** + * @brief Required by Telemeter Interface + * + * @param telem_fsm_ptr pointer to telem_fsm flatbuffer_log structure describing telem inputs & outputs + * \returns 0 on succcess + * \returns -1 on error + */ + int recordTelem(const telem_fsm *); + + /** + * @brief Required by Telemeter Interface + * + * @param force boolean; Telemetry is recorded every m_maxInterval (default value of 10) seconds. + * If 'true', force telemetry record outside of interval. + * \returns 0 on succcess + */ + int recordFsm(bool force = false); + ///@} + + /** \name shmim Monitor Interface + * + * @{ + */ + + /** + * Called after shmimMonitor connects to the fsm stream. + * + * \returns 0 on success + * \returns -1 if incorrect size or data type in stream. + */ + int allocate(const dev::shmimT &sp); + + /** + * Called by shmimMonitor when a new fsm command is available. + * + * \returns 0 on success + * \returns -1 if incorrect size or data type in stream. + */ + int processImage(void *curr_src, + const dev::shmimT &sp); + + /** + * @brief Send to fsm new DAC values from shmim + * + * Called as part of processImage. + * Checks shmim has an inputType keyword and that its value is 'dacs', 'voltages' or 'angles'. + * Updates INDI input.type property, if different. + * Updates corresponding INDI 'target' values with shmim values. + * Converts shmim values from specified inputType to DACs. + * Calls setDacs function with new DAC values. + * + * \returns 0 on success + * \returns -1 if incorrect size or data type in stream. + */ + int commandFSM(void *curr_src); + ///@} + }; + + fsmCtrl::fsmCtrl() : MagAOXApp(MAGAOX_CURRENT_SHA1, MAGAOX_REPO_MODIFIED), UartParser(LocalPortPinout, SocketProtocol, PacketCallbacks, false) + { + m_powerMgtEnabled = true; + m_getExistingFirst = true; // get existing shmim (??? should or shouldn't) + return; + } + + void fsmCtrl::setupConfig() + { + shmimMonitor::setupConfig(config); + + config.add("parameters.connection_type", "", "parameters.connection_type", argType::Required, "parameters", "connection_type", false, "string", "The type of connection: serial_port or socket."); + config.add("parameters.period_s", "", "parameters.period_s", argType::Required, "parameters", "period_s", false, "int", "The period of status queries to the fsm."); + + config.add("socket.client_entrance_ip", "", "socket.client_entrance_ip", argType::Required, "socket", "client_entrance_ip", false, "string", "The IP address on the client machine that the tunnel is set up from."); + config.add("socket.host_port", "", "socket.host_port", argType::Required, "socket", "host_port", false, "int", "The port at which the fsm driver is listening for connections."); + + config.add("fsm.a", "", "fsm.a", argType::Required, "fsm", "a", false, "double", "Conversion factor for converting from alpha/beta/z to actuator linear displacements."); + config.add("fsm.b", "", "fsm.b", argType::Required, "fsm", "b", false, "double", "Conversion factor for converting from alpha/beta/z to actuator linear displacements."); + config.add("fsm.v", "", "fsm.b", argType::Required, "fsm", "v", false, "double", "Conversion factor for converting from voltages to dacs."); + config.add("fsm.dac1_min", "", "fsm.dac1_min", argType::Required, "fsm", "dac1_min", false, "double", "Min safe value for dac1."); + config.add("fsm.dac1_max", "", "fsm.dac1_max", argType::Required, "fsm", "dac1_max", false, "double", "Max safe value for dac1."); + config.add("fsm.dac2_min", "", "fsm.dac2_min", argType::Required, "fsm", "dac2_min", false, "double", "Min safe value for dac2."); + config.add("fsm.dac2_max", "", "fsm.dac2_max", argType::Required, "fsm", "dac2_max", false, "double", "Max safe value for dac2."); + config.add("fsm.dac3_min", "", "fsm.dac3_min", argType::Required, "fsm", "dac3_min", false, "double", "Min safe value for dac3."); + config.add("fsm.dac3_max", "", "fsm.dac3_max", argType::Required, "fsm", "dac3_max", false, "double", "Max safe value for dac3."); + + // shmim parameters + config.add("shmimMonitor.shmimName", "", "shmimMonitor.shmimName", argType::Required, "shmimMonitor", "shmimName", false, "string", "The name of the ImageStreamIO shared memory image. Will be used as /tmp/.im.shm. Default is fsm"); + + config.add("shmimMonitor.width", "", "shmimMonitor.width", argType::Required, "shmimMonitor", "width", false, "string", "The width of the FSM in actuators."); + config.add("shmimMonitor.height", "", "shmimMonitor.height", argType::Required, "shmimMonitor", "height", false, "string", "The height of the FSM in actuators."); + + config.add("input.type", "", "input.type", argType::Required, "input", "type", false, "string", "The type of values that the shmim contains. Can be 'dacs', 'voltages' or 'angles'."); + config.add("input.toggle", "", "input.toggle", argType::Required, "input", "toggle", false, "string", "Where the input comes from. Can be 'shmim', 'indi'."); + telemeterT::setupConfig(config); + } + + int fsmCtrl::loadConfigImpl(mx::app::appConfigurator &_config) + { + _config(type, "parameters.connection_type"); + _config(period_s, "parameters.period_s"); + + _config(PortName, "socket.client_entrance_ip"); + _config(nHostPort, "socket.host_port"); + + _config(m_a, "fsm.a"); + _config(m_b, "fsm.b"); + _config(m_v, "fsm.v"); + _config(m_dac1_min, "fsm.dac1_min"); + _config(m_dac1_max, "fsm.dac1_max"); + _config(m_dac2_min, "fsm.dac2_min"); + _config(m_dac2_max, "fsm.dac2_max"); + _config(m_dac3_min, "fsm.dac3_min"); + _config(m_dac3_max, "fsm.dac3_max"); + + _config(shmimMonitor::m_width, "shmimMonitor.width"); + _config(shmimMonitor::m_height, "shmimMonitor.height"); + + m_inputType = DACS; + _config(m_inputType, "input.type"); + m_inputToggle = SHMIM; + _config(m_inputToggle, "input.toggle"); + + shmimMonitor::loadConfig(_config); + return 0; + } + + void fsmCtrl::loadConfig() + { + if (loadConfigImpl(config) < 0) + { + log("Error during config", logPrio::LOG_CRITICAL); + m_shutdown = true; + } + + if (telemeterT::loadConfig(config) < 0) + { + log("Error during telemeter config", logPrio::LOG_CRITICAL); + m_shutdown = true; + } + } + + int fsmCtrl::appStartup() + { + if (telemeterT::appStartup() < 0) + { + return log({__FILE__, __LINE__}); + } + + if (shmimMonitor::appStartup() < 0) + { + return log({__FILE__, __LINE__}); + } + + // set up the INDI properties + // dac boundaries + REG_INDI_NEWPROP(m_indiP_dac1, "dac_1", pcf::IndiProperty::Number); + m_indiP_dac1.add(pcf::IndiElement("min")); + m_indiP_dac1.add(pcf::IndiElement("max")); + m_indiP_dac1["min"] = m_dac1_min; + m_indiP_dac1["max"] = m_dac1_max; + REG_INDI_NEWPROP(m_indiP_dac2, "dac_2", pcf::IndiProperty::Number); + m_indiP_dac2.add(pcf::IndiElement("min")); + m_indiP_dac2.add(pcf::IndiElement("max")); + m_indiP_dac2["min"] = m_dac2_min; + m_indiP_dac2["max"] = m_dac2_max; + REG_INDI_NEWPROP(m_indiP_dac3, "dac_3", pcf::IndiProperty::Number); + m_indiP_dac3.add(pcf::IndiElement("min")); + m_indiP_dac3.add(pcf::IndiElement("max")); + m_indiP_dac3["min"] = m_dac3_min; + m_indiP_dac3["max"] = m_dac3_max; + + // vals + REG_INDI_NEWPROP(m_indiP_val1, "val_1", pcf::IndiProperty::Number); + m_indiP_val1.add(pcf::IndiElement("current")); + m_indiP_val1.add(pcf::IndiElement("target")); + m_indiP_val1["current"] = -99999; + m_indiP_val1["target"] = -99999; + REG_INDI_NEWPROP(m_indiP_val2, "val_2", pcf::IndiProperty::Number); + m_indiP_val2.add(pcf::IndiElement("current")); + m_indiP_val2.add(pcf::IndiElement("target")); + m_indiP_val2["current"] = -99999; + m_indiP_val2["target"] = -99999; + REG_INDI_NEWPROP(m_indiP_val3, "val_3", pcf::IndiProperty::Number); + m_indiP_val3.add(pcf::IndiElement("current")); + m_indiP_val3.add(pcf::IndiElement("target")); + m_indiP_val3["current"] = -99999; + m_indiP_val3["target"] = -99999; + + // adcs + REG_INDI_NEWPROP_NOCB(m_indiP_adc1, "adc_1", pcf::IndiProperty::Number); + m_indiP_adc1.add(pcf::IndiElement("current")); + m_indiP_adc1["current"] = -99999; + REG_INDI_NEWPROP_NOCB(m_indiP_adc2, "adc_2", pcf::IndiProperty::Number); + m_indiP_adc2.add(pcf::IndiElement("current")); + m_indiP_adc2["current"] = -99999; + REG_INDI_NEWPROP_NOCB(m_indiP_adc3, "adc_3", pcf::IndiProperty::Number); + m_indiP_adc3.add(pcf::IndiElement("current")); + m_indiP_adc3["current"] = -99999; + + // conversion_factors + REG_INDI_NEWPROP(m_indiP_conversion_factors, "conversion_factors", pcf::IndiProperty::Number); + m_indiP_conversion_factors.add(pcf::IndiElement("a")); + m_indiP_conversion_factors["a"] = m_a; + m_indiP_conversion_factors.add(pcf::IndiElement("b")); + m_indiP_conversion_factors["b"] = m_b; + m_indiP_conversion_factors.add(pcf::IndiElement("v")); + m_indiP_conversion_factors["v"] = m_v; + + // input + REG_INDI_NEWPROP(m_indiP_input, "input", pcf::IndiProperty::Text); + m_indiP_input.add(pcf::IndiElement("toggle")); + m_indiP_input["toggle"] = m_inputToggle; + m_indiP_input.add(pcf::IndiElement("type")); + m_indiP_input["type"] = m_inputType; + + // type of query + REG_INDI_NEWPROP(m_indiP_query, "status", pcf::IndiProperty::Text); + m_indiP_query.add(pcf::IndiElement("query")); + m_indiP_query["query"] = "none"; + + return 0; + } + + int fsmCtrl::appLogic() + { + if (shmimMonitor::appLogic() < 0) + { + return log({__FILE__, __LINE__}); + } + + // Set the INDI name, width & heigh properties to those of the shmim + if (shmimMonitor::updateINDI() < 0) + { + log({__FILE__, __LINE__}); + } + + if (state() == stateCodes::POWERON) + { + if (!powerOnWaitElapsed()) + { + return 0; + } + state(stateCodes::NOTCONNECTED); + } + + if (state() == stateCodes::NOTCONNECTED) + { + int rv; + rv = socketConnect(); + + if (rv == 0) + { + state(stateCodes::CONNECTED); + } + } + + if (state() == stateCodes::CONNECTED) + { + // Get current adc values + queryAdcs(); + + // Get current dac values + queryDacs(); + + // Get telemetry + queryStatus(); + + if (m_inputToggle == SHMIM) + { + state(stateCodes::OPERATING); + } + if (m_inputToggle == INDI) + { + state(stateCodes::READY); + } + } + + if ((state() == stateCodes::OPERATING) || (state() == stateCodes::READY)) + { + if (telemeterT::appLogic() < 0) + { + log({__FILE__, __LINE__}); + return 0; + } + } + + return 0; + } + + int fsmCtrl::appShutdown() + { + telemeterT::appShutdown(); + shmimMonitor::appShutdown(); + + return 0; + } + + ////////////// + // CONNECTION + ////////////// + + /// TODO: Test the connection to the device + int fsmCtrl::testConnection() + { + return 0; + } + + int fsmCtrl::socketConnect() + { + // Tell C lib (stdio.h) not to buffer output, so we can ditch all the fflush(stdout) calls... + setvbuf(stdout, NULL, _IONBF, 0); + + log("Welcome to SerialPortBinaryCmdr!"); + log("In order to tunnel to the lab use the following command before running this program:\n ssh -L 1337:localhost:1337 -N -f fsm \n(where fsm is the ssh alias of the remote server)!"); + + int err = fsmCtrl::LocalPortPinout.init(nHostPort, PortName.c_str()); + if (IUart::IUartOK != err) + { + log({__FILE__, __LINE__, errno, "SerialPortBinaryCmdr: can't open socket (" + PortName + ":" + std::to_string(nHostPort) + "), exiting.\n"}); + return -1; + } + + log("Starting to do something"); + + UartParser.Debug(false); + return 0; + } + + ////////////// + // FSM QUERIES + ////////////// + + // Function to request fsm Status + void fsmCtrl::queryStatus() + { + log(statusQuery->startLog); + query(statusQuery); + log(statusQuery->endLog); + recordFsm(false); + } + + // Function to request fsm ADCs + void fsmCtrl::queryAdcs() + { + log(adcsQuery->startLog); + query(adcsQuery); + log(adcsQuery->endLog); + + AdcsQuery *castAdcsQuery = dynamic_cast(adcsQuery); + + double samples1 = static_cast(castAdcsQuery->AdcVals[0].Samples); + double samples2 = static_cast(castAdcsQuery->AdcVals[1].Samples); + double samples3 = static_cast(castAdcsQuery->AdcVals[2].Samples); + + double numAccums1 = static_cast(castAdcsQuery->AdcVals[0].NumAccums); + double numAccums2 = static_cast(castAdcsQuery->AdcVals[1].NumAccums); + double numAccums3 = static_cast(castAdcsQuery->AdcVals[2].NumAccums); + + m_adc1 = (8.192 * ((samples1 - 0) / numAccums1)) / 16777216.0; + m_adc2 = (8.192 * ((samples2 - 0) / numAccums2)) / 16777216.0; + m_adc3 = (8.192 * ((samples3 - 0) / numAccums3)) / 16777216.0; + + updateIfChanged(m_indiP_adc1, "current", m_adc1); + updateIfChanged(m_indiP_adc2, "current", m_adc2); + updateIfChanged(m_indiP_adc3, "current", m_adc3); + + adcsQuery->logReply(); + } + + // Function to request fsm DACs + void fsmCtrl::queryDacs() + { + log(dacsQuery->startLog); + query(dacsQuery); + log(dacsQuery->endLog); + + DacsQuery *castDacsQuery = dynamic_cast(dacsQuery); + + m_dac1 = static_cast(castDacsQuery->DacSetpoints[0]); + m_dac2 = static_cast(castDacsQuery->DacSetpoints[1]); + m_dac3 = static_cast(castDacsQuery->DacSetpoints[2]); + + updateINDICurrentParams(); + + dacsQuery->logReply(); + } + + // Function to set fsm DACs + int fsmCtrl::setDacs(uint32_t *Setpoints) + { + if (Setpoints[0] < m_dac1_min || Setpoints[0] > m_dac1_max) + { + std::ostringstream oss; + oss << "Requested dac1 out of range; (min|dac1|max) : (" << m_dac1_min << "|" << Setpoints[0] << "|" << m_dac1_max << ");"; + log(oss.str(), logPrio::LOG_ERROR); + return -1; + } + + if (Setpoints[1] < m_dac2_min || Setpoints[1] > m_dac2_max) + { + std::ostringstream oss; + oss << "Requested dac2 out of range; (min|dac2|max) : (" << m_dac2_min << "|" << Setpoints[1] << "|" << m_dac2_max << ");"; + log(oss.str(), logPrio::LOG_ERROR); + return -1; + } + + if (Setpoints[2] < m_dac3_min || Setpoints[2] > m_dac3_max) + { + std::ostringstream oss; + oss << "Requested dac3 out of range; (min|dac3|max) : (" << m_dac3_min << "|" << Setpoints[2] << "|" << m_dac3_max << ");"; + log(oss.str(), logPrio::LOG_ERROR); + return -1; + } + + std::ostringstream oss; + oss << "SETDACS: " << Setpoints[0] << " | " << Setpoints[1] << " | " << Setpoints[2]; + log(oss.str()); + + DacsQuery *castDacsQuery = dynamic_cast(dacsQuery); + + log(dacsQuery->startLog); + castDacsQuery->setPayload(Setpoints, 3 * sizeof(uint32_t)); + query(castDacsQuery); + log(castDacsQuery->endLog); + + castDacsQuery->logReply(); + castDacsQuery->resetPayload(); + + m_dac1 = castDacsQuery->DacSetpoints[0]; + m_dac2 = castDacsQuery->DacSetpoints[1]; + m_dac3 = castDacsQuery->DacSetpoints[2]; + updateINDICurrentParams(); + + queryDacs(); + queryAdcs(); + return 0; + } + + void fsmCtrl::query(PZTQuery *pztQuery) + { + // Send command packet + (&UartParser)->TxBinaryPacket(pztQuery->getPayloadType(), pztQuery->getPayloadData(), pztQuery->getPayloadLen()); + + // The packet is read byte by byte, so keep going while there are bytes left + bool Bored = false; + while (!Bored) + { + Bored = true; + if (UartParser.Process(pztQuery)) + { + Bored = false; + } + + if (false == LocalPortPinout.connected()) + { + int err = LocalPortPinout.init(nHostPort, PortName.c_str()); + if (IUart::IUartOK != err) + { + log({__FILE__, __LINE__, errno, "SerialPortBinaryCmdr: can't open socket (" + PortName + ":" + std::to_string(nHostPort)}); + } + } + } + } + + ///////////////////////// + // TELEMETER INTERFACE + ///////////////////////// + + int fsmCtrl::checkRecordTimes() + { + return telemeterT::checkRecordTimes(telem_fsm()); + } + + int fsmCtrl::recordTelem(const telem_fsm *) + { + return recordFsm(true); + } + + int fsmCtrl::recordFsm(bool force) + { + static CGraphPZTStatusPayload LastStatus; ///< Structure holding the previous fsm voltage measurement. + StatusQuery *statusQueryPtr = dynamic_cast(statusQuery); + + if (!(LastStatus == statusQueryPtr->Status) || force) + { + LastStatus = statusQueryPtr->Status; + telem({LastStatus.P1V2, LastStatus.P2V2, LastStatus.P24V, LastStatus.P2V5, LastStatus.P3V3A, LastStatus.P6V, LastStatus.P5V, LastStatus.P3V3D, LastStatus.P4V3, LastStatus.N5V, LastStatus.N6V, LastStatus.P150V}); + } + + return 0; + } + + ///////////////////////// + // SHMIMMONITOR INTERFACE + ///////////////////////// + + int fsmCtrl::allocate(const dev::shmimT &sp) + { + static_cast(sp); // be unused + + int err = 0; + + // if(m_width != m_fsmWidth) + // { + // log({__FILE__,__LINE__, "shmim width does not match configured FSM width"}); + // ++err; + // } + + // if(m_height != m_fsmHeight) + // { + // log({__FILE__,__LINE__, "shmim height does not match configured FSM height"}); + // ++err; + // } + + if (err) + return -1; + + return 0; + } + + int fsmCtrl::processImage(void *curr_src, + const dev::shmimT &sp) + { + static_cast(sp); // be unused + + int rv = commandFSM(curr_src); + + if (rv < 0) + { + log({__FILE__, __LINE__, errno, rv, "Error from commandFSM"}); + return rv; + } + + return rv; + } + + int fsmCtrl::commandFSM(void *curr_src) + { + std::string inputType = ""; + + // Check that shmim has inputType keyword + int kwn = 0; + while ((m_imageStream.kw[kwn].type != 'N') && (kwn < m_imageStream.md->NBkw)) + { + std::string name(m_imageStream.kw[kwn].name); + if (name == "inputType") + { + inputType = m_imageStream.kw[kwn].value.valstr; + if (!(inputType == DACS || inputType == VOLTAGES || inputType == ANGLES)) + { + std::ostringstream oss; + oss << "Shmim '" << shmimMonitor::m_shmimName << "' has an inputType keyword with a value other than 'dacs', 'voltages', or 'angles': " << inputType; + log({__FILE__, __LINE__, errno, oss.str()}); + return -1; + } + + m_inputType = inputType; + updateIfChanged(m_indiP_input, "type", m_inputType); + } + kwn++; + } + + if (inputType == "") + { + std::ostringstream oss; + oss << "Shmim '" << shmimMonitor::m_shmimName << "' does not have an inputType keyword with a value of 'dacs', 'voltages', or 'angles'."; + log({__FILE__, __LINE__, errno, oss.str()}); + return -1; + } + + uint32_t dacs[3] = {0, 0, 0}; + + // if(state() != stateCodes::OPERATING) return 0; + float val1, val2, val3; + val1 = ((float *)curr_src)[0]; + val2 = ((float *)curr_src)[1]; + val3 = ((float *)curr_src)[2]; + + updateIfChanged(m_indiP_val1, "target", val1); + updateIfChanged(m_indiP_val2, "target", val2); + updateIfChanged(m_indiP_val3, "target", val3); + + if (m_inputType == DACS) + { + dacs[0] = val1; + dacs[1] = val2; + dacs[2] = val3; + } + else if (m_inputType == VOLTAGES) + { + dacs[0] = v1_to_dac1(val1, m_v); + dacs[1] = v2_to_dac2(val2, m_v); + dacs[2] = v3_to_dac3(val3, m_v); + } + else if (m_inputType == ANGLES) + { + + dacs[0] = angles_to_dac1(val1, val3, m_a); + dacs[1] = angles_to_dac2(val1, val2, val3, m_a, m_b); + dacs[2] = angles_to_dac3(val1, val2, val3, m_a, m_b); + } + + std::ostringstream oss; + oss << "SHMIM dacs callback: " << dacs[0] << " | " << dacs[1] << " | " << dacs[2]; + log(oss.str()); + + std::unique_lock lock(m_indiMutex); + + return setDacs(dacs); + } + + //////////////////// + // INDI CALLBACKS + //////////////////// + + // callback from setting m_indiP_val1 + // only 'target' is editable ('current' should be updated by code) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_val1) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_val1, ipRecv); + + float current = -999999, target = -999999; + + if (ipRecv.find("current")) + { + current = ipRecv["current"].get(); + } + + if (ipRecv.find("target")) + { + target = ipRecv["target"].get(); + } + + if (target == -999999) + target = current; + + if (target == -999999) + return 0; + + // Value only settable via INDI if FSM in READY state + if (state() == stateCodes::READY) + { + // Lock the mutex, waiting if necessary + std::unique_lock lock(m_indiMutex); + + updateIfChanged(m_indiP_val1, "target", target); + + uint32_t dacs[3] = {0, 0, 0}; + + if (m_inputType == DACS) + { + dacs[0] = target; + } + else if (m_inputType == VOLTAGES) + { + dacs[0] = v1_to_dac1(target, m_v); + } + else if (m_inputType == ANGLES) + { + // Get current z to calculate dac1 from the target + double z = get_z(m_dac1, m_dac2, m_dac3); + dacs[0] = angles_to_dac1(target, z, m_a); + } + + dacs[1] = m_dac2; + dacs[2] = m_dac3; + + std::ostringstream oss; + oss << "INDI dacs callback: " << dacs[0] << " | " << dacs[1] << " | " << dacs[2]; + log(oss.str()); + + return setDacs(dacs); + } + } + + // callback from setting m_indiP_val2 + // only 'target' is editable ('current' should be updated by code) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_val2) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_val2, ipRecv); + float current = -999999, target = -999999; + + if (ipRecv.find("current")) + { + current = ipRecv["current"].get(); + } + + if (ipRecv.find("target")) + { + target = ipRecv["target"].get(); + } + + if (target == -999999) + target = current; + + if (target == -999999) + return 0; + + // Value only settable via INDI if FSM in READY state + if (state() == stateCodes::READY) + { + // Lock the mutex, waiting if necessary + std::unique_lock lock(m_indiMutex); + + updateIfChanged(m_indiP_val2, "target", target); + + uint32_t dacs[3] = {0, 0, 0}; + dacs[0] = m_dac1; + + if (m_inputType == DACS) + { + dacs[1] = target; + } + else if (m_inputType == VOLTAGES) + { + dacs[1] = v2_to_dac2(target, m_v); + } + else if (m_inputType == ANGLES) + { + // Get current alpha and z to calculate dac2 from the target + double alpha = get_alpha(m_dac1, m_dac2, m_dac3, m_a); + double z = get_z(m_dac1, m_dac2, m_dac3); + dacs[1] = angles_to_dac2(alpha, target, z, m_a, m_b); + } + + dacs[2] = m_dac3; + + std::ostringstream oss; + oss << "INDI dacs callback: " << dacs[0] << " | " << dacs[1] << " | " << dacs[2]; + log(oss.str()); + + return setDacs(dacs); + } + } + + // callback from setting m_indiP_val3 + // only 'target' is editable ('current' should be updated by code) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_val3) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_val3, ipRecv); + float current = -999999, target = -999999; + + if (ipRecv.find("current")) + { + current = ipRecv["current"].get(); + } + + if (ipRecv.find("target")) + { + target = ipRecv["target"].get(); + } + + if (target == -999999) + target = current; + + if (target == -999999) + return 0; + + // Value only settable via INDI if FSM in READY state + if (state() == stateCodes::READY) + { + // Lock the mutex, waiting if necessary + std::unique_lock lock(m_indiMutex); + + updateIfChanged(m_indiP_val3, "target", target); + + uint32_t dacs[3] = {0, 0, 0}; + dacs[0] = m_dac1; + dacs[1] = m_dac2; + + if (m_inputType == DACS) + { + dacs[2] = target; + } + else if (m_inputType == VOLTAGES) + { + dacs[2] = v3_to_dac3(target, m_v); + } + else if (m_inputType == ANGLES) + { + // Get current alpha and beta to calculate dac3 from the target + double alpha = get_alpha(m_dac1, m_dac2, m_dac3, m_a); + double beta = get_beta(m_dac2, m_dac3, m_b); + dacs[2] = angles_to_dac3(alpha, beta, target, m_a, m_b); + } + + std::ostringstream oss; + oss << "INDI dacs callback: " << dacs[0] << " | " << dacs[1] << " | " << dacs[2]; + log(oss.str()); + + return setDacs(dacs); + } + } + + // callback from setting conversion_factors + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_conversion_factors) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_conversion_factors, ipRecv); + if (ipRecv.find("a")) + { + m_a = ipRecv["a"].get(); + updateIfChanged(m_indiP_conversion_factors, "a", m_a); + } + + if (ipRecv.find("b")) + { + m_b = ipRecv["b"].get(); + updateIfChanged(m_indiP_conversion_factors, "b", m_b); + } + + if (ipRecv.find("v")) + { + m_v = ipRecv["v"].get(); + updateIfChanged(m_indiP_conversion_factors, "v", m_v); + } + + std::ostringstream oss; + oss << "INDI conversion_factors callback: " << m_a << " | " << m_b << " | " << m_v; + log(oss.str()); + } + + // callback from setting m_indiP_input (dacs, voltages, angles) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_input) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_input, ipRecv); + if (ipRecv.find("type")) + { + std::string type = ipRecv["type"].get(); + if (!(m_inputType == DACS || m_inputType == VOLTAGES || m_inputType == ANGLES)) + { + std::ostringstream oss; + oss << "input.type '" << m_inputType << "' not dacs, voltages or angles"; + log({__FILE__, __LINE__, errno, oss.str()}); + return -1; + } + + if (state() == stateCodes::READY) + { + m_inputType = type; + updateIfChanged(m_indiP_input, "type", m_inputType); + + // Reset target values + updateIfChanged(m_indiP_val1, "target", -99999); + updateIfChanged(m_indiP_val2, "target", -99999); + updateIfChanged(m_indiP_val3, "target", -99999); + // Update current values + updateINDICurrentParams(); + } + + std::ostringstream oss; + oss << "INDI input type callback: " << m_inputType; + log(oss.str()); + } + + if (ipRecv.find("toggle")) + { + std::string toggle = ipRecv["toggle"].get(); + if (toggle == SHMIM) + { + state(stateCodes::OPERATING); + updateIfChanged(m_indiP_input, "toggle", toggle); + } + if (toggle == INDI) + { + state(stateCodes::READY); + updateIfChanged(m_indiP_input, "toggle", toggle); + } + + std::ostringstream oss; + oss << "INDI input toggle: " << m_inputToggle; + log(oss.str()); + } + } + + // callback from setting m_indiP_dac1 (min, max) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_dac1) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_dac1, ipRecv); + + std::ostringstream oss; + + if (ipRecv.find("min")) + { + m_dac1_min = ipRecv["min"].get(); + oss << "INDI dac1 min callback: " << m_dac1_min; + updateIfChanged(m_indiP_dac1, "min", m_dac1_min); + } + + if (ipRecv.find("max")) + { + m_dac1_max = ipRecv["max"].get(); + oss << "INDI dac1 max callback: " << m_dac1_max; + updateIfChanged(m_indiP_dac1, "max", m_dac1_max); + } + + log(oss.str()); + + } + + // callback from setting m_indiP_dac2 (min, max) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_dac2) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_dac2, ipRecv); + + std::ostringstream oss; + + if (ipRecv.find("min")) + { + m_dac2_min = ipRecv["min"].get(); + oss << "INDI dac2 min callback: " << m_dac2_min; + updateIfChanged(m_indiP_dac2, "min", m_dac2_min); + } + + if (ipRecv.find("max")) + { + m_dac2_max = ipRecv["max"].get(); + oss << "INDI dac2 max callback: " << m_dac2_max; + updateIfChanged(m_indiP_dac2, "max", m_dac2_max); + } + + log(oss.str()); + } + + // callback from setting m_indiP_dac3 (min, max) + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_dac3) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_dac3, ipRecv); + + std::ostringstream oss; + + if (ipRecv.find("min")) + { + m_dac3_min = ipRecv["min"].get(); + oss << "INDI dac3 min callback: " << m_dac3_min; + updateIfChanged(m_indiP_dac3, "min", m_dac3_min); + } + + if (ipRecv.find("max")) + { + m_dac3_max = ipRecv["max"].get(); + oss << "INDI dac3 max callback: " << m_dac3_max; + updateIfChanged(m_indiP_dac3, "max", m_dac3_max); + } + + log(oss.str()); + } + + // // callback from setting m_indiP_adc1 - not a settable param + // INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_adc1) + // (const pcf::IndiProperty &ipRecv) + // { + // log("INDI callback."); + // return 0; + // } + + // // callback from setting m_indiP_adc2 - not a settable param + // INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_adc2) + // (const pcf::IndiProperty &ipRecv) + // { + // log("INDI callback."); + // return 0; + // } + + // // callback from setting m_indiP_adc3 - not a settable param + // INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_adc3) + // (const pcf::IndiProperty &ipRecv) + // { + // log("INDI callback."); + // return 0; + // } + + // callback from setting m_indiP_query - trigger adc or dac query + INDI_NEWCALLBACK_DEFN(fsmCtrl, m_indiP_query) + (const pcf::IndiProperty &ipRecv) + { + INDI_VALIDATE_CALLBACK_PROPS(m_indiP_query, ipRecv); + + if (ipRecv.find("query")) + { + std::string query = ipRecv["query"].get(); + if (query == "adc") + { + log("INDI query ADCs."); + queryAdcs(); + updateIfChanged(m_indiP_query, "query", "adc"); + } + else if (query == "dac") + { + log("INDI query ADCs."); + queryDacs(); + updateIfChanged(m_indiP_query, "query", "dac"); + } + else + { + log("INDI query of unknown."); + updateIfChanged(m_indiP_query, "query", "none"); + } + } + } + + ///////// + // UTILS + ///////// + + void fsmCtrl::updateINDICurrentParams() + { + float val1, val2, val3; + + if (m_inputType == DACS) + { + val1 = m_dac1; + val2 = m_dac2; + val3 = m_dac3; + } + else if (m_inputType == VOLTAGES) + { + val1 = get_v1(m_dac1, m_v); + val2 = get_v2(m_dac2, m_v); + val3 = get_v3(m_dac3, m_v); + } + else if (m_inputType == ANGLES) + { + val1 = get_alpha(m_dac1, m_dac2, m_dac3, m_a); + val2 = get_beta(m_dac2, m_dac3, m_b); + val3 = get_z(m_dac1, m_dac2, m_dac3); + } + + updateIfChanged(m_indiP_val1, "current", val1); + updateIfChanged(m_indiP_val2, "current", val2); + updateIfChanged(m_indiP_val3, "current", val3); + } + + } // namespace app +} // namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/fsmCtrl_on_PinkyVM.md b/apps/fsmCtrl/fsmCtrl_on_PinkyVM.md new file mode 100644 index 000000000..8eb8df0dc --- /dev/null +++ b/apps/fsmCtrl/fsmCtrl_on_PinkyVM.md @@ -0,0 +1,128 @@ +### Steps to run fsmCtrl app on Pinky + +#### Setup +1. In ~5 terminals: + - ssh into Pinky as xsup (need to first have added you public key in /home/xsup/.ssh/authorized_keys) + ``` + ssh xsup@10.130.133.96 + ``` + + - Connect to brave-tattler VM. xsup has been authenticated on the VM, so it should not reprompt for that, but if it does, the password is the typical xsup password. + ``` + multipass shell brave-tattler + ``` + +3. In one terminal, start the trippLitePDU simulator: +``` +cd /opt/MagAOX/source/MagAOX/apps/trippLitePDU +./trippLitePDU -n pdu_sim +``` + +4. In one terminal, start the xindiserver: +``` +cd /opt/MagAOX/source/MagAOX/apps/xindiserver +./xindiserver --local.drivers=pdu_sim,fsmCtrl +``` + +5. In one terminal, open the fsmCtrl logs (could also open the telemetry in a different terminal): +``` +cd /opt/MagAOX/logs +logdump -f fsmCtrl +``` + +6. In one terminal, start the fsmCtrl app: +``` +cd /opt/MagAOX/source/MagAOX/apps/fsmCtrl +ssh -L 1337:localhost:1337 -N -f fsm +./fsmCtrl +``` + +7. In one last terminal, you can see the values for the INDI parameters with: +``` +getINDI fsmCtrl +``` + +8. When the app starts, it will have the virtual power off, so need to turn that on: +``` +setINDI -x pdu_sim.fsmCtrl.state=On +``` + + +#### Drive fsmCtrl +- Editable parameters: + - `fsmCtrl.input.toggle` + + Default value: `indi` (set in fsmCtrl.config) + + Possible values: `indi` / `shmim` + - `fsmCtrl.input.type` + + Default value: `dacs` (set in fsmCtrl.config) + + Possible values: `dacs` / `voltages` / `angles` + - `fsmCtrl.val_1.target`, `fsmCtrl.val_2.target`, `fsmCtrl.val_3.target` + + Comments: They only respond to new values if `fsmCtrl.input.toggle=indi`. They take values as indicated by `fsmCtrl.input.type` and within the range of the respective `fsmCtrl.dac_x.min` and `fsmCtrl.dac_x.max`. + - `fsmCtrl.dac_1.min`, `fsmCtrl.dac_1.max`, `fsmCtrl.dac_2.min`, `fsmCtrl.dac_2.max`, `fsmCtrl.dac_3.min`, `fsmCtrl.dac_3.max` + + Comments: Range of accepted dac values for each actuator. It only takes dac values. + - `fsmCtrl.conversion_factors.a`, `fsmCtrl.conversion_factors.b` + + Default value: a=0.0104; b=0.012 + + Comments: Conversion factors for angles to actuator displacements (as used in [fsm_conversion_factors.ipynb](https://github.com/stefi07/MagAOX/blob/fsm/apps/fsmCtrl/fsm_conversion_factors.ipynb)). Angle conversion not currently finished. + + - `fsmCtrl.conversion_factors.v` + + Default value: v=1.46484375e-05 (from (4.096 / (2.0**24)) * 60) + + Comments: Conversion factor for dacs to voltages & back (as used at bottom of [FSMComm](https://gitlab.sc.ascendingnode.tech/pearl-inst-design/electronics/software/-/tree/master/AOApps/FineSteeringMirrorController/FSMComm.py)). + +- Command via INDI params: + To send voltages, for example: + - Change input type to `voltages`: + ``` + setINDI -n "fsmCtrl.input.type=voltages" + ``` + + - To send a voltage to actuator 3, for example: + ``` + setINDI -n "fsmCtrl.val_3.target=80" + ``` + + - Check values (might need to give it a second after sending the voltage): + ``` + getINDI fsmCtrl + ``` + +- Command via shmim: + The app is looking for a 1x3 shmim called `fsm` in the folder `/milk/shm/`. The shmim should also have a keyword with name `inputType` and value either `dacs`, `voltages` or `angles`. + + - To make new shmim in `milk`: + ``` + $ milk + milk > mk2Dim "s>fsm" 1 3 + milk > imkwaddS fsm inputType voltages comment + ``` + (The first command makes a shmim called `fsm` of size 1x3 and saves it. The second command adds a keyword `inputType` with value `voltages` and comment `comment` to shmim `fsm`. The keyword requires an input for the `comment` field, but the fsmCtrl app only uses the value field.) + + Can also delete shmim with: + ``` + > rmshmim fsm + ``` + + - To edit shmim values in ipython: + ``` + $ ipython + import numpy as np + from magpyx.utils import ImageStream + dm = ImageStream('fsm') # Looks for shmim `fsm` in /milk/shm/ + new = np.array([[30, 40, 80]]) # Make np array with new voltage values + dm.write(new.transpose()) # Write new values to shmim + ``` + + - Other useful ipython commands: + ``` + dm.grab_latest() # Check current shmim values + dm.get_kws() # Check shmim's keywords + ``` \ No newline at end of file diff --git a/apps/fsmCtrl/fsm_conversion_factors.ipynb b/apps/fsmCtrl/fsm_conversion_factors.ipynb new file mode 100644 index 000000000..1918c8306 --- /dev/null +++ b/apps/fsmCtrl/fsm_conversion_factors.ipynb @@ -0,0 +1,378 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "684f51fc", + "metadata": {}, + "source": [ + "Convert between two angles and a displacement $(\\alpha, \\beta, Z)$ and the linear displacements of each actuator $(A, B, C)$\n", + "\n", + "\\begin{equation}\n", + " (\\alpha, \\beta, Z) \\longleftrightarrow (A, B, C)\n", + "\\end{equation}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "914937fd", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "plt.rcParams.update({\n", + " 'figure.dpi' : 100,\n", + " 'image.origin' : 'lower',\n", + " 'image.interpolation' : 'nearest'\n", + "})\n", + "\n", + "import numpy as np\n", + "from itertools import product" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91cc4475", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "85a25b0c", + "metadata": {}, + "outputs": [], + "source": [ + "# (A, B, C) ---> (alpha, beta, Z)\n", + "\n", + "def get_alpha(A,B,C,a):\n", + " return 1./a * (A - 0.5 * (B + C))\n", + "\n", + "def get_beta(B,C,b):\n", + " return 1./b * (B - C)\n", + "\n", + "def get_Z(A,B,C):\n", + " return 1./3. * (A + B + C)\n", + "\n", + "\n", + "# (alpha, beta, Z) ---> (A, B, C)\n", + "\n", + "def get_A(alpha, Z, a):\n", + " return Z + 2./3. * a * alpha\n", + "\n", + "def get_B(alpha, beta, Z, a, b):\n", + " return 0.5 * b * beta + Z - 1./3. * a * alpha\n", + "\n", + "def get_C(alpha, beta, Z, a, b):\n", + " return Z - 1./3. * a * alpha - 1./2. * b * beta\n", + "\n", + "# vectors\n", + "\n", + "def ABC_to_alphabetaZ(A,B,C,a,b):\n", + " return get_alpha(A,B,C,a), get_beta(B,C,b), get_Z(A,B,C)\n", + "\n", + "def alphabetaZ_to_ABC(alpha,beta,Z,a,b):\n", + " return get_A(alpha,Z,a), get_B(alpha,beta,Z,a,b), get_C(alpha,beta,Z,a,b)\n", + "\n", + "# constraints\n", + "\n", + "def get_alphamin(Amin, Bmax, Cmax, a):\n", + " return 1./a * (Amin - 0.5*(Bmax + Cmax))\n", + " \n", + "def get_alphamax(Amax, Bmin, Cmin, a):\n", + " return 1./a * (Amax - 0.5*(Bmin + Cmin))\n", + "\n", + "def get_betamin(Bmin, Cmax, b):\n", + " return 1./b * (Bmin - Cmax)\n", + "\n", + "def get_betamax(Bmax, Cmin, b):\n", + " return 1./b * (Bmax - Cmin)\n", + "\n", + "# command to voltages (DAC?)\n", + "\n", + "def command_to_DAC(alpha, beta, Z, fsmprops):\n", + " '''\n", + " Given two angles and a mean dispacement, convert to DAC values\n", + " '''\n", + " \n", + " # first, check that requested values are valid\n", + " # (should this happen here? or allow invalid values to pass and clip the DAC values?)\n", + " alpha_cl = np.clip(alpha, fsmprops['alphamin'], fsmprops['alphamax'])\n", + " beta_cl = np.clip(beta, fsmprops['betamin'], fsmprops['betamax'])\n", + " Z_cl = np.clip(Z, fsmprops['Zmin'], fsmprops['Zmax'])\n", + " #print(alpha_cl, beta_cl, Z_cl)\n", + " \n", + " # get linear actuator displacement (A,B,C)\n", + " A = get_A(alpha_cl, Z_cl, fsmprops['a'])\n", + " B = get_B(alpha_cl, beta_cl, Z_cl, fsmprops['a'], fsmprops['b'])\n", + " C = get_C(alpha_cl, beta_cl, Z_cl, fsmprops['a'], fsmprops['b'])\n", + " \n", + " return A, B, C\n", + " \n", + " # convert (A,B,C) to DAC values ?\n", + " #Adac, Bdac, Cdac = ABC_to_ABCdac(A,B,C,fsmprops['disp2dac'])\n", + " \n", + "def ABC_to_ABCdac(A,B,C, disp2dac):\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "f0d090d6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(-0.001153846153846154, 0.001153846153846154, -0.001, 0.001)" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fsmprops['alphamin'], fsmprops['alphamax'], fsmprops['betamin'], fsmprops['betamax']" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "7feba0f1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-0.001153846153846154 0.001153846153846154 -0.001 0.001\n" + ] + } + ], + "source": [ + "# example properties\n", + "a = 10.4e-3 #mm\n", + "b = 12.0e-3 #mm\n", + "\n", + "Amin = Bmin = Cmin = 0e-6 #um\n", + "Amax = Bmax = Cmax = 12e-6 #um\n", + "Zmin = 0e-6 #um\n", + "Zmax = 12e-6 #um\n", + "\n", + "fsmprops = {\n", + " 'a' : a,\n", + " 'b' : b,\n", + " 'Amin' : Amin,\n", + " 'Bmin' : Bmin,\n", + " 'Cmin' : Cmin,\n", + " 'Amax' : Amax,\n", + " 'Bmax' : Bmax,\n", + " 'Cmax' : Cmax,\n", + " 'disp2dac' : 1.0,\n", + "}\n", + "\n", + "fsmprops.update({\n", + " 'alphamin' : get_alphamin(fsmprops['Amin'], fsmprops['Bmax'], fsmprops['Cmax'], fsmprops['a']),\n", + " 'betamin' : get_betamin(fsmprops['Bmin'], fsmprops['Cmax'], fsmprops['b']),\n", + " 'Zmin' : Zmin,\n", + " 'alphamax' : get_alphamax(fsmprops['Amax'], fsmprops['Bmin'], fsmprops['Cmin'], fsmprops['a']),\n", + " 'betamax' : get_betamax(fsmprops['Bmax'], fsmprops['Cmin'], fsmprops['b']),\n", + " 'Zmax' : Zmax\n", + "})\n", + "\n", + "print(fsmprops['alphamin'], fsmprops['alphamax'], fsmprops['betamin'], fsmprops['betamax'])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "547ffa68", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "Zvals = np.linspace(0, 12, num=101) * 1e-6\n", + "\n", + "allmask = []\n", + "for Z in Zvals:\n", + "\n", + " alphavals = np.linspace(fsmprops['alphamin'], fsmprops['alphamax'], num=100)\n", + " betavals = np.linspace(fsmprops['betamin'], fsmprops['betamax'], num=100)\n", + " #Z = 6e-6 #optimal?\n", + "\n", + " #alphavals = betavals = np.linspace(-2e-3, 2e-3, num=100)\n", + "\n", + " alphabeta = product(alphavals, betavals)\n", + "\n", + " allABC = []\n", + " for (alpha, beta) in alphabeta:\n", + " ABC = command_to_DAC(alpha, beta, Z, fsmprops)\n", + " allABC.append(ABC)\n", + "\n", + " allABC = np.asarray(allABC).reshape((100,100,3)).swapaxes(0,-1)\n", + "\n", + " Amask = (allABC[0] >= fsmprops['Amin']) & (allABC[0] <= fsmprops['Amax'])\n", + " Bmask = (allABC[1] >= fsmprops['Bmin']) & (allABC[1] <= fsmprops['Bmax'])\n", + " Cmask = (allABC[2] >= fsmprops['Cmin']) & (allABC[2] <= fsmprops['Cmax'])\n", + " mask = Amask & Bmask & Cmask\n", + " \n", + " allmask.append(mask)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "bf652629", + "metadata": {}, + "outputs": [], + "source": [ + "allmask = np.asarray(allmask)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "14d5ab23", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1.4e-05, 8.000000000000001e-06, -4e-06)\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgoAAAHLCAYAAACpnbDZAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAB6/ElEQVR4nO3dd3xT9f7H8VeSZnSmkw4oLXtvpFZFUBBQFHECogwRXKBcFIWfigJ6UfBy71W54gJx4lZcCDJcVPbee7alO51JmpzfH4VoaQvdJ0k/z8fjPEpOvufkk7Scvvs93/M9GkVRFIQQQgghyqFVuwAhhBBCuC8JCkIIIYSokAQFIYQQQlRIgoIQQgghKiRBQQghhBAVkqAghBBCiApJUBBCCCFEhSQoCCGEEKJCEhSEEEIIUSEJCkIIIYSokAQFIYQQQlRIgoIQHuDdd99Fo9Fw7NgxAJ577jk0Gg3p6elV3lYIIapCgoIQQlTBunXreO6558jOzq5Ue41Gc8nlueeeq3FdBw8eZPjw4TRp0gQ/Pz/atm3LrFmzKCgoqNT2VquVJ598kpiYGHx9fUlISGDlypU1rkt4Ph+1CxBCXNo999zD8OHDMRqNapfS4K1bt46ZM2cyZswYgoODL9n+/fffr/C55557jsOHD5OQkFCjmk6ePEmvXr0wm81MnDiR0NBQkpKSePbZZ9m8eTPffPPNJfcxZswYPv/8cyZPnkyrVq149913ueGGG1izZg1XXXVVjeoTnk2CghAeQKfTodPp1C5DVMPdd99d7vq3336bw4cPM2nSJK6//voavcb7779PdnY2v//+Ox06dABgwoQJOJ1O3nvvPbKysggJCalw+w0bNrB06VLmzZvH448/DsCoUaPo2LEjTzzxBOvWratRfcKzyakH4RWOHz/OQw89RJs2bfD19SUsLIw77rij3PPyp0+fZty4ccTExGA0GmnWrBkPPvggNputym3uvfdeIiMjMRqNdOjQgUWLFpV6rdzcXCZPnkx8fDxGo5FGjRpx3XXXsWXLliq1qWicQXp6OnfeeSdBQUGEhYXx6KOPUlRUdMnPqzK1V+RS9Z4fP7Fv375L1lYX37fqvq/K1PLcc88xdepUAJo1a+Y6dVDV8R+7d+/mkUceoVu3bsybN69K25bHYrEAEBkZWWp9dHQ0Wq0Wg8Fw0e0///xzdDodEyZMcK0zmUyMGzeOpKQkTp486Vo/ZswY4uPjy+zj/Pf9wscHDhzg7rvvxmw2ExERwTPPPIOiKJw8eZKbb76ZoKAgoqKi+Ne//lWdty7qgfQoCK+wceNG1q1b5zpHe+zYMV5//XX69u3Lnj178PPzA+DMmTP06tWL7OxsJkyYQNu2bTl9+jSff/45BQUFGAyGSrVJTU3l8ssvR6PRMHHiRCIiIvjxxx8ZN24cFouFyZMnA/DAAw/w+eefM3HiRNq3b09GRga///47e/fupXv37pVuU5E777yT+Ph45syZw59//skrr7xCVlYW7733XoXbVLb2ilS23srUVpvft5q+r8rUcuutt3LgwAE+/vhj/v3vfxMeHg5ARETERff9dwUFBdx5553odDqWLl1a6nSS3W4nJyenUvsJDQ1Fqy35W69v37689NJLjBs3jpkzZxIWFsa6det4/fXXeeSRR/D397/ovrZu3Urr1q0JCgoqtb5Xr14AbNu2jdjY2Eq/x78bNmwY7dq148UXX+T777/n+eefJzQ0lDfeeINrr72Wl156iQ8//JDHH3+cyy67jKuvvrparyPqkCKEFygoKCizLikpSQGU9957z7Vu1KhRilarVTZu3FimvdPprHSbcePGKdHR0Up6enqp54cPH66YzWZXPWazWXn44YcvWntl2ixevFgBlKNHjyqKoijPPvusAihDhgwp1e6hhx5SAGX79u0VblvZ2qtbb1Vqq83vW03fV2VrmTdvXqnPs6ruvfdeBVCWLFlS5rk1a9YoQKWWC19/9uzZiq+vb6k2Tz31VKVq6tChg3LttdeWWb97924FUBYuXOhaN3r0aCUuLq5M2/Pf9wsfT5gwwbWuuLhYadKkiaLRaJQXX3zRtT4rK0vx9fVVRo8eXal6Rf2SUw/CK/j6+rr+bbfbycjIoGXLlgQHB7u6xJ1OJ19//TU33XQTPXv2LLMPjUZTqTaKovDFF19w0003oSgK6enprmXgwIHk5OS4XjM4OJj169dz5syZCmuvTJuKPPzww6UeT5o0CYAffvih3PZVqb2m9Vamttr6vtXG+6pMLTX10UcfsWjRIu655x5GjRpV5vkuXbqwcuXKSi1RUVGlto2Pj+fqq6/mzTff5IsvvuDee+/ln//8J6+99tol6yosLCx3oKzJZHI9X1333Xef6986nY6ePXuiKArjxo1zrQ8ODqZNmzYcOXKk2q8j6o6cehBeobCwkDlz5rB48WJOnz6Noiiu58535aalpWGxWOjYsWOF+6lsm+zsbN58803efPPNctucPXsWgLlz5zJ69GhiY2Pp0aMHN9xwA6NGjaJ58+autpVpU5FWrVqVetyiRQu0Wm2F58yrUntFKltvZWqrze9bTd9XZWqpiYMHD/LAAw/QunVr/ve//5XbJiQkhP79+1d530uXLmXChAkcOHCAJk2aAHDrrbfidDp58sknGTFiBGFhYRVu7+vri9VqLbP+/JiSv4eoqmratGmpx2azGZPJ5Dpt8/f1GRkZ1X4dUXckKAivMGnSJBYvXszkyZNJTEzEbDaj0WgYPnw4TqezVl/r/P7uvvtuRo8eXW6bzp07AyXn6Xv37s1XX33FihUrmDdvHi+99BJffvmla6R7ZdpU1t8Hk9W09opUt97yaqut71ttvK+6/BmyWq0MGzYMm83G0qVLCQgIKLedzWYjMzOzUvuMiIhwXQnzv//9j27durlCwnlDhgzh3XffZevWrRcNINHR0Zw+fbrM+uTkZABiYmIuWc/fg9XflXe1TkVX8FS0D6EuCQrCK3z++eeMHj261MjpoqKiUpPiREREEBQUxK5duyrcT2XbBAYG4nA4KvXXX3R0NA899BAPPfQQZ8+epXv37rzwwgulfqlWpk15Dh48SLNmzVyPDx06hNPpLHdUenVqr8l7qkxttfl9q+n7qkwtcOkwVp7HH3+crVu38t///pdu3bpV2G7dunVcc801ldrn0aNHXZ9lampquZc/2u12AIqLiy+6r65du7JmzRosFkupAY3r1693Pf93ubm5ZfaRmppaqbqF55ExCsIr6HS6Mn+NvPrqqzgcDtdjrVbL0KFD+fbbb9m0aVOZfSiKUqk2Op2O2267jS+++KLcX15paWkAOByOMl3WjRo1IiYmxtXNW5k2F7NgwYIy7xmoMGBUtvaKVKXeytRWW9+3mr6vytYCuK4gqOzMjF999RWvvfYaQ4YM4ZFHHrlo2+qOUWjdujVbt27lwIEDpfb38ccfo9VqS/WmFBQUsG/fvlLTf99+++04HI5Sp22sViuLFy8mISGhzBUPmZmZbN++vVTb82NPpFfA+0iPgvAKN954I++//z5ms5n27duTlJTEzz//XOa87D//+U9WrFhBnz59mDBhAu3atSM5OZnPPvuM33//neDg4Eq1efHFF1mzZg0JCQmMHz+e9u3bk5mZyZYtW/j555/JzMwkNzeXJk2acPvtt9OlSxcCAgL4+eef2bhxo+uv1sq0uZijR48yZMgQBg0aRFJSEh988AF33XUXXbp0qXCbytRekarUW5naavP7VpP3VZVaevToAcBTTz3F8OHD0ev13HTTTeVegpicnMy4cePQ6XT069ePDz74oNzXbtGiBYmJidUeozB16lR+/PFHevfuzcSJEwkLC+O7777jxx9/5L777it16mDDhg1cc801PPvss66poxMSErjjjjuYPn06Z8+epWXLlixZsoRjx47xzjvvlHk9o9HITTfdxMSJEzGZTHzwwQeuuRxmz57Ngw8+WOX3INxYPV9lIUSdyMrKUsaOHauEh4crAQEBysCBA5V9+/YpcXFxZS65On78uDJq1CglIiJCMRqNSvPmzZWHH35YsVqtVWqTmpqqPPzww0psbKyi1+uVqKgopV+/fsqbb76pKIqiWK1WZerUqUqXLl2UwMBAxd/fX+nSpYvyv//9z7WPyrRRlIovj9yzZ49y++23K4GBgUpISIgyceJEpbCw8KLbVqb2ilSm3qrUVtvft+q+r6rWMnv2bKVx48aKVqu96KWSlb3csTYuC1y/fr1y/fXXK1FRUYper1dat26tvPDCC4rdbi+3pmeffbbU+sLCQuXxxx9XoqKiFKPRqFx22WXK8uXLy7zO+csjX3/9dSU6Olrx9fVVbr/9dmXHjh1KbGysEhcXp6SkpLh+DtLS0sps7+/vX2a/ffr0UTp06FDjz0HUPo2iSD+REKL2PPfcc8ycOZO0tLQyI9uF5xszZgxr166Vu5E2IDJGQQghhBAVkqAghBBCiApJUBBCCCFEhTw6KPz666/cdNNNxMTEoNFo+Prrry+5zdq1a+nevTtGo5GWLVvy7rvvlmmzYMEC4uPjMZlMJCQksGHDhtovXggv9dxzz6EoioxP8FLvvvuujE9oYDw6KOTn59OlS5cy12tX5OjRowwePJhrrrmGbdu2MXnyZO677z5++uknV5tPPvmEKVOm8Oyzz7Jlyxa6dOnCwIEDLzn9qxBCCOGNvOaqB41Gw1dffcXQoUMrbPPkk0/y/fffl5qQZfjw4WRnZ7N8+XKg5Hriyy67zHUjFafTSWxsLJMmTWLatGl1+h6EEEIId9OgJlxKSkoqM5nJwIEDXfept9lsbN68menTp7ue12q19O/fn6SkpAr3a7VaS81K53Q6yczMJCwsrFrTvQohhBB1SVEUcnNziYmJQau9+MmFBhUUUlJSiIyMLLUuMjISi8VCYWEhWVlZOByOctvs27evwv3OmTOHmTNn1knNQgghRF05efJkmZuJXcijxyi4i+nTp5OTk+NaTpw4oXZJQgghxCUFBgZesk2D6lGIiooqc4ez1NRUgoKC8PX1RafTodPpym3z9xuwXMhoNGI0GuukZiGEEKKuVOb0eIPqUUhMTGTVqlWl1q1cuZLExEQADAYDPXr0KNXG6XSyatUqVxshhBCiQVHzRhM1lZubq2zdulXZunWrAijz589Xtm7dqhw/flxRFEWZNm2acs8997jaHzlyRPHz81OmTp2q7N27V1mwYIGi0+lK3fhk6dKlitFoVN59911lz549yoQJE5Tg4GAlJSWl0nXl5ORU6kYwssgiiyzesFxI7XpkqfySk5Nzyd9pHh0UKroz2/k7sY0ePVrp06dPmW26du2qGAwGpXnz5srixYvL7PfVV19VmjZtqhgMBqVXr17Kn3/+WaW6JCjIIossDWm5kNr1yFL5pTJBwWvmUXAnFosFs9msdhlCiAbGz8+P8PDwer8s+8Krwtq2bVuvry9KUxSF9PR0CgoKLtk2JyeHoKCgi7aRoFAHJCgIIeqTRqNh7NixDBkyBIPBUKtBIS4ursrbHD9+vNZeX1SdoijYbDaWLVvG4sWLudiv+coEhQZ11YMQQnijsWPHMmLECIKDg2t9382aNavyNvn5+bVeh6i6ESNGALBo0aIa7UeCghBCeDB/f3+GDBlSJyEBwGQy1cl+Rd0LDg5myJAhLF26tFKnISrSoC6PFEIIbxMWFobBYFC7DOGmDAZDje/kKj0KQgjhwTQaTa2OSejZs2et7UuorzZ+PqRHQQghhBAVkqAghBBCeKD777+ff/3rX3X+OnLqQQghhMc6ePAgc+fOZc+ePQQHBzNs2DBGjRp10W12797Na6+9xr59+9BoNHTo0IFJkybRunVrAM6cOcPNN99cZrtFixbRqVOnOnkf7kyCghBCNCDuPAbBbrej1+sr3T4vL4+JEyfSq1cvpk2bxuHDh5k1axYBAQHceuut5W5TUFDAo48+Su/evXnyySdxOBy8+eabTJo0ie+//x4fn79+LS5YsIDmzZu7HtfFlSXFxcWlXtMduXd1QgghvNb9999PixYt0Ol0/Pjjj7Rs2ZKFCxdWevvly5dTXFzMjBkz0Ov1tGjRgv379/PRRx9VGBSOHTtGTk4O999/v+uuwOPHj2fEiBEkJycTGxvrams2myu8YuC5554jLy+PDh06sHTpUmw2G3fddRdjx45lwYIFLFu2DJPJxP3338+QIUOAv3oqXnjhBT7//HN2797NtGnT6N27N/PmzWPr1q1YLBaaNGnC2LFjGThwoOv1CgsLefHFF1mzZg1+fn7cfffdlf6cakqCghBCeBlFUbA6yn+uwFZcp6/tq9dVqf3333/Pbbfdxttvvw3AI488wrZt2ypsHxUVxaeffgrAzp076datW6leiMTERN577z0sFku5Mw7GxcVhNptZtmwZY8eOxeFw8M0339CsWTOio6NLtX3sscew2Ww0bdqUe+65hz59+pR6ftOmTTRq1Ig33niDHTt2MHv2bHbs2EH37t1ZvHgxK1euZM6cOSQkJBAZGenabsGCBTz66KO0adMGo9GIzWajbdu2jBo1Cn9/f/744w+effZZmjRpQocOHQD473//y5YtW3j55ZcJDQ1lwYIF7N+/33W6pC5JUBBCCC9jdcDdX58t/8mvf6rT194za+ClG/1NbGwsjzzyiOvx008/jdVqrbD937vpMzIyiImJKfV8aGio67nygoK/vz8LFy5k6tSpvPPOO64aXn31Vde+/fz8mDx5Ml26dEGj0bB69WqmTp3KvHnzSoWFoKAgHn/8cbRaLfHx8bz33ntYrVbGjh0LwJgxY1iyZAnbt29nwIABru2GDx/OtddeW6que+65x/XvYcOG8eeff7Jy5Uo6dOhAQUEBy5YtY9asWfTq1Qso6dEYPHhwhZ9TbZKgIIQQQjUX3kCqUaNGdfp6RUVFPP/883Tp0oXnn38ep9PJBx98wOTJk1myZAkmk4ng4GBGjhzp2qZDhw6kp6fzwQcflAoKzZs3R6v96+LB0NBQWrRo4Xqs0+kwm81kZmaWqqF9+/alHjscDhYvXszPP/9MWloadrsdm83mmhXz1KlT2O12Onbs6NrGbDZX6z4c1SFBQQghvIxRBx8MbUT37t3q/bWreurB19e31OOqnHoICwsr80v4/OOwsLByt//pp59ITk5m0aJFrl/yzz//PNdeey2//vprqb/8/65Dhw6sX7++1LoLByFqNJpy1zmdzlLrLpwW+/3332fp0qVMmTKFli1b4uvry/z587Hb7eXWUt8kKAghhJfRaDSYfMDP4HmH+KqceujUqROvv/56qSsH1q9fT1xcXIV3RCwqKiozW+H5xxf+Qv+7AwcO1Hgq5Ips376dPn36cMMNNwDgdDo5ceKE64ZcTZo0wcfHh127drkGYFosFk6cOEH37t3rpKa/87yfIiGEEEDJoMWioiKOHj1Ks2bNvOIGTlU59TBo0CDeeustZs+ezahRozh8+DBLly7lH//4h6vNmjVrWLBgAZ9//jkACQkJvPLKK7z00ksMGzYMp9PJkiVL0Ol0rktHv/vuO/R6PW3atHHt49tvv+Wpp56qxXf6l6ZNm7Jq1Sq2b99OUFAQH374IRkZGa6g4Ofnx80338wrr7yC2WwmJCSE119/vdRpj7okQUEIIYRHCggI4LXXXmPu3LmMGjWK4OBg7rvvvlKXRubl5XH8+HHX4/j4eObPn89bb73Fvffei1arpXXr1rzyyiulegzeeecdkpOT0el0xMfH889//pN+/frVyfu49957OX36NI888ggmk4mhQ4fSt29f8vLyXG0eeeQRCgoKmDJliuvyyL8/X5c0iqIo9fJKDYjFYsFsNqtdhhDCy7lrj8KmTZvULkGck56ezgMPPFAqLP1dTk5OhadpzpN7PQghhBCiQnLqQQjhRjRo9EY0BhNavS8aHwMoThSnA5wOFKcTFEfJY0cxiuIsWe8oBqXigWjeQjqAhRokKAgh6ozG4ItPYDi6oAh8AsPQBUagCwxDFxCK1uCH1mBCo/c9FwxMaAwmNJrqdXQ6ivJwFlhwFlpwFFpwFuSc+2rBUZhT8rUgm+KsZJxFubX8ToXwXhIUhBA1ovUzY4iIRx8Rhz6sKT5BEegCw/EJCkdr9K/WPhXFiWIrQim2gkYLWh0ajRaNzqfk39qy1+rrTAHoTAFATNkdXsBRmEtx1hnsWWdKvmaWfC3OOoPTml+tmoXwVhIUhBCVovExog+PRR8Rfy4YxGOIiEPnH3LR7RxFeTgsaThyMyjOLfnqyMvAaS3AaStEsRXitBWh2AvPPT4XEC5ZkNYVGjQ+BrS+Qeh8g9D6mdH5BZU89jP/bX0QuoCwkp4N30B0vm0wxrQpW29BDvaMU9hSDmJNPoAt+QDF2SnV/djqjZyWEOVRFKXGPxsSFIQQ5dIFRWCK7YgxtiOmJu3xCW1c7mkBRXFSnJWCPe0YtvTjFOek4shNp9iSjiM3HcVeVDcFKk5wOFEcdhR7Ec5CC5W53ZFGb8QnOBp9SAw+ITH4hPzt34Fh6PzM6PzMmGI7uLZxFFqwJR/EmnIQ25kDWFMO4MzPrt23U82D+fkbIhUUFJSZ5VAIm81Genp6jfYhQUEIAYBPaOOSYNCkA6bYjviYy05848jPxpZ2HHv6MWxnj2FPO4Y94wSKvRI9AG5CsVtL6k47VuY5jd6ET0g0hoh4DFGtMMa0xtCoOTrfIHyb98C3eQ9X22LLWayn9lJ4dAtFx7biyMsss7/6oNPpCA4O5uzZkptA+fn5lZp1UDRc2dnZLFu2jIKCghrtR+ZRqAMyj4LwBLqAMHxb9sIU1wVTbIcypxAURzG2lEMUndyF9dRurMkHcRZkq1OsmrQ+GCLiMES3xhjdGkN0a/ThsWV6V2xpx0pCw9EtFJ3cDY6qzdNfk0OxoiikpKSQnZ1d7X3Upoqu2Rf1Q1EUbDYby5YtY/HixRf92arMPAoSFOqABAXhrvRhsfi2uhy/VokYY0rfx14ptmE9s78kGJzchfXMPo/qKahPGoMvhsiWmOK74BvfHUN0y1LBwWm3Yj25yxUc7Bkny+yjLg69DofDLW4kdOEdIUX9UhSF9PT0SvUkSFBQiQQF4TY0WowxbVzhQB9a+ooA6+l9FB7eSNGJnVhTDoCjMmf5xYW0pkBM8V3xbdYdU7Pu+ASWvnOhPeMk+Xt/JX/PLxRnnQG8e/ChnPrwHBIUVCJBQahLgzG2I/4d+uLXMgGdf7DrGaXYTuHxbRQe/JPCQxtw5GepV6YX04fHYWrWrSQ4xHYsmTjqHGvKIQr2/sqenz6gcbB3Dj6UoOA5JCioRIKCUIMuMIKATv3w79QffXCUa72jKI/CwxtLwsHRLSi2QhWrbHg0Bl/8WiXi3+5qTM26lZoDomdcCEO6xnB9x2giAo0qVlm7JCh4DgkKKpGgIOqNTo9fq8sJ6HwdpviurvPkTms++Xt/pWDf7xSd3AVOh8qFNiwVHVYz8238sDOZb7efYcOxTM4302rg6tYRjEqMo0/rRui0nv2LVoKC55CgoBIJCqKuGSJb4N/pOvzb90HnG+haX3R8O3k7VlJwIKlykxaJOlGZw2pyTiHf7ygJDdtP5bjWx4b6cndCHHf2jCXE33CRPbgvCQqeQ4KCSiQoiDqh0eLX5gqCLru11BULxZaz5O1cRf7OnynOSVWxQHFeVQ+rR9Ly+Gj9CT7ddBJLUcmAUqOPliFdYhiVGE+nJp51PJGg4DkkKKhEgoKoTRq9kYBO1xF42VDX2AOl2EbBgSTydq6k6PiOBnHnRE9S3cNqoc3Bsu2nWbLuOHuSLa71XWODGZUYx+DO0Rh9yt7nwt1IUPAcEhRUIkFB1Aatn5nA7jcS2H0wOt+S/8iOghxyt3xP7pbvcBZaLrEHUV9q+zCqKApbTmTxXtJxftiZjN1Rsv/wAAMTrm7O3ZfH4Wdw34l1JSh4jsoEherdz9WNLFiwgPj4eEwmEwkJCWzYsKHCtn379kWj0ZRZBg8e7GozZsyYMs8PGjSoPt6KEAD4hMQQOuBhGj+wiOArR6DzDcKelUzGiv9x+vV7yfnjIwkJXk6j0dAjLpT/Du/Gumn9eHxAa6LNJtLzbPzzh330fmkNC385TL5V5r0Qdc+jexQ++eQTRo0axcKFC0lISOA///kPn332Gfv376dRo7Lz1GdmZmKz2VyPMzIy6NKlC2+//TZjxowBSoJCamoqixcvdrUzGo2EhFz8Dnl/Jz0Kojp8QmIIvvoe/Npc6bp6wXpmP5YNX1JwIElOL7ix+jiM2h1Ovtp6mgVrDnE8o2TGvRA/Pff1bs7oK+IJMLpPD4P0KHgOrz/1kJCQwGWXXcZrr70GgNPpJDY2lkmTJjFt2rRLbv+f//yHGTNmkJycjL+/P1ASFLKzs/n666+rXZcEBVEVuoBQzFeOIKDzANc19gWHNmBZ/wXWU7tVrk642yGy2OHk621neG31QY6dCwzBfnruu6oZo6+IJ9CkV7lCCQqexKuDgs1mw8/Pj88//5yhQ4e61o8ePZrs7Gy++eabS+6jU6dOJCYm8uabb7rWjRkzhq+//hqDwUBISAjXXnstzz//PGFhYRXux2q1YrX+dSmaxWIhNja2em9MNBgaoz/mhNsI7DkErd4ElASE7F+WYE+Xm+q4C3c9RBY7nCzbfobXVh/iSHo+AGZfPeN7N2PcVc3xNag36FGCgueoTFBwn76qKkpPT8fhcBAZGVlqfWRkJPv27bvk9hs2bGDXrl288847pdYPGjSIW2+9lWbNmnH48GH+7//+j+uvv56kpCR0uvL/482ZM4eZM2dW/82IhkWnJ6jHjQRdfqdrDoSiU3vIXvsu1tN7VC5OeAofnZZbuzfh5q6N+Xb7GV5ZfZAjafm8vOIAH284yfQb2jK4U7T80hY15rE9CmfOnKFx48asW7eOxMRE1/onnniCX375hfXr1190+/vvv5+kpCR27Nhx0XZHjhyhRYsW/Pzzz/Tr16/cNtKjICpFo8W/Yz+Cr7oLn6AIAGzpx8n+ZQmFhyoehCvU5SmHSIdT4dvtZ5i7fB9ncooA6BUfyoyb2tOxcf2eCpVw4jm8ukchPDwcnU5HamrpCWZSU1OJioqqYKsS+fn5LF26lFmzZl3ydZo3b054eDiHDh2qMCgYjUaMRu+Zp13UPkN0a0IHPIQxqiVQMklS9m8fkr97jQxSdDOeEgwupNNqGNqtMQM7RPHGr4dZ+MthNhzL5KbXfmdYz1geH9iG8AA5Tomq89jLIw0GAz169GDVqlWudU6nk1WrVpXqYSjPZ599htVq5e67777k65w6dYqMjAyio6NrXLNoeDQGP0L6P0DUPS9jjGqJoyiPzNXvcPrN+8nftUpCgqh1vgYdk/u3ZvVjfRnSJQZFgaUbT3LNvLW89esRbMXyMyeqxmNPPUDJ5ZGjR4/mjTfeoFevXvznP//h008/Zd++fURGRjJq1CgaN27MnDlzSm3Xu3dvGjduzNKlS0utz8vLY+bMmdx2221ERUVx+PBhnnjiCXJzc9m5c2elew3kqgcB4Nf2KkKuHY9PYMlA2Lxdq8la8w7OgpxLbCnU5MGHxHJtOpbJzG/3sPN0yc9ds3B/Zg7pwNWtI+rsNeXUg+fw6lMPAMOGDSMtLY0ZM2aQkpJC165dWb58uWuA44kTJ9BqS3ea7N+/n99//50VK1aU2Z9Op2PHjh0sWbKE7OxsYmJiGDBgALNnz5ZTC6LSfMyRhA54EN/mPQGwZ5wic8X/KDpx8fEwQtSFnvGhfPPwlXy+5RRzl+/naHo+oxZtYPhlsfzf4HYEucHllMK9eXSPgruSHoUGSutDUK9bMF8xHK3eiFJsJ+fPT8n583Nw2NWuTpzTkA95uUV2/rXiAEuSjqEoEG028eJtnelTy70L0qPgObx6HgV3JkGh4dE3ak74TY9hCI8DoPDYdjJXLKA464zKlYkLySEP1h/J4IkvdrhmeBzWM5anbqy93gUJCp5DgoJKJCg0JBqCeg0l+OpRaHR6HPnZZK1+m/w9a9UuTFRADnklCm0O5v20n8Xrjrp6F+bc2om+bcpOf19VEhQ8hwQFlUhQaBh0AWGEDf4HvvFdASg4kETG8lflhk1uTg55pW08lsnUz7a7poO+s2cTnhrcHrNv9XsXJCh4DgkKKpGg4P18WyUSdv0kdL5BOO1FZK16i7ztP6ldlriAHN4q58LehRiziddGdqd708rfDO/vJCh4DgkKKpGg4L00eiMh144nsGvJrcetKYdI//ZlijNPqVyZKI8c3qpm07FMHj/Xu+Cj1TD9hnbce2V8lX/xS1DwHBIUVCJBwTsZIlsQftNU9GFNUBQnlvVfkv3bB+AsVrs0UQE5vFVdbpGdaV/u5PsdyQAM6hDF3Ds6V2mgowQFzyFBQSUSFLxPQJdBhF53PxqdnuLcdDK+my/zIrgBOXzVDUVReP/P48z+bg92h0JcmB//G9mdDjGVO65JUPAcEhRUIkHBi2h9CO0/gcBuNwBQsH8dGctfwVmUp3JhAiQo1LVtJ7N5+MMtnM4uxOCjZeaQDgy/LPaSQUCCgueQoKASCQreQetnJmLodEyxHVEUJ9m/vo/lz8/ULkv8jRy+6l52gY3HPt3Oqn1nAbi1W2Oev6UjfoaKJ/aVoOA5JCioRIKC59M3ak6j257GJ6gRTms+6cvmUXhkk9pliQvI4at+OJ0Kb/x6hJdX7MfhVGgTGciisZfRONi33PYSFDyHBAWVSFDwbH7tribs+kfQ6k3YM05x9svn5aoGNyCHKvWtP5LBxI+3kpZrJSLQyKLRl9GpSdljnQQFzyFBQSUSFDyURktw73swJ94BQOHhTaR9Ow/Fmq9yYQIkKLiLM9mFjF28kf2pufjqdbx2Vzf6tYss1UaCgueoTFDQXvRZIRoIjd5IxK1Pu0JCzp+fcfaLWRIShLhATLAvnz2YSO9W4RTaHYx/bxPvJx1TuyxRhyQoiAZPa/Sn0Z2z8WvZC6fdStqyuWT/sgQUp9qlCeGWgkx6Fo25jGE9Y3Eq8Mw3u3n+uz04ndLr443k1EMdkFMPnkPnH0KjYbMxRMTjKMzl7OczsZ3Zp3ZZDZIcijyPoij8b+1h5v20HyiZnOnfw7riZ6z4igjhXipz6kG+m6LB8gmOotGw59EHR1Gcm8HZT2dgTz+udllCeAyNRsPD17SkSYgvUz/bwfLdKaS89Sda3yC5OZoXkVMPokHSR8QTOXIu+uAo7FlnSP3wCQkJQlTTzV0b88F9CQT76dl2MpvIEXPQ+gWrXZaoJRIURINjbNyOqLtexCcgFFvqEVI+fILinFS1y2pwFEUptQjP1qtZKF88eAWRQUYMEXFE3TUHnX/17j4p3IsEBdGgmJr3pNGw2WhNARSd3E3Kx9Nx5merXZYQXqFFRACfTEik2JKGPiyWyBFz0AWEqV2WqCEJCqLB8G11OY1ufRqt3kTBoQ2c/XSGXP4oRC2LD/cn9aNpFOecRR/WpCQsBEpY8GQSFESDYGrWnYghT6LR+ZC/Zy1pX72AUmxVuywhvFJxTiopH02jODsFfWgMkSNeRBcYoXZZopokKAivZ4ztSMQtT6Hx0ZO/7zfSv5sPTofaZXm1C8cflLcI7+awnCXlo+nYs5LRh0SXjFkIaqR2WaIaJCgIr2aIbk2j22ag1RspOLSB9G//JRMpCVFPHLlppH48DXvmGXyCo0oGEZsjL72hcCsSFITX0kc0o9Gds9Aa/Sg8tp30b14EZ7HaZQnRoDhyM0rCQsZJfMyNSgYT+8mEdJ5EgoLwSj6hTYgcNhudKYCi03tJ+3I2SrFN7bKEaJAceZmkfvx/2LNT0IfE0Oj2Z9HoTWqXJSpJgoLwOj7mSCKHP4/OPxhryiHOfvYcir1I7bK8mow/EJfiyM/i7KczcBTkYIxuTcTQaaDVqV2WqAQJCsKraP2DaTT8BXwCw7GlH5dLIIVwI8VZZzj7+UyctiJ8m/ckbNAktUsSlSBBQXgPnZ5Gtz7tmpb57CfPyHzzQrgZW/IB0r95EcXpIKBTf4J736N2SeISJCgIrxF+w6MYY9qW3AXys+dw5GWqXZJXkEsdRW0rPLKJjOWvAWC+YhgB3W5QuSJxMRIUhFcwXzEc//Z9URzFpH39T4qzzqhdkhDiIvJ3riT7tw8ACL3uAXxbJ6pckaiIBAXh8fzaXkVw77sByFz5OtYTO1WuSAhRGTnrlpK79Uc0Gi0RN03FEN1a7ZJEOSQoCI9miGpF2A3/AMCy8Wvytv+kckVCiKrIXPk6BQfXo/ExEDF0usyx4IYkKAiPpQsMI+LWp0tmXTy8kaw1i9QuySvI+ANRrxQn6d+9jD3jFD5BEUQMeQI08qvJnch3Q3gkjd5IxK3P4BMYhi3tGOnL5srUzEJ4KMVWSNpXL+C0FWKK60Jwn9FqlyT+RoKC8Ehhgx7BGNUSR342Zz+fhWIrVLskIUQN2DNOkvHDfwAwJ9yGX5sr1S1IuHh8UFiwYAHx8fGYTCYSEhLYsGFDhW3fffddNBpNqcVkKj2NqKIozJgxg+joaHx9fenfvz8HDx6s67chqiCg83X4t++D4nSQ9tU/cVjOql2SEKIWFOz/g5z1XwAQdsNk9GGxKlckwMODwieffMKUKVN49tln2bJlC126dGHgwIGcPVvxL46goCCSk5Ndy/Hjx0s9P3fuXF555RUWLlzI+vXr8ff3Z+DAgRQVyRTA7kAf3pSQ/vcDkP3r+1hP71G5Is8jcyIId5b9yxKKjm9Ha/AtuT28wU/tkho8jw4K8+fPZ/z48YwdO5b27duzcOFC/Pz8WLSo4kFtGo2GqKgo1xIZ+dctTxVF4T//+Q9PP/00N998M507d+a9997jzJkzfP311/XwjsTFaHwMhA95Eq3eROHRLVjO/eUhhPAiipO0ZXMptqShD2tC+OB/ABq1q2rQPDYo2Gw2Nm/eTP/+/V3rtFot/fv3JykpqcLt8vLyiIuLIzY2lptvvpndu3e7njt69CgpKSml9mk2m0lISLjoPkX9COk3HkNEHI68LNK/mw/IX79CeCNnQQ5pX89BKbbj1zqRwJ5D1C6pQfPYoJCeno7D4SjVIwAQGRlJSkpKudu0adOGRYsW8c033/DBBx/gdDq54oorOHXqFIBru6rsE8BqtWKxWEotonb5tb2KwK7Xo5y7lMpZkK12SUKIOmRLPkDmqjcBCOkzWsYrqMhjg0J1JCYmMmrUKLp27UqfPn348ssviYiI4I033qjRfufMmYPZbHYtsbHyA12bfMyRrrvMWf78nKLj21WuyHPIGAThyfK2/UjhkU1ofAyE3fiY3JZaJR4bFMLDw9HpdKSmppZan5qaSlRUVKX2odfr6datG4cOHQJwbVfVfU6fPp2cnBzXcvLkyaq8FXExWh/ChzyB1uhP0ak9rrnhhRANQ8aPr+AotGCMaon5iuFql9MgeWxQMBgM9OjRg1WrVrnWOZ1OVq1aRWJi5W4u4nA42LlzJ9HR0QA0a9aMqKioUvu0WCysX7/+ovs0Go0EBQWVWkTtCL5yOMaYNjiK8khfNk8mVRKigXHkZZK54n8AmBPvlPtBqMBjgwLAlClTeOutt1iyZAl79+7lwQcfJD8/n7FjxwIwatQopk+f7mo/a9YsVqxYwZEjR9iyZQt33303x48f57777gNKroiYPHkyzz//PMuWLWPnzp2MGjWKmJgYhg4dqsZbbND0Ec0ISrgdgMzlr+LITVO5IvcjlzqKhqBg3+/k71mLRqsjfPAUND5GtUtqUHzULqAmhg0bRlpaGjNmzCAlJYWuXbuyfPly12DEEydOoNX+lYWysrIYP348KSkphISE0KNHD9atW0f79u1dbZ544gny8/OZMGEC2dnZXHXVVSxfvrzMxEyijmm0hF0/CY3Oh/z9f1Cw/w+1KxJCqChz5UKMsZ3QhzUhuO8Ysn6u2dgyUXkaRf7sqHUWiwWzWe6AVhOBlw0l9Nr7cBblcebtB3HkZ6ldkluS/77CHWk0dTPvgSm+G5HDZgOQ+snTFB3bViev05Dk5ORc8nS5R596EN7JxxxJcO+7Achas0hCghACgKJjW8nd8h1QMsWzxuCrckUNgwQF4XZCB01EqzdRdHw7eTtWqF2O25AxCEJA1trF2LPO4BMYLldB1BMJCsKt+He8Ft/4bjjtVjKWv6Z2OUIIN6PYrWT+XDIRU1DPm2UipnogQUG4Da1fMCHXjgcg5/ePKM5OVrkiIYQ7KjqyiYKDf6LR+RB63QNql+P1JCgItxHabzw630CsKYewbPxK7XKEEG4sa9VbOO1WTHFd8GvbW+1yvJoEBeEWjI3b49++D4rTQebyVxvkxEoyJ4IQlVeck4rlz88ACLl2HBq9XMJeVyQoCLcQcu04APJ2rMCWeljlaoQQniBn/RfYs5JlYGMdk6AgVOfX7mqMMW1wWgvI/v1DtcsRQngKh52sc3eYDLpsKD5hTVQuyDtJUBDq0ukJ6TMaAMv6L3DmZ6tbTz2SUwtC1Fzh4Y0UHNpQMrCxvwxsrAsSFISqgnrchI85kuLcdCwbv1a7HCGEB8r6+Q2UYhu+8V3xbXGZ2uV4HQkKQjVa3yDMiXcCkP3reyjFVpUrEkJ4ouKcVCyblgFgvmokUDdTSDdUEhSEasxXDEdrCsCWepj8XWvULkcI4cEs67/AaS3AGNUSv9aJapfjVSQoCFX4hMQQ2O0GALJWvwN41zn6S13qKGMShKhdzqJcLJu+BsDceyRo5NdbbZFPUqgiuM9oNDofCg5toOjEDrXLEUJ4AcvGb3AU5WEIj8O/3dVql+M1JCiIeqcPi8W/zZUAZP+yROVqhBDeQrHmY1n/BQDmK+8CrU7liryDBAVR74IuvwOA/P1/YE8/rnI1Qghvkrv5WxwFOehDYwjoeK3a5XgFCQqiXumCGuHfvg+Aa/pVbyDjD4RwD4q9iJxzxxbzFSNA56NyRZ5PgoKoV+aEW9FodRQe3YIt5ZDa5QghvFDe1h8ozs3Ax9yIwC6D1C7H40lQEPVG6x9MQOcBAOQkeU9vghDCvSjFNnKSPgEgqNetcgVEDcmnJ+pNUM+haHwMFJ3ei/XkTrXLqTS51FEIz5O/82ccBTn4mBvJvAo1JEFB1Aut0d81b4Il6VOVqxFCeDul2Ebuth8BCOw5ROVqPJsEBVEvArvfiNboh+3sUQoPb1S7HCFEA5C39QcUhx1Tkw4YolqpXY7HkqAg6pzGx+BK9DledKWDEMK9OfIyyd/7GyC9CjUhQUHUOb82V6HzM1Ock0rBvt/VLueSZPyBEN4jd9M3APi37Y0uIFTlajyTBAVR5wK6DgQgd/tPoDhVrkYI0ZDYUg9TdHI3Gp2Pa5yUqBoJCqJO6cObYmrSAcXpIH/nz2qXI4RogM73KgR0vR6Nj0HlajyPBAVRpwLOTXZSeHA9jrxMlasRQjREBQf/pDgnFZ2fGf/2fdUux+NIUBB1RuNjwP/cXOu525erXE0JmRNBiAZIcWLZ/B0AAXL6ocokKIg649fmKnSmAIpzUik6ulXtcoQQDVj+rlUojmKMUS3Rh8epXY5HkaAg6kxA15LTDrnbfwLkL3UhhHqchRYKj2wCwL/DNSpX41kkKIg6oQ+Pw9Skfckgxh0r1S5HCCHI37UaAP8OfeX+D1Ugn5SoEwFdSi6JLDy4Hkd+lmp1yPgDIcR5BYc34CjKwycwHFPTTmqX4zEkKIjap9Hi3+5qAHJ3rFC5GCGEOMdRTMG+kpka5fRD5UlQELXO2KQDOv9gHIW5FB2TQYxCCPeRv2sNAH6tr0DjY1S5Gs8gQUHUOr82VwJQePBPcDrq9LXkUkchRFVYT+/Bnp2C1uiHb6vL1S7HI3h8UFiwYAHx8fGYTCYSEhLYsGFDhW3feustevfuTUhICCEhIfTv379M+zFjxqDRaEotgwYNquu34UU0+LW5AoD8/X+oXIsQQpSVv7ukVyGgo5x+qAyPDgqffPIJU6ZM4dlnn2XLli106dKFgQMHcvbs2XLbr127lhEjRrBmzRqSkpKIjY1lwIABnD59ulS7QYMGkZyc7Fo+/vjj+ng7XsHYuB0+AaE4i/IoOr5N7XKEEKKM80HBFN8NrV+wusV4AI8OCvPnz2f8+PGMHTuW9u3bs3DhQvz8/Fi0aFG57T/88EMeeughunbtStu2bXn77bdxOp2sWrWqVDuj0UhUVJRrCQkJqY+34xX82pacdig4tAEcxSpXI4QQZRVnncGafBCNVodvi55ql+P2PDYo2Gw2Nm/eTP/+/V3rtFot/fv3JykpqVL7KCgowG63Expa+taja9eupVGjRrRp04YHH3yQjIyMWq3de2nwa11y2qFgf+3fTlrGIAghakvhkY0A+La4TOVK3J/HBoX09HQcDgeRkZGl1kdGRpKSklKpfTz55JPExMSUChuDBg3ivffeY9WqVbz00kv88ssvXH/99TgcFQ/Ks1qtWCyWUktDZIhpjU9QBE5rAYUyZbMQwo0VHjoXFOK7gdZH5WrcW4P9dF588UWWLl3K2rVrMZlMrvXDhw93/btTp0507tyZFi1asHbtWvr161fuvubMmcPMmTPrvGZ357ra4dAGcNhVrkYIISpmSzmEIy8LXUAIpibtKTqxQ+2S3JbH9iiEh4ej0+lITU0ttT41NZWoqKiLbvvyyy/z4osvsmLFCjp37nzRts2bNyc8PJxDhw5V2Gb69Onk5OS4lpMnT1b+jXgRv1aJAOQfkKsdhBDuTqHwaMm9H3xb9lK5FvfmsUHBYDDQo0ePUgMRzw9MTExMrHC7uXPnMnv2bJYvX07PnpcexHLq1CkyMjKIjo6usI3RaCQoKKjU0tD4mCPRh0SjOIqrfadImRNBCFGfCg+fCwrNZUDjxXhsUACYMmUKb731FkuWLGHv3r08+OCD5OfnM3bsWABGjRrF9OnTXe1feuklnnnmGRYtWkR8fDwpKSmkpKSQl5cHQF5eHlOnTuXPP//k2LFjrFq1iptvvpmWLVsycOBAVd6jpzDFlfTMWJP3o9iLVK5GCCEurfDoFhRHMfqwJvgEV/zHYEPn0UFh2LBhvPzyy8yYMYOuXbuybds2li9f7hrgeOLECZKTk13tX3/9dWw2G7fffjvR0dGu5eWXXwZAp9OxY8cOhgwZQuvWrRk3bhw9evTgt99+w2iUqT4vxhTXBYCi43KeTwjhGRRbIUUndwNy9cPFaBTp0611FosFs9msdhn1qsnD76MLCCHlo+lYT+6s1DbyoyeEd9JoNGqXUGmBlw0l9Nr7KDy6hbOfzlC7nHqXk5NzydPlHt2jINyDPrwpuoAQnHYr1jN71S5HCCEqrfBwyWWSpthOoNOrXI17kqAgaszU9Nz4hNN7ZDZGIYRHKc48jSMvC42PHmNUC7XLcUsSFESNucYnHNuuciVCCFF11jP7gJJ71YiyJCiImtFoMTXtBFBqwpJLXeoo4xOEEO7CerrklKkxRoJCeSQoiBoxRLZAawrAWZSHLaXiSamEEMJduYKC9CiUS4KCqBFjk5L/WEWn9oDiVLkaIYSoOmvKIRSHHV1ACD7myEtv0MBIUBA1YohsCYAt5aDKlQghRDU57NhSDgPSq1AeCQqiRgyRzQH4/M1/y/gDIYTH+mtAY1uVK3E/EhREtWl8DOjDYgHo0Ljh3d9CCOE9ZJxCxSQoiGrTR8Sj0epwFOQQFWS69AZCCOGmzgcFfUQ8GoOvytW4FwkK4pIqurxx0VcrAOjbpYVHTdkqhBAXcuRlUpybjkarwxARr3Y5bqVGQcFut3Py5En2799PZmZmbdUkPMTuMxYAOsQ0rPtaCCG8kz39BIDrlKooUeWgkJuby+uvv06fPn0ICgoiPj6edu3aERERQVxcHOPHj2fjxo11UatwM7tP5wDQIUbGJwghPJ894xQgQeFCVQoK8+fPJz4+nsWLF9O/f3++/vprtm3bxoEDB0hKSuLZZ5+luLiYAQMGMGjQIA4elEvmvFWxw8m+lFwAOjaWHgUhhOezZ5wEJChcyKcqjTdu3Mivv/5Khw4dyn2+V69e3HvvvSxcuJDFixfz22+/0apVq1opVNSfylzeeDgtH2uxkwCjD3GhfvVQlRBC1K2/gkITlStxL1UKCh9//HGl2hmNRh544IFqFSQ8w+G0PABaRQag1cpARiGE5zsfFHTmRmh8jCjFVpUrcg9y1YOolpOZBQDEhkhvghDCOzgLcnAUWtBotPiExqhdjtuoUo/ClClTKt12/vz5VS5GeI5TWYUAxIbK9cZCCO9hzziJrkkH9GGx2M8eVbsct1CloLB169ZSj7ds2UJxcTFt2rQB4MCBA+h0Onr06FF7FYpaVVvTK5/Mkh4FIYT3sWecwnQuKIgSVQoKa9ascf17/vz5BAYGsmTJEkJCQgDIyspi7Nix9O7du3arFG7n/KmHJhIUhBBeRK58KKvaYxT+9a9/MWfOHFdIAAgJCeH555/nX//6V60UJ9yToihy6kEI4ZWKs5IB8AlqpHIl7qPaQcFisZCWllZmfVpaGrm5uTUqSri3tDwr1mInWg3EBEtQEEJ4D0d+FgC6gJBLtGw4qh0UbrnlFsaOHcuXX37JqVOnOHXqFF988QXjxo3j1ltvrc0aRQ1ceH+G2nAys6Q3Idrsi14nF84IIbyHI+9cUPAPBuTSb6jiGIW/W7hwIY8//jh33XUXdrsdRVHQ6/WMGzeOefPm1WaNws2cOjeQsXGI9CYIIbyLo6AkKGh0erS+gTgLLSpXpL5qBwU/Pz/+97//MW/ePA4fPgxAixYt8Pf3r7XihHtyjU+QgYxCCG/jKMZRaEHnG4TOP0SCAjUICucdP36cM2fOYLPZOHbsmGv9kCFDarprUQm1dTqhKtJyS2YraxRkrPfXFkKIuubIyzoXFIKxpx9XuxzVVTsoHDlyhFtuuYWdO3ei0Whcv7A0mpJzOg6Ho3YqFG7HUmQHwOyrV7kSIYSofY78LIiIQxcQqnYpbqHaI9EeffRRmjVrxtmzZ/Hz82PXrl38+uuv9OzZk7Vr19ZiicLdWAqLAQgySVAQQngfR14mADp/ufIBatCjkJSUxOrVqwkPD0er1aLT6bjqqquYM2cOjzzySJlZHIX3kB4FIYQ3++sSSelRgBr0KDgcDgIDAwEIDw/nzJkzAMTFxbF///7aqU6UcuGljmqMTwCwFJYEhSDfGg9xEUIIt+PIzwakR+G8ah/pO3bsyPbt22nWrBkJCQnMnTsXg8HAm2++SfPmzWuzRuFmXEFBTj0IIbyQsygPAI1RruyCGgSFp59+mvz8fABmzZrFjTfeSO/evQkLC+OTTz6ptQKF+7EUnRujIKcehBBeSCm2AaDRGVSuxD1UOygMHDjQ9e+WLVuyb98+MjMzCQkJcV35ILxPscNJnvX8YEY59SCE8D6uoOAjfwxBNcco2O12+vXrx8GDB0utDw0NlZBQA+WNQVB7PMKFcs/1JoD0KAghvJPiOB8UpEcBqhkU9Ho9O3bsqO1ahAc4f8WDr14n93kQQngl6VEordpH+rvvvpt33nmnNmsRHsDucAJg1EtIEEJ4J6W45A8iGaNQotpH++LiYl5//XV69uzJ/fffz5QpU0ot9WXBggXEx8djMplISEhgw4YNF23/2Wef0bZtW0wmE506deKHH34o9byiKMyYMYPo6Gh8fX3p379/mVMstcUdTy0IIURDpxSXTFMvpx5KVDso7Nq1i+7duxMYGMiBAwfYunWra9m2bVstllixTz75hClTpvDss8+yZcsWunTpwsCBAzl79my57detW8eIESMYN24cW7duZejQoQwdOpRdu3a52sydO5dXXnmFhQsXsn79evz9/Rk4cCBFRUX18p6EEEKoy9WjIEEBAI3iwX/KJiQkcNlll/Haa68B4HQ6iY2NZdKkSUybNq1M+2HDhpGfn893333nWnf55ZfTtWtXFi5ciKIoxMTE8Nhjj/H4448DkJOTQ2RkJO+++y7Dhw+vVF0WiwWz2XzJdp740R86m0v/+b8S7Kdn24wBapcjhHBDnj6o3cccSeMHSk6tH3/pRpWrqVs5OTkEBQVdtI3Hnmi22Wxs3ryZ/v37u9ZptVr69+9PUlJSudskJSWVag8ll3meb3/06FFSUlJKtTGbzSQkJFS4TwCr1YrFYim1CCGE8FB/H8SolcvAa/QJFBUVsWPHDs6ePYvT6Sz1XF3fZjo9PR2Hw0FkZGSp9ZGRkezbt6/cbVJSUsptn5KS4nr+/LqK2pRnzpw5zJw5s8rvQQghhHB31Q4Ky5cvZ9SoUaSnp5d5TqPRNKjbTE+fPr3UAE6LxUJsbKyKFQkhhKi2c2MUnLZCcBZforH3q/aph0mTJnHHHXeQnJyM0+kstdRHSAgPD0en05GamlpqfWpqKlFRUeVuExUVddH2579WZZ8ARqORoKCgUosQQgjPdH4Q4/n5FBq6ageF1NRUpkyZUqabvr4YDAZ69OjBqlWrXOucTierVq0iMTGx3G0SExNLtQdYuXKlq32zZs2Iiooq1cZisbB+/foK9ymEEMK7uIKCw65yJe6h2qcebr/9dtauXUuLFi1qs54qmTJlCqNHj6Znz5706tWL//znP+Tn5zN27FgARo0aRePGjZkzZw4Ajz76KH369OFf//oXgwcPZunSpWzatIk333wTKDllMnnyZJ5//nlatWpFs2bNeOaZZ4iJiWHo0KG1Xv+FI4M96SoIDypVCCGq5PyMjNKjUKLaQeG1117jjjvu4LfffqNTp07o9aWnunzkkUdqXNylDBs2jLS0NGbMmEFKSgpdu3Zl+fLlrl6OEydOoNX+1WlyxRVX8NFHH/H000/zf//3f7Rq1Yqvv/6ajh07uto88cQT5OfnM2HCBLKzs7nqqqtYvnw5JpOpzt+PJzD66AAosjecMShCiIbl/IyM5+dTaOiqPY/CO++8wwMPPIDJZCIsLKzUX8cajYYjR47UWpGeprLzKFzIE3oUcgrtdJm5AoB9swdh0utUrkgI4W48fR4FU/OeRN7xHNbkg6S89w+1y6lTlZlHodo9Ck899RQzZ85k2rRppf5qF9V3qf9c7hAkAo0+aDQlpx5yi4olKAghvI7WNUZBTj1ADQYz2mw2hg0bJiGhgdFqNQQYS/Ll+TtJCiGEN5GrHkqr9m/50aNH88knn9RmLcJDmH1LxqPkFEpQEEJ4n78GM8oxDmpw6sHhcDB37lx++uknOnfuXGYw4/z582tcnHBPQSY9UIhFgoIQwgtpTSXn7J1FeSpX4h6qHRR27txJt27dAErdfRE8fyCLuyrvc1Vj3EKQ7/lTDzJjmRDC++gCQgBw5GepXIl7qHZQWLNmTW3WITxISY8C0qMghPBKOv9zQSEvU+VK3EOVxiicOHGiSjs/ffp0ldoLz3B+jIIMZhRCeCNdQCggPQrnVSkoXHbZZdx///1s3LixwjY5OTm89dZbdOzYkS+++KLGBQr3E3R+MGOBBAUhhPfR+QcDEhTOq9Kphz179vDCCy9w3XXXYTKZ6NGjBzExMZhMJrKystizZw+7d++me/fuzJ07lxtuuKGu6hbnqDH3QqNAIwAplqJa37cQQqjN1aOQJ0EBqtijEBYWxvz580lOTua1116jVatWpKenc/DgQQBGjhzJ5s2bSUpKkpDgxWJD/QA4mVmgciVCCFG7ND5GtEZ/QHoUzqvWYEZfX19uv/12br/99tquR3iAJiG+AJzMKlS5EiGEqF3nTzs47VYUa766xbgJmVZRVFlsSEmPQlquVW4OJYTwKucvjXRKb4KLBAUvp9FoSi21IdhP75rG+ZT0KgghvIjO//wVD9nqFuJGJCiIKtNoNH87/SDjFIQQ3sMntDEAxdkpKlfiPiQoiGppcu70g/QoCCG8iT4sFgB7xkmVK3Ef1Z6ZUXimypx+qMwllbGhJT0Kp+TKByGEF9GHNgEkKPxdlXoUfvjhB+Li4ggNDaVfv34sX74cgFmzZjF48GDmzJnD2bNn66RQ4V7O9yjIqQchhDfRh0lQuFCVgsLjjz/Orbfeyqeffkq3bt0YOnQod9xxB3PnzqVp06Z8++23dOvWjQMHDtRVvcJNxJ4bo3BCehSEEF5CFxiG1uiH4nRgz0pWuxy3UaVTD8ePH+fRRx8lPj6e/v3707ZtW+6//37mz5/Po48+CsDkyZN56qmn+Oyzz+qkYOEeWkUGAnAgNQ+7w4leJ8NdhBCe7fz4hOKsM+CUu+OeV6Wje3x8PBs2bHA9HjlyJIqicOWVV7rWPfTQQ/z++++1V6God5W5pDIu1I8Aow+2YieH0+Se7UIIz/fXQMZTKlfiXqoUFKZOncq4ceOYNWsWGzduRKfT8fvvv9O2bVtXm4KCAvLzZTYrb6fVamgfHQTA7tMWlasRQoiakyseylelUw9jxowhMDCQf//738yaNQudTkfbtm3p3r073bt3p127dsyaNYvExMS6qle4kfYxQWw4lsnuMxZu66F2NUIIUTMSFMpX5csjb7vtNm677Tby8vLYvn0727ZtY9u2bbz33nvs3r2boqIiYmJiuO222+jcuTOdO3fmlltuqYvahco6NjYDsOtMjsqVCCFEzenDmwISFC6kUWrxPsQOh4N9+/a5wsO2bdvYsWMHqamptfUSHsFisWA2m9Uuo87pI+KJufc1nNZ8jv3rDrTa2pkiWgjh2Wpruvj65BMcReP730YptnPiP3eCw652SfUiJyeHoKCgi7ap1QmXdDodHTp0oEOHDowcObI2dy3ckD3jJEqxDa3RnxOZBcSH+6tdkhBCVIuxcTsArKmHGkxIqCy5pk1Un9OBLe0YALvPyIBGIYTncgWF03tVrsT9SFAQNWJLPQLA3ZOm1fpdKoUQor4YY0qu3pOgUJYEBVEjttTDABiiW6tciRBCVI/G4Is+Ig4A2+l9KlfjfiQoiBqxntwFnOu208k9xoQQnscY3RqNVkdxdgqO/Cy1y3E7EhREjdgzTlKcl4lWb8QY007tcoQQosr+Gp8gvQnlkaAgasx6fAcAprjOrnUXTgNd3iKEEO7gfFAokvEJ5ZKgIGqs6MR2AExxXVSuRAghqkqDMaYNIAMZKyJBQdRY4bGSoGCMbo3G4KtyNUIIUXmGyOZoTQE4rQXYz13uLUqToCBqzGE5iz07BY3OB2OTDmqXI4QQlebb4jIAio5vB8WpcjXuSYKCqBVFx8+ffuh8iZZ/kTELQgi1nQ8KhYc3qlyJ+/LYoJCZmcnIkSMJCgoiODiYcePGkZeXd9H2kyZNok2bNvj6+tK0aVMeeeQRcnJK39CovEF3S5cureu34/HOBwVfGacghPAQWj8zhuhWABQe2aRyNe7LYy98HzlyJMnJyaxcuRK73c7YsWOZMGECH330Ubntz5w5w5kzZ3j55Zdp3749x48f54EHHuDMmTN8/vnnpdouXryYQYMGuR4HBwfX5VvxCkXnrnwwRLZA6xuEs1CmdBZCuDff5j3QaLRYUw7hyMtUuxy35ZFBYe/evSxfvpyNGzfSs2dPAF599VVuuOEGXn75ZWJiYsps07FjR7744gvX4xYtWvDCCy9w9913U1xcjI/PXx9FcHAwUVFRdf9GvIizIBvb2aMYGjXDt3lP8nevrvI+LnX6oRZvdCqEEPi26AXIaYdL8chTD0lJSQQHB7tCAkD//v3RarWsX7++0vs5f3vNv4cEgIcffpjw8HB69erFokWLLvkLymq1YrFYSi0NUcGBdQD4tb1K5UqEEOIStDp8m3UDJChcikcGhZSUFBo1alRqnY+PD6GhoaSkpFRqH+np6cyePZsJEyaUWj9r1iw+/fRTVq5cyW233cZDDz3Eq6++etF9zZkzB7PZ7FpiY2Or9oa8RMH+PwDwje+GxuCncjVCCFExY5P2aI3+OPKzsSUfVLsct+ZWQWHatGmXnM1v376aT7FpsVgYPHgw7du357nnniv13DPPPMOVV15Jt27dePLJJ3niiSeYN2/eRfc3ffp0cnJyXMvJkydrXKMnsqefwJ5xEo2PHr+WvdQuRwghKuS62uHIJkBOa16MW41ReOyxxxgzZsxF2zRv3pyoqCjOnj1ban1xcTGZmZmXHFuQm5vLoEGDCAwM5KuvvkKv11+0fUJCArNnz8ZqtWI0GsttYzQaK3yuocnf/wfBVwzHr82V5O9ZW6v7Lm8Mg4xbEEJUh5+MT6g0twoKERERREREXLJdYmIi2dnZbN68mR49egCwevVqnE4nCQkJFW5nsVgYOHAgRqORZcuWYTKZLvla27ZtIyQkRIJAJRXsKwkKvs17oDH4otgK1S5JCCFKMUS2QB/WBKfdSuHRLWqX4/bc6tRDZbVr145BgwYxfvx4NmzYwB9//MHEiRMZPny464qH06dP07ZtWzZs2ACUhIQBAwaQn5/PO++8g8ViISUlhZSUFBwOBwDffvstb7/9Nrt27eLQoUO8/vrr/POf/2TSpEmqvVdPY087ij3zDBofA77Ne156AyGEqGf+Ha8FoPDQevljphLcqkehKj788EMmTpxIv3790Gq13Hbbbbzyyiuu5+12O/v376egoACALVu2uK6IaNmyZal9HT16lPj4ePR6PQsWLOAf//gHiqLQsmVL5s+fz/jx4+vvjXmBgv1/YE68A782V1Kw7ze1yxFCiL9otPi3uxqA/N1rVC7GM2gUOclb6ywWC2azWe0yVGOIbEH0mP/itBVx6rWRKHararXIj7cQ9c+dp2Q3Ne9J5B3P4cjP5tT/RoPToXZJqjo/TcDFeOSpB+HebKmHKc5OQWsw4duy4jEjQghR3wLOnXbI3/trgw8JlSVBQdSJvHNdeoFdBqpciRBClNAY/Fx/vFRn9tiGSoKCqBN521egKE5McV3wCSk7pbYQQtQ3vzZXotUbsWecxJZySO1yPIYEBVEnHLlpFB7ZDECAir0KcitrIcR5AR2uASBvl/QmVIUEBVFn8rb9CEBAp/6g89gLbIQQXkAXGIEprjNArU8G5+0kKIg6U3h4E8W56ej8zPi1vkLtcoQQDVhg15KezaLjO3BY0lSuxrNIUBB1R3GSt2MF4D6DGi91LxE5PSGE99H4GAjoej0AuVu+U7kazyNBQdSpvO0rUZwOGdQohFCNf/u+6PzMFOekUnDwT7XL8TgSFESdcpdBjUKIhiuw580AWDZ/B4pT5Wo8jwQFUefyti0HSgY1anwMKlcjhGhITHFdMETE4bQVuk6FiqqRoCDqXOGRTRRnp6DzM+Pf6Tq1y7kkGbMghPc435uQt/NnFGu+ytV4JgkKou4pTnLWfwGAOeE20OpULkgI0RD4hMTg17IXALmbv1W5Gs8lQUHUi7ydP+PIy8LH3Aj/9n3ULkcI0QAE9hgCQMGhDRRnnVG5Gs8lQUHUD4cdy8avATBffgdo5EdPCFF3NEZ/Ajr1AyD33LFHVI8crUW9yd32A46iPPRhsfi1ulztcipN5l4QwvME9bgJrcEX29mjFJ3YoXY5Hk2Cgqg3iq3QdZ4wKPFOlasRQngrrSmAoF63AJCT9InK1Xg+CQqiXuVu/hanrQhjVEtM8d3ULkcI4YWCet2K1uiP7exRCvb9oXY5Hk+CgqhXzkILedtL5lUwe1GvgpyKEMI9aP3MrkGM2b+9DyjqFuQFJCiIemfZ+BWKw46paSeMTTqoXY4QwouYL78DrcGE9cwBCg9tULscryBBQdQ7R24GeTtWAhByzThA/gIXQtScLiCMwG43AOd7E0RtkKAgVJH9x0c4rQUYY1rj16632uUIIbyAOfFOND4Gik7uoujYVrXL8RoSFIQqnPnZWM7N1hjSZzTo9CpXVLvkkkoh6pcuqBEBXQYAkP2r9CbUJgkKQjWWjV9TnJuOjzmSoB43qV2OEMKDBV85Ao1OT+HRLVhP7Va7HK8iQUGoRim2kv3re0BJl6HWN0jlioQQnsgQ1RL/c7MwZv/2gcrVeB8JCkJV+bvWYEs9jNYUgPmK4WqXI4TwOBpCr3sAjUZL3u412JIPqF2Q15GgIFSmkLX6HQACu92AT0iMyvXUHxmzIETNBXTujzGmLU5rAdlrFqldjleSoCBUV3RiBwWHNqDR+RDSd6za5QghPITWFEBwnzEAZP/+IY78LHUL8lISFIRbyFq7CMXpwK91IqZm3dUuRwjhAYJ734POz4wt7Ti5W75TuxyvJUFBuIXijFOuG0aFDXwYjd6kckX1Ty6nFKLyDJEtCOh2PQCZK18Hp0PliryXBAXhNrJ/e5/i7BR8zJEEX32P2uUIIdyWhtABD6LRaMnfsxbryV1qF+TVJCgIt6HYrWT8tACAwB43YYhurXJFQgh35N/prwGMWTKAsc5JUBBupejYVvJ2rkKj0RJ2/SOg9VG7JCGEG9H6mQnpOwaA7D8+xpGXqW5BDYAEBeF2sla/jSM/G0NEPObLb1e7HLch4xaEgLBBk0oGMJ49Su7mZWqX0yBIUBBux1mUS+aqNwEwXzEMfVisyhUJIdyBf6f++LW6HMVhJ/27+TKAsZ5IUBBuqWDvr+fmVtATOmgScitqIRo2XVAjQvtNAEqmabanHVW5oobDY4NCZmYmI0eOJCgoiODgYMaNG0deXt5Ft+nbt2+ZrtsHHnigVJsTJ04wePBg/Pz8aNSoEVOnTqW4uLgu34qoQOaK/+G0FmBq0p7AnnLTKCEaLg3hg/+B1uhH0ak9WDZ8pXZBDYrHBoWRI0eye/duVq5cyXfffcevv/7KhAkTLrnd+PHjSU5Odi1z5851PedwOBg8eDA2m41169axZMkS3n33XWbMmFGXb0VUwJGbTtbaxQCE9BmLIbKFyhW5H5l7QTQEgZfdjKlpJ5y2QjK+nw+KU+2SGhbFA+3Zs0cBlI0bN7rW/fjjj4pGo1FOnz5d4XZ9+vRRHn300Qqf/+GHHxStVqukpKS41r3++utKUFCQYrVaK11fTk6OAshSS0vELU8pcU9+p8SMf0PRGHxVr8eTFiHUUJs/w/rwpkrTx75U4p78TgnoMlD1/1PetuTk5Fzy++mRPQpJSUkEBwfTs2dP17r+/fuj1WpZv379Rbf98MMPCQ8Pp2PHjkyfPp2CgoJS++3UqRORkZGudQMHDsRisbB7d8X3N7darVgsllKLqD0ZP/6XYstZ9KGNCb3uQbXLEULUF60PYYOnoPExUHh4E3nbf1K7ogbJI4NCSkoKjRo1KrXOx8eH0NBQUlJSKtzurrvu4oMPPmDNmjVMnz6d999/n7vvvrvUfv8eEgDX44vtd86cOZjNZtcSGyuj9GuTsyiP9GUvozgdBHS8Fv+O/dQuSQhRD4KvGokxqiWOQgsZP/5X7XIaLLcKCtOmTbvkOdd9+/ZVe/8TJkxg4MCBdOrUiZEjR/Lee+/x1Vdfcfjw4RrVPX36dHJyclzLyZMna7Q/UZb19B6yf/8QgNDrHsQntInKFXkGGbcgPJVvywTMiXcAkPnTArkzpIrcatq7xx57jDFjxly0TfPmzYmKiuLs2bOl1hcXF5OZmUlUVFSlXy8hIQGAQ4cO0aJFC6KiotiwYUOpNqmpqQAX3a/RaMRoNFb6dUX1WP78HFPTLvjGdyHi5idJef8xlGKb2mUJIWqZT0gM4TdOAcCyaRkF+/9QuaKGza2CQkREBBEREZdsl5iYSHZ2Nps3b6ZHjx4ArF69GqfT6frlXxnbtm0DIDo62rXfF154gbNnz7pObaxcuZKgoCDat29fxXcjap3iJOO7l4ke+yqGRs0IueZeMlcuVLsqIUQt0uhNRNz6FFqjP0Und5O15h21S2rw3OrUQ2W1a9eOQYMGMX78eDZs2MAff/zBxIkTGT58ODExMQCcPn2atm3bunoIDh8+zOzZs9m8eTPHjh1j2bJljBo1iquvvprOnTsDMGDAANq3b88999zD9u3b+emnn3j66ad5+OGHpcfATTjys0j/fj4Agd1vxK/d1SpX5HnkkkrhzsJueBRDeBzFuRmkf/OizL7oBjwyKEDJ1Qtt27alX79+3HDDDVx11VW8+eabruftdjv79+93XdVgMBj4+eefGTBgAG3btuWxxx7jtttu49tvv3Vto9Pp+O6779DpdCQmJnL33XczatQoZs2aVe/vT1Ss6OgWcv78DICw6x+Vu0wK4SUCL7sF/7a9URzFpH8zR8YluAnNuWteRS2yWCyYzWa1y/BuGi0Rtz6NX8teFOdlkvLeP3DkZqhdlVeQQ4Koqer0TBljOxE5/Hk0Wh0ZK14nb+v3dVCZuFBOTg5BQUEXbeOxPQqigVOcpH87D1vaMXwCQom49Rk0ejk9JIQn0gWGEXHzk2i0OvJ2rZaQ4GYkKAiPpdgKOfv5LBz52RijWhJ+4+PIzaNqTsYsiPqk0ZuIuOVpdP7B2FKPkPnTArVLEheQoCA8msNylrSvXkAptuPXOpHgq0epXZIQorK0OiKGTscY3QpHQc65/8tWtasSF5CgIDye9fRe16xt5sQ78O9wrcoVCSEqI2zQI/g274HTVsTZz2dSnJOqdkmiHBIUhFfI37OWnHWfABA2aBLGxjLvhRDuLPjqUQR06ofidJD+zYvYkg+oXZKogAQF4TWyf/uA/P1/oPHRE3HbM+gj4tUuyStcat4FGccgqiqw+42YE+8EIGP5axQe2aRyReJiJCgIL6KQ8f18rKf3ofMNJHLYbHxCYtQuSgjxN36tryCk/wTgXLjfuVLlisSlSFAQXkWxW0n97FlsqYfR+YcQOfwFdEGNLr2hEKLOGZt0IPymx9FotORu/ZGcdUvVLklUggQF4XUUaz6pnzyDLf0EPkERJWEhIEztsryanIoQl6IPjyPitmfQ+BgoOJBE5srX1S5JVJIEBeGVnIUWzn7yNPasZPQh0UQOfx6t78VnHxNC1A19RDMiR/wTnSmAolN7SP92HihOtcsSlSRBQXgtR14mqUufotiShj4slshhs9Ea/dUuS4gGxRDZgsgRL6DzM2M9c4C0z2fK7eE9jAQF4dUclrOkLn0KR14WhsgWNLpjJhqDr9plCdEgGKJa0mj4C+h8g7Ce3kfqJ0/jtOarXZaoIgkKwusVZ50h9ZOncRRaMDZuS6M7nkMjPQt1Si6pFIbo1kQOe951uiH102dQbAVqlyWqQYKCaBDs6cc5++kMHEV5mJp0IOquF9H5h6hdlhBeydi4LZHDnkdrCqDo5C7OfvYsiq1Q7bJENUlQEA2GLeUQqR9NozgvE0OjZkSOnIuPOVLtsoTwKhuPZdLojllojX4UHd8hIcELSFAQDYo97RipH0z962qIu+ehD49TuywhvMK6Q+mMXrQBrdGPwmPbOPv5TBS73OTJ00lQEA1OcU4qqR8+ge3sUXwCQokc+RLGxm3VLqvBkTEL3uXzzacYtWgDBTYHhUe3kPbFLLkTpJeQoCAaJEd+FqkfTaPo1G50pgAaDXseU/MeapclhMdRFIV/rzzA459tp9ipcGPnaM5+MVsugfQiEhREg+W05nP2kxkUHt6EVm+i0a3P4NfuarXLEsJj2IqdPPbpdv676iAAD/VtwSvDu4HDrnJlojZpFEVR1C7C21gsFsxms9pliMrS6gi/4R/4d+gLQPYfH5Hz+8eA/NdwJ3Koci85BXbu/2ATfx7JRKfV8PzQjozo1RRATiV5kJycHIKCLj5rrQSFOiBBwRNpCLnmXoJ63QJAwYEk0r+fL6O13YgcqtzHycwCxizewOG0fAKMPiwY2Z0+rSNcz0tQ8BwSFFQiQcFz+Xe8lrCBE9H4GLClHSfty9kUZ6eoXZZAgoK72HYym/uWbCQ9z0a02cSiMZfRLrr0LxoJCp6jMkFBxigI8Tf5u1aT8tE0inMzMETEETXq35jiu6pdlhBu4dONJxn2RhLpeTbaRwfx1UNXlgkJwvtIj0IdkB4Fz6cLCCXilv/DGNMWxekga80icjd9o3ZZ4gJy+KofhTYHz3yzi883nwLg2raNeGVENwKMPuW2lx4FzyGnHlQiQcFL6PSEDXyIgE7XAZC3cxUZP70mI7rdiBy+6t7htDwe+mAL+1Nz0WrgsQFteLBPC7TaisOABAXPIUFBJRIUvEtgj5sIufY+NFodttTDpC2bR3HmKbXLEkhQqGvfbj/DtC92kG9zEB5g5JURXbmiRfglt5Og4DkkKKhEgoL3McV1IXzIE+j8zDjtRWStfoe8bT+qXVaDJ4evumEtdvD8d3t5/8/jAFzePJRXRnSjUaCpUttLUPAcEhRUIkHBO+kCQgm7YTK+zboDUHDwTzJ+fAVnoUXlykRF5PBWdSczC3j4oy3sOJUDwMPXtOAf/Vvjo6v82HcJCp5DgoJKJCh4Mw2BPYcQ0mcMGh89xXmZZHz/b4qObVW7MFEOObxVnqIofLb5FLO/20NuUTHBfnr+fWdXrmnbqMr7kqDgOSQoqESCgvfTRzQjfMjjGM7dedKy8WuyfnkXHMXqFiZKkcNb5STnFDLti538ciANgG5Ng3ntru40Dvat1v4kKHgOCQoqkaDQMGh8DAT3vZegHjcCYDt7lPRv52FPP6FyZeI8ObxdnKIofLbpXC+CtRiDj5bHrmvNfb2bo7vIVQ2XIkHBc0hQUIkEhYbFt8VlhF3/KDr/YBSHHcv6L8hJ+lTunuem5JBX4kx2IdO+3Mmvf+tFmHd7F1o2CqjxviUoeA4JCiqRoNDwaP2CCRs0Cb9WCQDYs86QueJ1Gbvghhr6IU9RFD7ZeJLnv99L3rlehMcHtGbcVTXrRfg7CQqeQ4KCSiQoNFy+rRIJve5+fAJLrjXP3/MLmavfwpmfrW5hwqUhH/KOpefzzDe7+O1gOgDdmwYzt5Z6Ef5OgoLnkKCgEgkKDZvG4EvwVSMJ7HETGq0OZ1EeWb++R9625aA41S5PXII3HhJzi+y8tvoQi/44it2hYPTR8viANtx7VbNa60X4OwkKnsOrbwqVmZnJyJEjCQoKIjg4mHHjxpGXl1dh+2PHjqHRaMpdPvvsM1e78p5funRpfbwl4SUUWyFZq98m5b0pWJMPoDUFEDbgIaLunoe+UTO1yxMNiMOp8MnGE1zz8lre+PUIdodCn9YR/Phob8ZfXXunGoR389geheuvv57k5GTeeOMN7HY7Y8eO5bLLLuOjjz4qt73D4SAtLa3UujfffJN58+aRnJxMQEBJ15tGo2Hx4sUMGjTI1S44OBiTqXIzkoH0KIi/0WgJ6Ho9IX1GoTX6ozgd5O38mZzfP8KRl6F2daIcHnpILGPjsUxmfrubXadLJgRrHu7PMze2r9a8CFUlPQqew2tPPezdu5f27duzceNGevbsCcDy5cu54YYbOHXqFDExMZXaT7du3ejevTvvvPOOa51Go+Grr75i6NCh1a5PgoK4kC4glJBr78O/3dUAOO1Wcjd/i+XPz3Ba81WuTvydBx4SSzmdXciLP+7j2+1nAAg0+vBo/1aMSozH4FM/ncgSFDyH1waFRYsW8dhjj5GVleVaV1xcjMlk4rPPPuOWW2655D42b95Mz549+eOPP7jiiitc6zUaDTExMVitVpo3b84DDzzA2LFjq/SDL0FBVMTYuC3BfcZgiu0IgKMoD8ufn5G7+Vu5nNJNecohMrvAxju/H+Wt345QZHei0cDwy5ry2IDWhAcY67UWCQqeozJBofybibu5lJQUGjUq3X3m4+NDaGgoKSkpldrHO++8Q7t27UqFBIBZs2Zx7bXX4ufnx4oVK3jooYfIy8vjkUceqXBfVqsVq9XqemyxyNz/onzW0/tI/Wgavs17EtxnNIZGzQjpO5bAHkPI+eMj8naslAGPokqy8ksCwrvrjpFnLZkZtFezUGbc2J6OjeUPFlFzbhUUpk2bxksvvXTRNnv37q3x6xQWFvLRRx/xzDPPlHnu7+u6detGfn4+8+bNu2hQmDNnDjNnzqxxXaLhKDyyicKjW/Bv35fg3iPxMUcSNmgSQZcNJfu3Dyg4kCSBQVxUZr6Nt387wpJ1x8i3OQBoGxXI5P6tGNghSv6qF7XGrU49pKWlkZFx8QFezZs354MPPqjRqYf333+fcePGcfr0aSIiIi7a9vvvv+fGG2+kqKgIo7H87rvyehRiY2Mvul8hXHQ+BHa7AXPiMHR+JX8B2jNPY9nwFfm7V8spCZW50SESgIw8K2/9dpT3ko5RcC4gtIsO4tF+LRnQPgqtG1zJICHFc3jcqYeIiIhL/uIGSExMJDs7m82bN9OjRw8AVq9ejdPpJCEh4ZLbv/POOwwZMqRSr7Vt2zZCQkIqDAkARqPxos8LcVGOYnI3LSNvx0qCet1CYI8h6EMbEzZoIsG97yZ3y3fkbvkeZ1Gu2pU2SJX5pVcfYSIt18rbvx3hvaTjFNpLAkKHmCAe7deK69pHyi9nUWfcqkehKq6//npSU1NZuHCh6/LInj17ui6PPH36NP369eO9996jV69eru0OHTpE69at+eGHH0pdAgnw7bffkpqayuWXX47JZGLlypU8/vjjPP7441U6tSCDGUVNaAy+BHS+jqCeQ/Exl4zFcdqKyNu5ktyNX1Ock6pyheJCdXUYVRSFLSeyeT/pGD/sTMHmKDkd1amxmUf7taJfu0ZuGRDcsSZRPo/rUaiKDz/8kIkTJ9KvXz+0Wi233XYbr7zyiut5u93O/v37KSgoKLXdokWLaNKkCQMGDCizT71ez4IFC/jHP/6Boii0bNmS+fPnM378+Dp/P0Kcp9gKyd20jNwt3+PX5kqCet2KMaolQT1uIrDbDRTs/wPLxq+xJR9Qu1RRRwptDr7dfoYlScfYfeavwdHdmgYz6dqWXNPGPQOC8E4e26PgzqRHQdQ2U1wXgnrdim/zHq51ttQj5O1cSf7utXJaws1U97B6PCOfD/48zqebTpFTaAfA4KNlSJcYRiXG0blJcC1WWXckxHgOr51Hwd1JUBB1RR8RT1CvW/FvexUaHwMASrGdgkPrydu5kqKjW+VqCTdQlcOqrdjJrwfS+GD9cX45kMb5TZuE+HLP5XHc2TOWEH9DHVVaNyQoeA4JCiqRoCDqmtYUgF+7PgR06o8xupVrfXFuOvk7V5G382eKs5NVrLBhu9Rh1eFUWH8kg2Xbz/DjrhRX7wFAn9YRjEqMo2+bRh57LwYJCp5DgoJKJCiI+qSPaEZA5/74t+/rurwSoOjkLgr2/U7BwT9x5KarWGHDU95hVVEUtp7MZtm2M3y/M5m03L8uqQ4PMDK0awwjL4+jWbh/fZZaJyQoeA4JCiqRoCBUofPBr2UC/p3649usOxqtzvWUNeUQhQf/pODgn9jTjqlXYwOkj4jHv10f/Nv1xic4yrXe7Kvn+o5R3NQlhsubh3ls70F5JCh4DgkKKpGgINSmCwjDv/3V+La8HGOTdmg0f90MyJ6d4goN1lN7ZExDLdMa/THFd8XUrDu+zbrhE/TXdPNOWyGFB9eTv/cXsvf9WW83aapvEhQ8hwQFlUhQEO5E62fGt0Uv/Fpdjim+K1r9X5ODOQpyKDy6BevJXRSd3EVx5mkVK/VQGi2G6Nb4NuuOb7PuGKJblerNUYptFB7ZTP7eXyg8tBGluOSUgzcfeiUoeA4JCiqRoCDclUZvxBTfHb9Wl+Pb8jJ0vqUPEI78LIpO7nYFB3vacUAOEaVodRgi4jFEty7pOYjrgs4UUKqJLf0ERUe3nAthu13h4O+8+dArQcFzSFBQiQQF4RE0WoxNOmCK64wptiPGmDauSy7PcxTlYT25G+upXViTD2FPO9bA5mzQ4BMSjTG6NYbo1iVfI5uX/ZwKcyk6vu1cONharcGj3nQolqDgOSQoqESCgvBIOh+MUa0xxnbA1LQTxsbt0Bp8yzQrzs3Ann4c29mj2NOOY0s7hj3jJDjs5ezUc2gMfuhDovEJbXyux6AVxqhWaC/oLYCSAGVLPoD11B4Kj27BlnKoxmM9vOlQLEHBc0hQUIkEBeEVNFoMkS0wxnbE1KQ9+kbN0P9t1P7fKU4HxVlnsKUdpzgnFUduOsWW9JKvuWk483Nwh1MYGoMv+pAYfEKi8QmJOffvkq86/+Byt3HardhSD5cEg+SD2JIP1MkcFd50KJag4DkkKKhEgoLwVhqDL/rwOAwRcegj4jFExKOPiCsz1uFCisOOIzeD4tzz4SEDxVaI01aAYivCaS8697iw5Ku9CMVWhGIvAq0OtNqSAYIaXclX7fmvJes1Pka0voHofIPQ+plLvvoGofMzo/UNQusXVLLO6HfROh15WdizzmDPPHUuGBzAnn4CnI7a/BirzVMO1xIUPIcEBZVIUBANjS4gFH1EPPqwWHyCItAFhuMTFI4uMAJdQEipyzPV5sjPxp51huKsM399zTxDcXYyiq1Q7fIuylMO1xIUPIdX3z1SCOE+HHmZOPIyKTq6peyTWh06/1B8gsJKgkNgOD4BoWgMvmgNJjT6v75qDCa0Bt9zz/m6LjNUnA5wOku+Ko5zjx0oTmfJV0cxzkILjkILzsIcHAUWnAUWHIU5575acBbk4CjIdvswIIS7kaAghKhbTgeO3DQcuWnAvqptq9WB04k7jG8QoqGSoCCEcF9uMjbAXVzYpe8ppyKEZ3OfE4dCCCGEcDsSFIQQQghRIQkKQgghhKiQjFEQQggPdanLEGUMg6gN0qMghBBCiApJUBBCCCFEhSQoCCGEEKJCMkZBCCG8VHljGGTcgqgq6VEQQgghRIUkKAghhBCiQnLqQQghGhC5pFJUlfQoCCGEEKJCEhSEEEIIUSEJCkIIIYSokIxREEII4SK3shYXkh4FIYQQQlRIgoIQQgghKiRBQQghhBAVkjEKQgghKnSpeRdAxjF4O+lREEIIIUSFPDYovPDCC1xxxRX4+fkRHBxcqW0URWHGjBlER0fj6+tL//79OXjwYKk2mZmZjBw5kqCgIIKDgxk3bhx5eXl18A6EEEII9+exQcFms3HHHXfw4IMPVnqbuXPn8sorr7Bw4ULWr1+Pv78/AwcOpKioyNVm5MiR7N69m5UrV/Ldd9/x66+/MmHChLp4C0II4RU0Gk2pRXgZxcMtXrxYMZvNl2zndDqVqKgoZd68ea512dnZitFoVD7++GNFURRlz549CqBs3LjR1ebHH39UNBqNcvr06UrXlJOTowCyyCKLLLLI4tZLTk7OJX+neWyPQlUdPXqUlJQU+vfv71pnNptJSEggKSkJgKSkJIKDg+nZs6erTf/+/dFqtaxfv77eaxZCCCHU1mCuekhJSQEgMjKy1PrIyEjXcykpKTRq1KjU8z4+PoSGhrralMdqtWK1Wl2Pc3JyaqtsIYQQos4olbhixa16FKZNm1bmXNeFy759+9Qus4w5c+ZgNptdS9OmTdUuSQghhLik3NzcS7Zxqx6Fxx57jDFjxly0TfPmzau176ioKABSU1OJjo52rU9NTaVr166uNmfPni21XXFxMZmZma7tyzN9+nSmTJnieux0OsnMzCQsLKxeBvZYLBZiY2M5efIkQUFBdf56DZ183vVPPvP6JZ93/avvz1xRFHJzc4mJiblkW7cKChEREURERNTJvps1a0ZUVBSrVq1yBQOLxcL69etdV04kJiaSnZ3N5s2b6dGjBwCrV6/G6XSSkJBQ4b6NRiNGo7HUuspeslmbgoKC5D91PZLPu/7JZ16/5POuf/X5mZvN5kq1c6tTD1Vx4sQJtm3bxokTJ3A4HGzbto1t27aVmvOgbdu2fPXVV0DJ5TuTJ0/m+eefZ9myZezcuZNRo0YRExPD0KFDAWjXrh2DBg1i/PjxbNiwgT/++IOJEycyfPjwSqUuIYQQwtu4VY9CVcyYMYMlS5a4Hnfr1g2ANWvW0LdvXwD2799famDhE088QX5+PhMmTCA7O5urrrqK5cuXYzKZXG0+/PBDJk6cSL9+/dBqtdx222288sor9fOmhBBCCDfjsUHh3Xff5d13371omwtHc2o0GmbNmsWsWbMq3CY0NJSPPvqoNkqsN0ajkWeffbbM6Q9RN+Tzrn/ymdcv+bzrnzt/5hqlMtdGCCGEEKJB8tgxCkIIIYSoexIUhBBCCFEhCQpCCCGEqJAEBSGEEEJUSIKCh3rhhRe44oor8PPzq/TkToqiMGPGDKKjo/H19aV///4cPHiwbgv1EpmZmYwcOZKgoCCCg4MZN25cqTk7ytO3b98yU5A/8MAD9VSx51mwYAHx8fGYTCYSEhLYsGHDRdt/9tlntG3bFpPJRKdOnfjhhx/qqVLvUJXP+9133y3zs/z3y8rFxf3666/cdNNNxMTEoNFo+Prrry+5zdq1a+nevTtGo5GWLVte8iq/uiRBwUPZbDbuuOMO16ySlTF37lxeeeUVFi5cyPr16/H392fgwIEUFRXVYaXeYeTIkezevZuVK1fy3Xff8euvvzJhwoRLbjd+/HiSk5Ndy9y5c+uhWs/zySefMGXKFJ599lm2bNlCly5dGDhwYJkp1c9bt24dI0aMYNy4cWzdupWhQ4cydOhQdu3aVc+Ve6aqft5QMmPg33+Wjx8/Xo8Ve7b8/Hy6dOnCggULKtX+6NGjDB48mGuuuYZt27YxefJk7rvvPn766ac6rrQCl7wRtXBrixcvVsxm8yXbOZ1OJSoqSpk3b55rXXZ2tmI0GpWPP/64Div0fHv27FEAZePGja51P/74o6LRaJTTp09XuF2fPn2URx99tB4q9Hy9evVSHn74Yddjh8OhxMTEKHPmzCm3/Z133qkMHjy41LqEhATl/vvvr9M6vUVVP+/KHmfEpQHKV199ddE2TzzxhNKhQ4dS64YNG6YMHDiwDiurmPQoNBBHjx4lJSWF/v37u9aZzWYSEhJISkpSsTL3l5SURHBwMD179nSt69+/P1qtlvXr11902w8//JDw8HA6duzI9OnTKSgoqOtyPY7NZmPz5s2lfja1Wi39+/ev8GczKSmpVHuAgQMHys9yJVTn8wbIy8sjLi6O2NhYbr75Znbv3l0f5TZI7vbz7bEzM4qqSUlJASAyMrLU+sjISNdzonwpKSk0atSo1DofHx9CQ0Mv+tndddddxMXFERMTw44dO3jyySfZv38/X375ZV2X7FHS09NxOBzl/mxWdFv5lJQU+Vmupup83m3atGHRokV07tyZnJwcXn75Za644gp2795NkyZN6qPsBqWin2+LxUJhYSG+vr71Wo/0KLiRadOmlRkwdOFS0X9kUXV1/XlPmDCBgQMH0qlTJ0aOHMl7773HV199xeHDh2vxXQhR9xITExk1ahRdu3alT58+fPnll0RERPDGG2+oXZqoB9Kj4EYee+wxxowZc9E2zZs3r9a+o6KiAEhNTSU6Otq1PjU11XXb7Yamsp93VFRUmUFexcXFZGZmuj7Xyjh/q/JDhw7RokWLKtfrrcLDw9HpdKSmppZan5qaWuHnGxUVVaX24i/V+bwvpNfr6datG4cOHaqLEhu8in6+g4KC6r03ASQouJWIiAgiIiLqZN/NmjUjKiqKVatWuYKBxWJh/fr1VbpywptU9vNOTEwkOzubzZs306NHDwBWr16N0+l0/fKvjG3btgGUCmoCDAYDPXr0YNWqVa5bvjudTlatWsXEiRPL3SYxMZFVq1YxefJk17qVK1eSmJhYDxV7tup83hdyOBzs3LmTG264oQ4rbbgSExPLXO6r6s+3KkMoRY0dP35c2bp1qzJz5kwlICBA2bp1q7J161YlNzfX1aZNmzbKl19+6Xr84osvKsHBwco333yj7NixQ7n55puVZs2aKYWFhWq8BY8yaNAgpVu3bsr69euV33//XWnVqpUyYsQI1/OnTp1S2rRpo6xfv15RFEU5dOiQMmvWLGXTpk3K0aNHlW+++UZp3ry5cvXVV6v1Ftza0qVLFaPRqLz77rvKnj17lAkTJijBwcFKSkqKoiiKcs899yjTpk1ztf/jjz8UHx8f5eWXX1b27t2rPPvss4per1d27typ1lvwKFX9vGfOnKn89NNPyuHDh5XNmzcrw4cPV0wmk7J792613oJHyc3NdR2jAWX+/PnK1q1blePHjyuKoijTpk1T7rnnHlf7I0eOKH5+fsrUqVOVvXv3KgsWLFB0Op2yfPlyVeqXoOChRo8erQBlljVr1rjaAMrixYtdj51Op/LMM88okZGRitFoVPr166fs37+//ov3QBkZGcqIESOUgIAAJSgoSBk7dmypUHb06NFSn/+JEyeUq6++WgkNDVWMRqPSsmVLZerUqUpOTo5K78D9vfrqq0rTpk0Vg8Gg9OrVS/nzzz9dz/Xp00cZPXp0qfaffvqp0rp1a8VgMCgdOnRQvv/++3qu2LNV5fOePHmyq21kZKRyww03KFu2bFGhas+0Zs2aco/X5z/j0aNHK3369CmzTdeuXRWDwaA0b9681LG8vsltpoUQQghRIbnqQQghhBAVkqAghBBCiApJUBBCCCFEhSQoCCGEEKJCEhSEEEIIUSEJCkIIIYSokAQFIYQQQlRIgoIQQgghKiRBQQghhBAVkqAghKg3GRkZNGrUiGPHjqldikvfvn1L3Vxq+PDh/Otf/1KvICHcjAQFIUS9eeGFF7j55puJj49Xu5QKPf3007zwwgvk5OSoXYoQbkGCghCiXhQUFPDOO+8wbty4Wt+3zWartX117NiRFi1a8MEHH9TaPoXwZBIUhBDVsmHDBvr27Yuvry9t27Zl06ZNvPnmmwwZMqTc9j/88ANGo5HLL7+81Pq+ffsyadIkJk+eTEhICJGRkbz11lvk5+czduxYAgMDadmyJT/++GOpbSZOnMjkyZMJDw9n4MCBACxfvpyrrrqK4OBgwsLCuPHGGzl8+LBru/z8fEaNGkVAQADR0dEVnmK46aabWLp0aU0/IiG8ggQFIUSV/fnnn/Tp04fBgwezY8cO2rVrx6xZs3jppZeYOXNmudv89ttv9OjRo9znlixZQnh4OBs2bGDSpEk8+OCD3HHHHVxxxRVs2bKFAQMGcM8991BQUFBqG4PBwB9//MHChQuBkiAwZcoUNm3axKpVq9Bqtdxyyy04nU4Apk6dyi+//MI333zDihUrWLt2LVu2bClTT69evdiwYQNWq7WmH5UQnk+1G1wLITxWYmKics8997gef/LJJ4pWq1VuueWWCre5+eablXvvvbfM+j59+ihXXXWV63FxcbHi7+9fav/JyckKoCQlJbm26dat2yXrTEtLUwBl586dSm5urmIwGJRPP/3U9XxGRobi6+urPProo6W22759uwIox44du+RrCOHtpEdBCFElp06dIikpiQceeMC1zsfHB0VRKuxNACgsLMRkMpX7XOfOnV3/1ul0hIWF0alTJ9e6yMhIAM6ePetaV17vxMGDBxkxYgTNmzcnKCjINWjyxIkTHD58GJvNRkJCgqt9aGgobdq0KbMfX19fgFI9GEI0VD5qFyCE8Cx79+4FoHv37q51+/fvp1evXqV+uV8oPDycrKyscp/T6/WlHms0mlLrNBoNgOsUAoC/v3+Z/dx0003ExcXx1ltvERMTg9PppGPHjlUe7JiZmQlARERElbYTwhtJj4IQokpycnLQ6XSuX96ZmZm8/PLL+Pn5XXS7bt26sWfPnjqrKyMjg/379/P000/Tr18/2rVrVyqYtGjRAr1ez/r1613rsrKyOHDgQJl97dq1iyZNmhAeHl5n9QrhKSQoCCGqpGvXrjgcDubOncu+ffsYMWIE8fHx7Nmzh+PHj1e43cCBA9m9e3eFvQo1FRISQlhYGG+++SaHDh1i9erVTJkyxfV8QEAA48aNY+rUqaxevZpdu3YxZswYtNqyh8HffvuNAQMG1EmdQngaCQpCiCpp2bIls2bN4r///S/dunUjJiaGFStW0LhxYwYNGlThdp06daJ79+58+umndVKXVqtl6dKlbN68mY4dO/KPf/yDefPmlWozb948evfuzU033UT//v256qqryox1KCoq4uuvv2b8+PF1UqcQnkajKIqidhFCiIbh+++/Z+rUqezatavcv+Tdweuvv85XX33FihUr1C5FCLcggxmFEPVm8ODBHDx4kNOnTxMbG6t2OeXS6/W8+uqrapchhNuQHgUhhBBCVMg9+/6EEEII4RYkKAghhBCiQhIUhBBCCFEhCQpCCCGEqJAEBSGEEEJUSIKCEEIIISokQUEIIYQQFZKgIIQQQogKSVAQQgghRIUkKAghhBCiQv8PeH68XeAxyCQAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import labellines as ll\n", + "\n", + "idx = 67#33#50\n", + "Z = Zvals[idx] # 6um\n", + "mask = allmask[idx]\n", + "\n", + "# need to double check all this\n", + "extent=np.asarray([fsmprops['alphamin'], fsmprops['alphamax'], fsmprops['betamin'], fsmprops['betamax']]) * 1e3\n", + "#extent = np.asarray([alphavals.min(), alphavals.max(), alphavals.min(), alphavals.max()]) * 1e3\n", + "\n", + "plt.imshow(mask, extent=extent, cmap='Greys_r')\n", + "#plt.colorbar()\n", + "plt.ylabel('$\\\\beta\\ (\\mathrm{mrad})$')\n", + "plt.xlabel('$\\\\alpha\\ (\\mathrm{mrad})$')\n", + "plt.title(f'accessible space at Z={Z*1e6:.1f}' + '$\\mathrm{\\mu m}$')\n", + "\n", + "\n", + "r = 0.85 # mrad\n", + "theta = np.linspace(0, 2*np.pi, num=100)\n", + "x = r * np.cos(theta)\n", + "y = r * np.sin(theta)\n", + "plt.plot(x, y, label=f'r={r}mrad')\n", + "plt.legend()\n", + "#ll.labelLines()\n", + "\n", + "print(alphabetaZ_to_ABC(alpha,beta,6e-6,a,b))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4c0b88f4", + "metadata": {}, + "outputs": [], + "source": [ + "masksum = np.sum(allmask.astype(float), axis=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c20c7a36", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1.4e-05, 8.000000000000001e-06, -4e-06)\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# need to double check all this\n", + "extent=np.asarray([fsmprops['alphamin'], fsmprops['alphamax'], fsmprops['betamin'], fsmprops['betamax']]) * 1e3\n", + "#extent = np.asarray([alphavals.min(), alphavals.max(), alphavals.min(), alphavals.max()]) * 1e3\n", + "\n", + "plt.imshow(masksum > 1e-16, extent=extent, cmap='Greys_r')\n", + "#plt.colorbar()\n", + "plt.ylabel('$\\\\beta\\ (\\mathrm{mrad})$')\n", + "plt.xlabel('$\\\\alpha\\ (\\mathrm{mrad})$')\n", + "plt.title(f'total accessible space')\n", + "\n", + "\n", + "r = 0.85 # mrad\n", + "theta = np.linspace(0, 2*np.pi, num=100)\n", + "x = r * np.cos(theta)\n", + "y = r * np.sin(theta)\n", + "plt.plot(x, y, label=f'r={r}mrad')\n", + "plt.legend()\n", + "#ll.labelLines()\n", + "\n", + "print(alphabetaZ_to_ABC(alpha,beta,6e-6,a,b))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f6887f2", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/apps/fsmCtrl/iPacket.hpp b/apps/fsmCtrl/iPacket.hpp new file mode 100644 index 000000000..37c941430 --- /dev/null +++ b/apps/fsmCtrl/iPacket.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +class IPacket +{ +public: + IPacket() { } + virtual ~IPacket() { } + + virtual bool FindPacketStart(const uint8_t* Buffer, const size_t BufferLen, size_t& Offset) const = 0; + virtual bool FindPacketEnd(const uint8_t* Buffer, const size_t BufferLen, size_t& Offset) const = 0; + virtual size_t HeaderLen() const = 0; + virtual size_t FooterLen() const = 0; + virtual size_t PayloadOffset() const = 0; + virtual size_t MaxPayloadLength() const = 0; + virtual size_t PayloadLen(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const = 0; + virtual bool IsValid(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const = 0; + virtual uint64_t SerialNum() const = 0; + virtual uint64_t PayloadType(const uint8_t* Buffer, const size_t PacketStartPos, const size_t PacketEndPos) const = 0; + virtual bool DoesPayloadTypeMatch(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos, const uint32_t CmdType) const = 0; + virtual size_t MakePacket(uint8_t* Buffer, const size_t BufferCount, const void* Payload, const uint16_t PayloadType, const size_t PayloadLen) const = 0; +}; diff --git a/apps/fsmCtrl/linux_pinout_client_socket.hpp b/apps/fsmCtrl/linux_pinout_client_socket.hpp new file mode 100755 index 000000000..3bab3622e --- /dev/null +++ b/apps/fsmCtrl/linux_pinout_client_socket.hpp @@ -0,0 +1,241 @@ +/// \file +/// $Source: /raincloud/src/projects/include/client_socket/linux_pinout_client_socket.hpp,v $ +/// $Revision: 1.9 $ +/// $Date: 2009/11/14 00:20:04 $ +/// $Author: steve $ + +#pragma once + +#include /* Standard input/output definitions */ +#include /* String function definitions */ + +#include /* UNIX standard function definitions */ +#include /* File control definitions */ +#include /* Error number definitions */ +#include /* POSIX terminal control definitions */ +#include /* ioctl() */ +#include /* FIONREAD on cygwin */ +#include /* struct sockaddr_in */ +#include +#include +#include +#include + +// #include "format/formatf.h" + +#include "IUart.h" + +#define HOST_NAME_SIZE 255 + +namespace MagAOX +{ + namespace app + { + + class linux_pinout_client_socket : public IUart + { + public: + linux_pinout_client_socket() : IUart(), hSocket(-1) {} + virtual ~linux_pinout_client_socket() {} + + virtual int init(const int HostPort, const char *HostName) + { + struct hostent *pHostInfo; /* holds info about a machine */ + struct sockaddr_in Address; /* Internet socket address stuct */ + long nHostAddress; + char strHostName[HOST_NAME_SIZE]; + int nHostPort = 20339; + + if (-1 != hSocket) + { + deinit(); + } + + strcpy(strHostName, "localhost"); + + if (NULL != HostName) + { + strncpy(strHostName, HostName, HOST_NAME_SIZE); + strHostName[HOST_NAME_SIZE] = '\0'; + } + if (0 != HostPort) + { + nHostPort = HostPort; + } + + MagAOXAppT::log("linux_pinout_client_socket::init(): Making a socket"); + /* make a socket */ + hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + + if (hSocket == -1) + { + MagAOXAppT::log({__FILE__, __LINE__, "linux_pinout_client_socket::init(): Could not make a socket"}); + return (errno); + } + else + { + std::ostringstream oss; + oss << "linux_pinout_client_socket::init(): Socket handle: " << hSocket; + MagAOXAppT::log(oss.str()); + } + + /* get IP address from name */ + pHostInfo = gethostbyname(strHostName); + if (NULL == pHostInfo) + { + std::ostringstream oss; + oss << "linux_pinout_client_socket::init(): Could not gethostbyname(): " << strerror(errno); + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + return (errno); + } + /* copy address into long */ + memcpy(&nHostAddress, pHostInfo->h_addr, pHostInfo->h_length); + + /* fill address struct */ + Address.sin_addr.s_addr = nHostAddress; + Address.sin_port = htons(nHostPort); + Address.sin_family = AF_INET; + + std::ostringstream oss; + oss << "linux_pinout_client_socket::init(): Connecting to " << strHostName << " on port " << nHostPort; + MagAOXAppT::log(oss.str()); + + int isConnected = connect(hSocket, (struct sockaddr *)&Address, sizeof(Address)); + /* connect to host */ + if (isConnected == -1) + { + std::ostringstream oss; + oss << "linux_pinout_client_socket::init(): Could not connect to host: " << strerror(errno); + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + return (errno); + } + else + { + MagAOXAppT::log("linux_pinout_client_socket::init(): Connected."); + } + + return (IUartOK); + } + + virtual void deinit() + { + if (-1 != hSocket) + { + MagAOXAppT::log("linux_pinout_client_socket::deinit(): Closing socket"); + + close(hSocket); + + hSocket = -1; + } + } + + virtual bool dataready() const + { + fd_set sockset; + struct timeval nowait; + memset((char *)&nowait, 0, sizeof(nowait)); + if (-1 == hSocket) + { + return (false); + } // open? + FD_ZERO(&sockset); + FD_SET(hSocket, &sockset); + int result = select(hSocket + 1, &sockset, NULL, NULL, &nowait); + if (result < 0) + { + } //"You have an error" + else if (result == 1) + { + if (FD_ISSET(hSocket, &sockset)) // The socket has data. For good measure, it's not a bad idea to test further + { + return (true); + } + } + return (false); + } + + virtual char getcqq() + { + char c = 0; + + if (-1 != hSocket) + { + int numbytes = recv(hSocket, &c, 1, 0); + if (1 != numbytes) + { + std::ostringstream oss; + oss << "linux_pinout_client_socket::getcqq(): read from socket error (" << numbytes << " bytes gave: " << errno << ")"; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + + deinit(); + + return (0); + } + } + else + { + MagAOXAppT::log("linux_pinout_client_socket::getcqq(): read on uninitialized socket; please open socket!"); + } + + return (c); + } + + virtual char putcqq(char c) + { + // std::ostringstream oss; + // oss << "Char: " << std::setw(2) << std::setfill('0') << std::hex << static_cast(c); + // MagAOXAppT::log(oss.str()); + + if (-1 != hSocket) + { + int numbytes = send(hSocket, &c, 1, 0); + + if (1 != numbytes) + { + std::ostringstream oss; + oss << "linux_pinout_client_socket::putcqq(): write from socket error (" << numbytes << " bytes gave: " << errno << ")"; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + + deinit(); + + return (0); + } + } + else + { + MagAOXAppT::log({__FILE__, __LINE__, "linux_pinout_client_socket::putcqq(): write on uninitialized socket; please open socket!"}); + } + + return (c); + } + + virtual void flushoutput() + { + if (-1 != hSocket) + { + fsync(hSocket); + } + else + { + MagAOXAppT::log({__FILE__, __LINE__, "linux_pinout_client_socket::putcqq(): fflush on uninitialized socket; please open socket!"}); + } + } + + virtual void purgeinput() + { + //~ if(dataready) + } + + virtual bool connected() const + { + return (-1 != hSocket); + } + + virtual bool isopen() const { return (connected()); } + + public: + int hSocket; /* handle to socket */ + }; + + } // namespace app +} // namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/readme.md b/apps/fsmCtrl/readme.md new file mode 100644 index 000000000..6ddcd7b51 --- /dev/null +++ b/apps/fsmCtrl/readme.md @@ -0,0 +1,57 @@ +Adding An application {#page_module_appadd} +========== + +[TOC] + +------------------------------------------------------------------------ + +# Introduction + +This document describes how to add an application to the MagAO-X source code. + +# 1. Code + +Start by copying the folder `template` to a new folder with the name of the app, e.g. `hardwareCtrl`. + +The three basic files for any application are the header, the main program file, and the Makefile. In the new `hardwareCtrl` folder, rename `template.hpp` and `template.cpp` to `hardwareCtrl.hpp` and `hardwareCtrl.cpp` (substituting the appropriate name for the new application for `hardwareCtrl`). + +Now in `Makefile`, `hardwareCtrl.hpp` and `hardwareCtrl.cpp`, change `template` to `hardwareCtrl` (you should be able to use find-all and replace). + +If all replacement is done correctly, the application will build with only warnings if you type `make` on the command line. + +Next edit the code in the `.hpp` file to implement the application. You can also edit the Makefile adding additional libraries, or perhaps another header. You will typically not need to edit any code in the `.cpp` file other than replacing `template` as above. + +# 2. Build System Integration + +To cause the new app to be built, add it to the appropriate list of apps in the top level Makefile. Pay attention to which machine you expect the app to run on. + +# 3. Tests + +The file `tests/template_test.cpp` should have its name changed to (example) `tests/hradwareCtrl_test.cpp`. Implement any unit tests in this file. + +Add the `*_test.cpp` file to the top level `tests/testMagAOX.cpp` file, and to the `tests/Makefile` `TEST_INCLUDES` dependency list. + +# 4. Software Documentation + +Document your code with doxygen. Be sure that `template` was changed to the application name in all documentation blocks in the source code, including the group definitions and `\ingroup` directives. + +# 5. Program Documentation + +Rename and edit the file `doc/template.md`, in the above example it should become `doc/hardwareCtrl.md`. Change all instances of `template` to the application name, update the text appropriately, including the app specific options and INDI section. + +# 6. Documentation System Integration + +Next, follow all of these steps to integrate the documentation: +- in the file `libMagAOX/doc/libMagAOX_doxygen.in` add the application folder to the INPUT directive +- in the same file, add the application/doc folder to the EXCLUDE directive +- in the file 'apps/doc/magaox_apps_doxygen.in` add the application `xxxx/doc/` folder to the `INPUT` directive + +Now running `make doc` in the top level should build all documentation with your new application integrated into it like all the others. + +# 7. Final Steps + +Delete this `readme.md` from the new application folder. + +Commit all of the new files. + +Use your new application to find planets. diff --git a/apps/fsmCtrl/set_vals_INDI.drawio.svg b/apps/fsmCtrl/set_vals_INDI.drawio.svg new file mode 100644 index 000000000..8e9907e3c --- /dev/null +++ b/apps/fsmCtrl/set_vals_INDI.drawio.svg @@ -0,0 +1,1034 @@ + + + + + + + + + +
+
+
+ + fsmCtrl + +
+
+
+
+ + fsmCtrl + +
+
+ + + + +
+
+
+ + m_dac3_max + +
+
+
+
+ + m_dac3_max + +
+
+ + + + + + +
+
+
+
+
+ + dacs + + + [ + + + 3 + + + ] = {dac1 + + + , + + + dac2 + + + , + + + convert_to_dac( + + + target) + + + } + +
+
+
+
+
+
+
+ + dacs[3] = {dac1, dac2, convert_to_dac(target)... + +
+
+ + + + + + +
+
+
+
+
+ + m_daci_min <= dacs[i] + +
+
+ + && + +
+
+ + dacs[i] <= m_daci_max + +
+
+
+
+
+
+ + m_daci_min <= dacs[i]... + +
+
+ + + + + + +
+
+
+
+
+ + send_to_fsm(dacs) + +
+
+ + returns + + + DacSetpoints + +
+
+
+
+
+
+ + send_to_fsm(dacs)... + +
+
+ + + + +
+
+
+ + m_dac3_min + +
+
+
+
+ + m_dac3_min + +
+
+ + + + +
+
+
+
+
+ + convert DacSepoints to m_inputType val_1, val_2, val_3 + +
+
+
+
+
+
+ + convert DacSepoints to m_inputType val_1,... + +
+
+ + + + +
+
+
+ + m_dac1 + +
+
+
+
+ + m_dac1 + +
+
+ + + + +
+
+
+ m_dac3 +
+
+
+
+ + m_dac3 + +
+
+ + + + +
+
+
+ m_dac2 +
+
+
+
+ + m_dac2 + +
+
+ + + + +
+
+
+ + m_inputType + +
+
+
+
+ + m_inputType + +
+
+ + + + +
+
+
+ + m_dac1_max + +
+
+
+
+ + m_dac1_max + +
+
+ + + + +
+
+
+ + m_dac2_max + +
+
+
+
+ + m_dac2_max + +
+
+ + + + +
+
+
+ + m_dac1_min + +
+
+
+
+ + m_dac1_min + +
+
+ + + + +
+
+
+ + m_dac2_min + +
+
+
+
+ + m_dac2_min + +
+
+ + + + + + + + + + + + +
+
+
+
+
+ + new inputType in (dacs, voltages, angles)? + +
+
+
+
+
+
+
+ + new inputType in (dacs, voltages, angles)... + +
+
+ + + + +
+
+
+ Yes +
+
+
+
+ + Yes + +
+
+ + + + +
+
+
+ No +
+
+
+
+ + No + +
+
+ + + + + + + + + + +
+
+
+ Log error +
+
+
+
+ + Log error + +
+
+ + + + + + + + + + +
+
+
+ + fsmCtrl.conf + +
+
+
+
+ + fsmCtrl.conf + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ a +
+
+
+
+ + a + +
+
+ + + + +
+
+
+ b +
+
+
+
+ + b + +
+
+ + + + +
+
+
+ v +
+
+
+
+ + v + +
+
+ + + + +
+
+
+ dac1_min +
+
+
+
+ + dac1_min + +
+
+ + + + +
+
+
+ dac1_max +
+
+
+
+ + dac1_max + +
+
+ + + + +
+
+
+ dac2_min +
+
+
+
+ + dac2_min + +
+
+ + + + +
+
+
+ dac2_max +
+
+
+
+ + dac2_max + +
+
+ + + + +
+
+
+ dac3_min +
+
+
+
+ + dac3_min + +
+
+ + + + +
+
+
+ dac3_max +
+
+
+
+ + dac3_max + +
+
+ + + + +
+
+
+ --- +
+
+
+
+ + --- + +
+
+ + + + + + +
+
+
+ + + INDI properties + + +
+
+
+
+ + INDI properties + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ fsmCtrl.input.type +
+ (dacs, voltages, angles) +
+
+
+
+ + fsmCtrl.input.type... + +
+
+ + + + +
+
+
+ fsmCtrl.val_1.current +
+
+
+
+ + fsmCtrl.val_1.current + +
+
+ + + + +
+
+
+ fsmCtrl.val_1.target +
+
+
+
+ + fsmCtrl.val_1.target + +
+
+ + + + +
+
+
+ fsmCtrl.val_2.current +
+
+
+
+ + fsmCtrl.val_2.current + +
+
+ + + + +
+
+
+ fsmCtrl.val_2.target +
+
+
+
+ + fsmCtrl.val_2.target + +
+
+ + + + +
+
+
+ fsmCtrl.val_3.current +
+
+
+
+ + fsmCtrl.val_3.current + +
+
+ + + + +
+
+
+ fsmCtrl.val_3.target +
+
+
+
+ + fsmCtrl.val_3.target + +
+
+ + + + +
+
+
+ fsmCtrl.conversion_factors.a +
+
+
+
+ + fsmCtrl.conversion_factors.a + +
+
+ + + + +
+
+
+ fsmCtrl.conversion_factors.b +
+
+
+
+ + fsmCtrl.conversion_factors.b + +
+
+ + + + +
+
+
+ fsmCtrl.conversion_factors.v +
+
+
+
+ + fsmCtrl.conversion_factors.v + +
+
+ + + + +
+
+
+ fsmCtrl.dac_1.min +
+
+
+
+ + fsmCtrl.dac_1.min + +
+
+ + + + +
+
+
+ fsmCtrl.dac_1.max +
+
+
+
+ + fsmCtrl.dac_1.max + +
+
+ + + + +
+
+
+ fsmCtrl.dac_2.min +
+
+
+
+ + fsmCtrl.dac_2.min + +
+
+ + + + +
+
+
+ fsmCtrl.dac_2.max +
+
+
+
+ + fsmCtrl.dac_2.max + +
+
+ + + + +
+
+
+ fsmCtrl.dac_3.min +
+
+
+
+ + fsmCtrl.dac_3.min + +
+
+ + + + +
+
+
+ fsmCtrl.dac_3.max +
+
+
+
+ + fsmCtrl.dac_3.max + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+ + stateCodes + + :: + + READY + +
+
+
+
+
+ + stateCodes::READY + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/apps/fsmCtrl/set_vals_shmim.drawio.svg b/apps/fsmCtrl/set_vals_shmim.drawio.svg new file mode 100644 index 000000000..90ccfa866 --- /dev/null +++ b/apps/fsmCtrl/set_vals_shmim.drawio.svg @@ -0,0 +1,1289 @@ + + + + + + + + + +
+
+
+ + INDI properties + +
+
+
+
+ + INDI properties + +
+
+ + + + +
+
+
+ fsmCtrl.input.type +
+ (dacs, voltages, angles) +
+
+
+
+ + fsmCtrl.input.type... + +
+
+ + + + +
+
+
+ fsmCtrl.val_1.current +
+
+
+
+ + fsmCtrl.val_1.current + +
+
+ + + + +
+
+
+ fsmCtrl.val_1.target +
+
+
+
+ + fsmCtrl.val_1.target + +
+
+ + + + +
+
+
+ fsmCtrl.val_2.current +
+
+
+
+ + fsmCtrl.val_2.current + +
+
+ + + + +
+
+
+ fsmCtrl.val_2.target +
+
+
+
+ + fsmCtrl.val_2.target + +
+
+ + + + +
+
+
+ fsmCtrl.val_3.current +
+
+
+
+ + fsmCtrl.val_3.current + +
+
+ + + + +
+
+
+ fsmCtrl.val_3.target +
+
+
+
+ + fsmCtrl.val_3.target + +
+
+ + + + + +
+
+
+ fsmCtrl.conversion_factors.a +
+
+
+
+ + fsmCtrl.conversion_factors.a + +
+
+ + + + +
+
+
+ fsmCtrl.conversion_factors.b +
+
+
+
+ + fsmCtrl.conversion_factors.b + +
+
+ + + + +
+
+
+ fsmCtrl.conversion_factors.v +
+
+
+
+ + fsmCtrl.conversion_factors.v + +
+
+ + + + +
+
+
+ fsmCtrl.dac_1.min +
+
+
+
+ + fsmCtrl.dac_1.min + +
+
+ + + + +
+
+
+ fsmCtrl.dac_1.max +
+
+
+
+ + fsmCtrl.dac_1.max + +
+
+ + + + +
+
+
+ fsmCtrl.dac_2.min +
+
+
+
+ + fsmCtrl.dac_2.min + +
+
+ + + + +
+
+
+ fsmCtrl.dac_2.max +
+
+
+
+ + fsmCtrl.dac_2.max + +
+
+ + + + +
+
+
+ fsmCtrl.dac_3.min +
+
+
+
+ + fsmCtrl.dac_3.min + +
+
+ + + + +
+
+
+ fsmCtrl.dac_3.max +
+
+
+
+ + fsmCtrl.dac_3.max + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + + + +
+
+
+ + fsmCtrl + +
+
+
+
+ + fsmCtrl + +
+
+ + + + +
+
+
+ + m_dac3_max + +
+
+
+
+ + m_dac3_max + +
+
+ + + + + + +
+
+
+
+
+ + dacs + + + [ + + + 3 + + + ] = { + +
+
+ + convert_to_dac(val1), + +
+
+ + convert_to_dac(val2 + + + ) + + + , + +
+
+ + convert_to_dac(val3 + + + ) + + + } + +
+
+
+
+
+
+
+ + dacs[3] = {... + +
+
+ + + + + + +
+
+
+
+
+ + m_daci_min <= dacs[i] + +
+
+ + && + +
+
+ + dacs[i] <= m_daci_max + +
+
+
+
+
+
+ + m_daci_min <= dacs[i]... + +
+
+ + + + + + +
+
+
+
+
+ + send_to_fsm(dacs) + +
+
+ + returns + + + DacSetpoints + +
+
+
+
+
+
+ + send_to_fsm(dacs)... + +
+
+ + + + +
+
+
+ + m_dac3_min + +
+
+
+
+ + m_dac3_min + +
+
+ + + + +
+
+
+
+
+ + convert DacSepoints to m_inputType val_1, val_2, val_3 + +
+
+
+
+
+
+ + convert DacSepoints to m_inputType val_1,... + +
+
+ + + + +
+
+
+ + m_dac1 + +
+
+
+
+ + m_dac1 + +
+
+ + + + +
+
+
+ m_dac3 +
+
+
+
+ + m_dac3 + +
+
+ + + + +
+
+
+ m_dac2 +
+
+
+
+ + m_dac2 + +
+
+ + + + +
+
+
+ + m_inputType + +
+
+
+
+ + m_inputType + +
+
+ + + + +
+
+
+ + m_dac1_max + +
+
+
+
+ + m_dac1_max + +
+
+ + + + +
+
+
+ + m_dac2_max + +
+
+
+
+ + m_dac2_max + +
+
+ + + + +
+
+
+ + m_dac1_min + +
+
+
+
+ + m_dac1_min + +
+
+ + + + +
+
+
+ + m_dac2_min + +
+
+
+
+ + m_dac2_min + +
+
+ + + + + + + + + + +
+
+
+
+
+ + new inputType in (dacs, voltages, angles)? + +
+
+
+
+
+
+
+ + new inputType in (dacs, voltages, angles)... + +
+
+ + + + +
+
+
+ Yes +
+
+
+
+ + Yes + +
+
+ + + + +
+
+
+ No +
+
+
+
+ + No + +
+
+ + + + + + + + + + +
+
+
+ Log error +
+
+
+
+ + Log error + +
+
+ + + + + + + + + + + + +
+
+
+ + fsmCtrl.conf + +
+
+
+
+ + fsmCtrl.conf + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ shmimName=fsm +
+
+
+
+ + shmimName=fsm + +
+
+ + + + +
+
+
+ width=3 +
+
+
+
+ + width=3 + +
+
+ + + + +
+
+
+ height=1 +
+
+
+
+ + height=1 + +
+
+ + + + +
+
+
+ inputType +
+
+
+
+ + inputType + +
+
+ + + + +
+
+
+ a +
+
+
+
+ + a + +
+
+ + + + +
+
+
+ b +
+
+
+
+ + b + +
+
+ + + + +
+
+
+ v +
+
+
+
+ + v + +
+
+ + + + +
+
+
+ dac1_min +
+
+
+
+ + dac1_min + +
+
+ + + + +
+
+
+ dac1_max +
+
+
+
+ + dac1_max + +
+
+ + + + +
+
+
+ dac2_min +
+
+
+
+ + dac2_min + +
+
+ + + + +
+
+
+ dac2_max +
+
+
+
+ + dac2_max + +
+
+ + + + +
+
+
+ dac3_min +
+
+
+
+ + dac3_min + +
+
+ + + + +
+
+
+ dac3_max +
+
+
+
+ + dac3_max + +
+
+ + + + +
+
+
+ --- +
+
+
+
+ + --- + +
+
+ + + + + + + + + + + + + +
+
+
+
+ + + stateCodes + + :: + + + OPERATING + +
+
+
+
+
+ + stateCodes::OPERATING + +
+
+ + + + + + + +
+
+
+ + 1x3 shmim called 'fsm' + +
+
+
+
+ + 1x3 shmim called 'fsm' + +
+
+ + + + + + +
+
+
+ + kws + +
+
+
+
+ + kws + +
+
+ + + + + + +
+
+
+ + name + +
+
+
+
+ + name + +
+
+ + + + +
+
+
+ inputType +
+
+
+
+ + inputType + +
+
+ + + + + + +
+
+
+ + value + +
+
+
+
+ + value + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + + + +
+
+
+ + values + +
+
+
+
+ + values + +
+
+ + + + +
+
+
+ val1 +
+
+
+
+ + val1 + +
+
+ + + + +
+
+
+ val2 +
+
+
+
+ + val2 + +
+
+ + + + +
+
+
+ val3 +
+
+
+
+ + val3 + +
+
+ + + + + + + +
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/apps/fsmCtrl/socket.hpp b/apps/fsmCtrl/socket.hpp new file mode 100644 index 000000000..ae277cfd1 --- /dev/null +++ b/apps/fsmCtrl/socket.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include // for stringstreams + +#include "binaryUart.hpp" + +namespace MagAOX +{ +namespace app +{ + +struct SocketBinaryUartCallbacks : public BinaryUartCallbacks +{ + SocketBinaryUartCallbacks() { } + virtual ~SocketBinaryUartCallbacks() { } + + //Malformed/corrupted packet handler: + virtual void InvalidPacket(const uint8_t* Buffer, const size_t& BufferLen) + { + if ( (NULL == Buffer) || (BufferLen < 1) ) + { + std::ostringstream oss; + oss << "SocketUartCallbacks: NULL(" << BufferLen << " ) InvalidPacket!"; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + return; + } + + size_t len = BufferLen; + if (len > 32) { len = 32; } + MagAOXAppT::log({__FILE__, __LINE__, "SocketUartCallbacks: InvalidPacket! contents: :"}); + for(size_t i = 0; i < len; i++) + { + MagAOXAppT::log({__FILE__, __LINE__, Buffer[i]}); + } + } + + //Packet with no matching command handler: + virtual void UnHandledPacket(const IPacket* Packet, const size_t& PacketLen) + { + if ( (NULL == Packet) || (PacketLen < sizeof(CGraphPacketHeader)) ) + { + std::ostringstream oss; + oss << "SocketUartCallbacks: NULL(" << PacketLen << " ) UnHandledPacket!"; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + return; + } + + const CGraphPacketHeader* Header = reinterpret_cast(Packet); + std::ostringstream oss; + oss << "SocketUartCallbacks: Unhandled packet(" << PacketLen << "): "; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + Header->formatf(); + } + + //In case we need to look at every packet that goes by... + //~ virtual void EveryPacket(const IPacket& Packet, const size_t& PacketLen) { } + + //We just wanna see if this is happening, not much to do about it + virtual void BufferOverflow(const size_t& BufferLen) + { + std::ostringstream oss; + oss << "SocketBinaryUartCallbacks: BufferOverflow(" << BufferLen << ")!"; + MagAOXAppT::log({__FILE__, __LINE__, oss.str()}); + } + +} PacketCallbacks; + +} //namespace app +} //namespace MagAOX \ No newline at end of file diff --git a/apps/fsmCtrl/tests/Makefile b/apps/fsmCtrl/tests/Makefile new file mode 100644 index 000000000..1cb6030b2 --- /dev/null +++ b/apps/fsmCtrl/tests/Makefile @@ -0,0 +1,9 @@ + +allall: all + +OTHER_HEADERS=../fsmCtrl.hpp +OTHER_OBJS= +TARGET=fsmCtrl_test + + +include ../../../tests/magAOX_test.mk diff --git a/apps/fsmCtrl/tests/binaryUart_test.cpp b/apps/fsmCtrl/tests/binaryUart_test.cpp new file mode 100644 index 000000000..c4ae27eaa --- /dev/null +++ b/apps/fsmCtrl/tests/binaryUart_test.cpp @@ -0,0 +1,674 @@ +/** \file fsmCtrl_test.cpp + * \brief Catch2 tests for the template app. + * + * History: + */ +#include "../../../tests/catch2/catch.hpp" + +#include "../fsmCtrl.hpp" +#include "../binaryUart.hpp" +#include "../iPacket.hpp" +#include "../fsmCommands.hpp" + +using namespace MagAOX::app; + +namespace binaryUart_test +{ + +/*! \brief Mock classes + */ +class MockIUart : public IUart +{ +public: + std::string sentData = ""; // Stream to capture sent data + + // Implement IUart interface methods + bool dataready() const override { return false; } + char getcqq() override { return '\0'; } + char putcqq(char c) override { + // // Convert the byte to its hexadecimal representation + // std::stringstream ss; + // ss << std::setw(2) << std::setfill('0') << std::hex << static_cast(c); + // std::string hexByte = ss.str(); + + // // Append the hexadecimal representation to sentData + // sentData += hexByte; + sentData += c; + return c; + } + void flushoutput() override { } + void purgeinput() override { } + bool isopen() const override { return false; } +}; + +class MockIPacket : public IPacket +{ +public: + bool packetStartFound = false; + bool packetEndFound = false; + size_t payloadLen = 0; + size_t maxPayloadLength = 0; + size_t offsetChange = 0; + bool packetIsValid = false; + + // Implement IPacket virtual methods + bool FindPacketStart(const uint8_t* Buffer, const size_t BufferLen, size_t& Offset) const override { return packetStartFound; } + bool FindPacketEnd(const uint8_t* Buffer, const size_t BufferLen, size_t& Offset) const override { + Offset += offsetChange; + return packetEndFound; + } + size_t HeaderLen() const override { return 8; } + size_t FooterLen() const override { return 8; } + size_t PayloadOffset() const override { return 0; } + size_t MaxPayloadLength() const override { return maxPayloadLength; } + size_t PayloadLen(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const override { return payloadLen; } + bool IsValid(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos) const override { return packetIsValid; } + uint64_t SerialNum() const override { return 0; } + uint64_t PayloadType(const uint8_t* Buffer, const size_t PacketStartPos, const size_t PacketEndPos) const override { return 0; } + bool DoesPayloadTypeMatch(const uint8_t* Buffer, const size_t BufferCount, const size_t PacketStartPos, const uint32_t CmdType) const override { return false; } + size_t MakePacket(uint8_t* Buffer, const size_t BufferCount, const void* Payload, const uint16_t PayloadType, const size_t PayloadLen) const override { + if (NULL != Payload) { memcpy(Buffer, Payload, PayloadLen); } // if Payload is not null, copy Payload into Buffer + return PayloadLen; + } +}; + +class MockBinaryUartCallbacks : public BinaryUartCallbacks +{ +public: + bool BufferOverflowCalled = false; + bool InvalidPacketCalled = false; + bool UnHandledPacketCalled = false; + bool EveryPacketCalled = false; + + // Implement BinaryUartCallbacks virtual methods + void InvalidPacket(const uint8_t* Buffer, const size_t& BufferLen) override { InvalidPacketCalled = true; } + void UnHandledPacket(const IPacket* Packet, const size_t& PacketLen) override { UnHandledPacketCalled = true; } + void EveryPacket(const IPacket* Packet, const size_t& PacketLen) override { EveryPacketCalled = true; } + void BufferOverflow(const size_t& BufferLen) override { BufferOverflowCalled = true; } +}; + +class MockQuery : public PZTQuery +{ +public: + bool processReplyCalled = false; + + // Implement PZTQuery virtual methods + void errorLogString(const size_t ParamsLen) override { }; + void processReply(char const *Params, const size_t ParamsLen) override { processReplyCalled = true; }; + void logReply() override { }; + uint16_t getPayloadType() const override { return 0; } + uint16_t getPayloadLen() const override { return 0; } + void setPayload(void *newPayloadData, uint16_t newPayloadLen) override { } + void resetPayload() override { } +}; + + +SCENARIO("Creating BinaryUart instance", "[BinaryUart]") +{ + GIVEN("A new BinaryUart instance") + { + // Create mock instances to pass to BinaryUart + MockIUart* pinout = new MockIUart(); + MockIPacket* packet = new MockIPacket(); + MockBinaryUartCallbacks* callbacks = new MockBinaryUartCallbacks(); + + const uint64_t rxCountInit = BinaryUart::RxCountInit; + const bool inPacketInit = BinaryUart::InPacketInit; + const uint64_t packetStartInit = BinaryUart::PacketStartInit; + const uint64_t packetLenInit = BinaryUart::PacketLenInit; + const size_t rxBufferLenBytes = BinaryUart::RxBufferLenBytes; + const char emptyBufferChar = BinaryUart::EmptyBufferChar; + const uint64_t invalidSerialNumber = BinaryUart::InvalidSerialNumber; + + WHEN("Instance created with an IUart, IPacket, callbacks and a given serialnum.") + { + uint64_t serialnum = 0xAAA; + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks, serialnum); + + THEN("Instance creation is successful") + { + // Verify RxCount state + REQUIRE(uart.RxCount == rxCountInit); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + // Verify serialnum + REQUIRE(uart.SerialNum == serialnum); + // Verify PacketStart + REQUIRE(uart.PacketStart == packetStartInit); + // Verify PacketLen + REQUIRE(uart.PacketLen == packetLenInit); + + // Check if the RxBuffer is empty + bool isEmpty = true; + for (size_t i = 0; i < rxBufferLenBytes; ++i) + { + if (uart.RxBuffer[i] != emptyBufferChar) + { + isEmpty = false; + break; + } + } + // Assert that the RxBuffer is empty + REQUIRE(isEmpty); + } + } + + WHEN("Instance created with an IUart, IPacket, callbacks and no given serialnum.") + { + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks); + + THEN("Instance creation is successful") + { + // Verify RxCount state + REQUIRE(uart.RxCount == rxCountInit); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + // Verify serialnum + REQUIRE(uart.SerialNum == invalidSerialNumber); + // Verify PacketStart + REQUIRE(uart.PacketStart == packetStartInit); + // Verify PacketLen + REQUIRE(uart.PacketLen == packetLenInit); + + // Check if the RxBuffer is empty + bool isEmpty = true; + for (size_t i = 0; i < rxBufferLenBytes; ++i) + { + if (uart.RxBuffer[i] != emptyBufferChar) + { + isEmpty = false; + break; + } + } + // Assert that the RxBuffer is empty + REQUIRE(isEmpty); + } + } + + } +} + +/// TODO: Would really need to test Init on its own, not only when called in constructor + + +// Test cases for BinaryUart::ProcessByte method +SCENARIO("Testing ProcessByte", "[BinaryUart]") +{ + GIVEN("An instance of BinaryUart and a new constant character") + { + // Create mock instances to pass to BinaryUart + MockIUart* pinout = new MockIUart(); + MockIPacket* packet = new MockIPacket(); + MockBinaryUartCallbacks* callbacks = new MockBinaryUartCallbacks(); + + const uint64_t rxCountInit = BinaryUart::RxCountInit; + const bool inPacketInit = BinaryUart::InPacketInit; + const uint64_t packetStartInit = BinaryUart::PacketStartInit; + const uint64_t packetLenInit = BinaryUart::PacketLenInit; + const size_t rxBufferLenBytes = BinaryUart::RxBufferLenBytes; + const char emptyBufferChar = BinaryUart::EmptyBufferChar; + + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks); + + WHEN("The buffer is empty") + { + const char testChar = 'a'; + + uart.ProcessByte(testChar); + + THEN("The new character is added to the buffer on the first position") + { + // Verify RxCount state + REQUIRE(uart.RxCount == (rxCountInit + 1)); + // Verify Buffer has the new character + REQUIRE(uart.RxBuffer[rxCountInit] == testChar); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + } + } + WHEN("The buffer is neither empty nor full") + { + uint64_t start = uart.RxCount = 20; + const char testChar = 'a'; + + uart.ProcessByte(testChar); + + THEN("The new character is added to the buffer on the next available position") + { + // We're not testing the case where we're out of buffer bounds + REQUIRE(start < sizeof(uart.RxBuffer)); + + // Verify RxCount state + REQUIRE(uart.RxCount == (start + 1)); + // Verify Buffer has the new character + REQUIRE(uart.RxBuffer[start] == testChar); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + } + } + WHEN("The buffer is full") + { + uint64_t start = uart.RxCount = sizeof(uart.RxBuffer); + const uint64_t serialnum = uart.SerialNum; + const char testChar = 'a'; + + // Fill buffer + for (size_t i = 0; i < rxBufferLenBytes; ++i) { uart.RxBuffer[i] = 'a'; } + + uart.ProcessByte(testChar); + + THEN("BufferOverflow is called and the buffer is reset") + { + // We're testing the case where we're out of buffer bounds + REQUIRE(start >= sizeof(uart.RxBuffer)); + + // Verify BufferOverflow called + REQUIRE(callbacks->BufferOverflowCalled); + + // Verify serialnum + REQUIRE(uart.SerialNum == serialnum); + // Verify RxCount state + REQUIRE(uart.RxCount == rxCountInit); + // Verify PacketStart + REQUIRE(uart.PacketStart == packetStartInit); + // Verify PacketLen + REQUIRE(uart.PacketLen == packetLenInit); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + + // Check if the RxBuffer is empty + bool isEmpty = true; + for (size_t i = 0; i < rxBufferLenBytes; ++i) + { + if (uart.RxBuffer[i] != emptyBufferChar) + { + isEmpty = false; + break; + } + } + // Assert that the RxBuffer is empty + REQUIRE(isEmpty); + } + } + } +} + + +// Test cases for BinaryUart::CheckPacketStart method +SCENARIO("Testing CheckPacketStart", "[BinaryUart]") +{ + GIVEN("An instance of BinaryUart") + { + // Create mock instances to pass to BinaryUart + MockIUart* pinout = new MockIUart(); + MockIPacket* packet = new MockIPacket(); + MockBinaryUartCallbacks* callbacks = new MockBinaryUartCallbacks(); + + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks); + + WHEN("Not in packet and in header position") + { + uart.InPacket = false; + uart.RxCount = (packet->HeaderLen() - 1); + + uart.CheckPacketStart(); + + THEN("Packet start not found") + { + REQUIRE(uart.InPacket == false); + } + } + + WHEN("In packet and after header") + { + uart.InPacket = true; + uart.RxCount = (packet->HeaderLen() + 1); + + uart.CheckPacketStart(); + + THEN("Packet start not here (found before)") + { + REQUIRE(uart.InPacket == true); + } + } + + WHEN("Not in packet and after header, but packet start not marked as expected") + { + uart.InPacket = false; + uart.RxCount = (packet->HeaderLen() + 1); + + uart.CheckPacketStart(); + + THEN("Packet start not found") + { + REQUIRE(uart.InPacket == false); + } + } + + WHEN("Not in packet and after header, and packet start marked as expected") + { + uart.InPacket = false; + uart.RxCount = (packet->HeaderLen() + 1); + packet->packetStartFound = true; + + uart.CheckPacketStart(); + + THEN("Packet start found") + { + REQUIRE(uart.InPacket == true); + } + } + } +} + + +// Test cases for BinaryUart::CheckPacketEnd method +SCENARIO("Testing CheckPacketEnd", "[BinaryUart]") +{ + GIVEN("An instance of BinaryUart") + { + // Create mock instances + MockIUart* pinout = new MockIUart(); + MockIPacket* packet = new MockIPacket(); + MockBinaryUartCallbacks* callbacks = new MockBinaryUartCallbacks(); + MockQuery* query = new MockQuery(); + + const uint64_t rxCountInit = BinaryUart::RxCountInit; + const bool inPacketInit = BinaryUart::InPacketInit; + const uint64_t packetStartInit = BinaryUart::PacketStartInit; + const uint64_t packetLenInit = BinaryUart::PacketLenInit; + const size_t rxBufferLenBytes = BinaryUart::RxBufferLenBytes; + const char emptyBufferChar = BinaryUart::EmptyBufferChar; + const uint64_t invalidSerialNumber = BinaryUart::InvalidSerialNumber; + + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks); + + WHEN("Not in packet, but RxCount larger or equal than header + footer lengths") + { + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + 1; + + THEN("Packet end not found. Keep looking") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + } + } + + WHEN("In packet, but RxCount less than header + footer lengths") + { + uart.InPacket = true; + uart.RxCount = packet->HeaderLen() + packet->FooterLen() - 1; + + THEN("Packet end not found. Keep looking") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + } + } + + WHEN("In packet and RxCount larger or equal than header + footer lengths, but packet end not marked as expected") + { + uart.InPacket = true; + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + 1; + packet->packetEndFound = false; + + THEN("Packet end not found. Keep looking") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + } + } + + WHEN("In packet and RxCount larger or equal than header + footer lengths; packet end marked as expected, but packet shorter than expected." + "\nUnrealistic payload len") + { + uart.InPacket = true; + packet->packetEndFound = true; + + packet->payloadLen = rxBufferLenBytes + 1; + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart) - 1; + + // Fill buffer + for (size_t i = 0; i < uart.RxBufferLenBytes; ++i) { uart.RxBuffer[i] = 'a'; } + + THEN("Invalid packet callback. Buffer reset.") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + + // Verify InvalidPacket called + REQUIRE(callbacks->InvalidPacketCalled); + + // Verify serialnum + REQUIRE(uart.SerialNum == invalidSerialNumber); + // Verify RxCount state + REQUIRE(uart.RxCount == rxCountInit); + // Verify PacketStart + REQUIRE(uart.PacketStart == packetStartInit); + // Verify PacketLen + REQUIRE(uart.PacketLen == packetLenInit); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + + // Check if the RxBuffer is empty + bool isEmpty = true; + for (size_t i = 0; i < rxBufferLenBytes; ++i) + { + if (uart.RxBuffer[i] != emptyBufferChar) + { + isEmpty = false; + break; + } + } + // Assert that the RxBuffer is empty + REQUIRE(isEmpty); + } + } + + WHEN("In packet and RxCount larger or equal than header + footer lengths; packet end marked as expected, but packet shorter than expected." + "\nPayload mis-identified as footer") + { + uart.InPacket = true; + packet->packetEndFound = true; + + packet->maxPayloadLength = 0; + packet->payloadLen = rxBufferLenBytes - 1; + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart) - 1; + + THEN("Packet end not found. Keep looking") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + } + } + + WHEN("Packet end found (all's good) but packet is invalid.") + { + uart.InPacket = true; + packet->packetEndFound = true; + + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart); + + THEN("Invalid packet callback.") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + + // Verify InvalidPacket called + REQUIRE(callbacks->InvalidPacketCalled); + } + } + + WHEN("Packet end found (all's good), packet is valid and serial num matches.") + { + uart.InPacket = true; + packet->packetEndFound = true; + packet->packetIsValid = true; + + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart); + + THEN("Processing reply.") + { + REQUIRE(uart.CheckPacketEnd(query)); + + // Verify processReply called + REQUIRE(query->processReplyCalled); + + // Verify everyPacket called + REQUIRE(callbacks->EveryPacketCalled); + } + } + + WHEN("Packet end found (all's good), packet is valid, but serial num doesn't match.") + { + + uint64_t serialnum = 0xAAA; + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks, serialnum); + + uart.InPacket = true; + packet->packetEndFound = true; + packet->packetIsValid = true; + + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart); + + THEN("Unhandled packet called.") + { + REQUIRE_FALSE(uart.CheckPacketEnd(query)); + + // Verify unhandled packet called + REQUIRE(callbacks->UnHandledPacketCalled); + + // Verify everyPacket called + REQUIRE(callbacks->EveryPacketCalled); + } + } + + WHEN("Packet end found (all's good), packet validity irrelevant. More data in buffer after packet end.") + { + uart.InPacket = true; + packet->packetEndFound = true; + + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart); + uint16_t rxCount = uart.RxCount; + + const char testChar = 'a'; + // Set buffer to 00000000aaaaaaaaa0000.... + for (size_t i = 8; i < uart.RxCount; i++) { uart.RxBuffer[i] = testChar; } + + uart.CheckPacketEnd(query); + + THEN("Data moved to beginning of buffer") + { + REQUIRE_FALSE(uart.InPacket); + + REQUIRE(uart.RxCount == (rxCount - 4)); + + // Check if the RxBuffer matches 0000aaaaaaaa000.... + bool asExpected = true; + for (size_t i = 0; i < rxBufferLenBytes; ++i) + { + if ((i>=4) && i < uart.RxCount){ + if (uart.RxBuffer[i] != testChar) + { + asExpected = false; + break; + } + } else { + if (uart.RxBuffer[i] != emptyBufferChar) + { + asExpected = false; + break; + } + } + } + // Assert that the RxBuffer is empty + REQUIRE(asExpected); + } + } + + WHEN("Packet end found (all's good), packet validity irrelevant. No more data in buffer.") + { + uart.InPacket = true; + packet->packetEndFound = true; + packet->offsetChange = 2 * (packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart)); + + uart.RxCount = packet->HeaderLen() + packet->FooterLen() + packet->PayloadLen(uart.RxBuffer, uart.RxCount, uart.PacketStart); + + const char testChar = 'a'; + // Set buffer to 00000000aaaaaaaaa0000.... + for (size_t i = 8; i < uart.RxCount; i++) { uart.RxBuffer[i] = testChar; } + + uart.CheckPacketEnd(query); + + THEN("Buffer reset.") + { + // Verify serialnum + REQUIRE(uart.SerialNum == invalidSerialNumber); + // Verify RxCount state + REQUIRE(uart.RxCount == rxCountInit); + // Verify PacketStart + REQUIRE(uart.PacketStart == packetStartInit); + // Verify PacketLen + REQUIRE(uart.PacketLen == packetLenInit); + // Verify InPacket flag + REQUIRE(uart.InPacket == inPacketInit); + + // Check if the RxBuffer is empty + bool isEmpty = true; + for (size_t i = 0; i < rxBufferLenBytes; ++i) + { + if (uart.RxBuffer[i] != emptyBufferChar) + { + isEmpty = false; + break; + } + } + // Assert that the RxBuffer is empty + REQUIRE(isEmpty); + } + } + + } +} + + +// Test cases for BinaryUart::CheckPacketEnd method +SCENARIO("Testing TxBinaryPacket", "[BinaryUart]") +{ + GIVEN("An instance of BinaryUart") + { + // Create mock instances + MockIUart* pinout = new MockIUart(); + MockIPacket* packet = new MockIPacket(); + MockBinaryUartCallbacks* callbacks = new MockBinaryUartCallbacks(); + MockQuery* query = new MockQuery(); + + const uint64_t rxCountInit = BinaryUart::RxCountInit; + const bool inPacketInit = BinaryUart::InPacketInit; + const uint64_t packetStartInit = BinaryUart::PacketStartInit; + const uint64_t packetLenInit = BinaryUart::PacketLenInit; + const size_t rxBufferLenBytes = BinaryUart::RxBufferLenBytes; + const char emptyBufferChar = BinaryUart::EmptyBufferChar; + const uint64_t invalidSerialNumber = BinaryUart::InvalidSerialNumber; + + BinaryUart uart = BinaryUart(*pinout, *packet, *callbacks); + + WHEN("Called with given PayloadType, pointer to start of PayloadData and PayloadLen") + { + const uint16_t payloadType = 0x00000; + const char testChar = 'a'; + uint8_t payloadData[3] = {static_cast(testChar), static_cast(testChar), static_cast(testChar)}; + size_t payloadLen = 3 * sizeof(uint8_t); + + uart.TxBinaryPacket(payloadType, reinterpret_cast(payloadData), payloadLen); + + THEN("Builds and sends non-empty packet") + { + // Convert payloadData to a string representation for comparison + ostringstream oss(""); + for (int temp = 0; temp < 3; temp++) + oss << static_cast(payloadData[temp]); + std::string expectedData = oss.str(); + + REQUIRE(expectedData == pinout->sentData); + + } + } + } +} + +} //namespace binaryUart_test diff --git a/apps/fsmCtrl/tests/fsmCtrl_test.cpp b/apps/fsmCtrl/tests/fsmCtrl_test.cpp new file mode 100644 index 000000000..67cdfa39c --- /dev/null +++ b/apps/fsmCtrl/tests/fsmCtrl_test.cpp @@ -0,0 +1,70 @@ +/** \file fsmCtrl_test.cpp + * \brief Catch2 tests for the template app. + * + * History: + */ +#include "../../../tests/catch2/catch.hpp" +#include "../../tests/testMacrosINDI.hpp" + +#include "../fsmCtrl.hpp" + +using namespace MagAOX::app; + +namespace FSMTEST +{ + +class fsmCtrl_test : public fsmCtrl +{ + +public: + fsmCtrl_test(const std::string device) + { + m_configName = device; + + XWCTEST_SETUP_INDI_NEW_PROP(val1); + XWCTEST_SETUP_INDI_NEW_PROP(val2); + XWCTEST_SETUP_INDI_NEW_PROP(val3); + + XWCTEST_SETUP_INDI_NEW_PROP(dac1); + XWCTEST_SETUP_INDI_NEW_PROP(dac2); + XWCTEST_SETUP_INDI_NEW_PROP(dac3); + + XWCTEST_SETUP_INDI_NEW_PROP(conversion_factors); + XWCTEST_SETUP_INDI_NEW_PROP(input); + XWCTEST_SETUP_INDI_NEW_PROP(query); + } +}; + + +SCENARIO( "INDI Callbacks", "[fsmCtrl]" ) +{ + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, val1); + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, val2); + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, val3); + + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, dac1); + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, dac2); + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, dac3); + + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, conversion_factors); + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, input); + XWCTEST_INDI_NEW_CALLBACK( fsmCtrl, query); +} + + + +// SCENARIO("") +// { +// GIVEN("") +// { +// WHEN("") +// { +// REQUIRE(); +// REQUIRE(); +// REQUIRE(); +// } +// } +// } + + +} //namespace FSMTEST diff --git a/libMagAOX/Makefile b/libMagAOX/Makefile index a90e69af0..cf7dc7b69 100644 --- a/libMagAOX/Makefile +++ b/libMagAOX/Makefile @@ -52,45 +52,46 @@ INCLUDEDEPS= app/MagAOXApp.hpp \ logger/types/state_change.hpp \ logger/types/string_log.hpp \ logger/types/user_log.hpp \ - logger/types/config_log.hpp \ - logger/types/saving_state_change.hpp \ - logger/types/saving_start.hpp \ - logger/types/saving_stop.hpp \ - logger/types/telem_saving.hpp \ - logger/types/telem_saving_state.hpp \ - logger/types/telem_dmmodes.hpp \ - logger/types/telem_dmspeck.hpp \ - logger/types/telem_blockgains.hpp \ - logger/types/telem_chrony_status.hpp \ - logger/types/telem_chrony_stats.hpp \ - logger/types/telem_cooler.hpp \ - logger/types/telem_coreloads.hpp \ - logger/types/telem_coretemps.hpp \ - logger/types/telem_drivetemps.hpp \ - logger/types/telem_fgtimings.hpp \ - logger/types/telem_fxngen.hpp \ - logger/types/telem_observer.hpp \ - logger/types/telem_loopgain.hpp \ + logger/types/config_log.hpp \ + logger/types/saving_state_change.hpp \ + logger/types/saving_start.hpp \ + logger/types/saving_stop.hpp \ + logger/types/telem_saving.hpp \ + logger/types/telem_saving_state.hpp \ + logger/types/telem_dmmodes.hpp \ + logger/types/telem_dmspeck.hpp \ + logger/types/telem_blockgains.hpp \ + logger/types/telem_chrony_status.hpp \ + logger/types/telem_chrony_stats.hpp \ + logger/types/telem_cooler.hpp \ + logger/types/telem_coreloads.hpp \ + logger/types/telem_coretemps.hpp \ + logger/types/telem_drivetemps.hpp \ + logger/types/telem_fgtimings.hpp \ + logger/types/telem_fsm.hpp \ + logger/types/telem_fxngen.hpp \ + logger/types/telem_observer.hpp \ + logger/types/telem_loopgain.hpp \ logger/types/telem_pi335.hpp \ - logger/types/telem_pico.hpp \ - logger/types/telem_position.hpp \ - logger/types/telem_pokecenter.hpp \ + logger/types/telem_pico.hpp \ + logger/types/telem_position.hpp \ + logger/types/telem_pokecenter.hpp \ logger/types/telem_pokeloop.hpp \ - logger/types/telem_rhusb.hpp \ - logger/types/telem_stage.hpp \ - logger/types/telem_stdcam.hpp \ - logger/types/telem_telcat.hpp \ - logger/types/telem_teldata.hpp \ - logger/types/telem_telenv.hpp \ - logger/types/telem_telpos.hpp \ - logger/types/telem_telsee.hpp \ - logger/types/telem_telvane.hpp \ - logger/types/telem_temps.hpp \ - logger/types/telem_usage.hpp \ - logger/types/telem_zaber.hpp \ - logger/types/text_log.hpp \ - sys/thSetuid.hpp \ - sys/runCommand.hpp \ + logger/types/telem_rhusb.hpp \ + logger/types/telem_stage.hpp \ + logger/types/telem_stdcam.hpp \ + logger/types/telem_telcat.hpp \ + logger/types/telem_teldata.hpp \ + logger/types/telem_telenv.hpp \ + logger/types/telem_telpos.hpp \ + logger/types/telem_telsee.hpp \ + logger/types/telem_telvane.hpp \ + logger/types/telem_temps.hpp \ + logger/types/telem_usage.hpp \ + logger/types/telem_zaber.hpp \ + logger/types/text_log.hpp \ + sys/thSetuid.hpp \ + sys/runCommand.hpp \ tty/ttyErrors.hpp \ tty/ttyIOUtils.hpp \ tty/ttyUSB.hpp \ @@ -121,10 +122,10 @@ OBJS = app/MagAOXApp.o \ tty/ttyUSB.o \ tty/usbDevice.o - -all: libMagAOX.hpp.gch libMagAOX.a -app/MagAOXApp.o: app/MagAOXApp.hpp app/indiDriver.hpp app/indiMacros.hpp app/indiUtils.hpp app/stateCodes.o +all: libMagAOX.hpp.gch libMagAOX.a + +app/MagAOXApp.o: app/MagAOXApp.hpp app/indiDriver.hpp app/indiMacros.hpp app/indiUtils.hpp app/stateCodes.o app/stateCodes.o: app/stateCodes.hpp libMagAOX.hpp.gch: libMagAOX.hpp $(INCLUDEDEPS) logger/generated/logTypes.hpp $(OBJS) @@ -138,7 +139,7 @@ libMagAOX.a: libMagAOX.hpp.gch $(OBJS) ar rvs libMagAOX.a $(OBJS) logger/logMeta.o: logger/logMap.hpp logger/logMap.cpp logger/logMeta.hpp logger/logMeta.cpp logger/generated/logTypes.hpp -logger/logMap.o: logger/logMap.hpp logger/logMap.cpp logger/logFileName.hpp +logger/logMap.o: logger/logMap.hpp logger/logMap.cpp logger/logFileName.hpp clean: rm -f libMagAOX.hpp.gch diff --git a/libMagAOX/logger/logCodes.dat b/libMagAOX/logger/logCodes.dat index 5ebb6a40e..2de5d84e8 100644 --- a/libMagAOX/logger/logCodes.dat +++ b/libMagAOX/logger/logCodes.dat @@ -78,7 +78,8 @@ telem_fgtimings 20905 telem_fgtimings telem_dmmodes 20910 telem_dmmodes telem_loopgain 20915 telem_loopgain -telem_blockgains 20920 telem_blockgains +telem_blockgains 20920 telem_blockgains telem_pi335 20930 telem_pi335 +telem_fsm 21100 telem_fsm \ No newline at end of file diff --git a/libMagAOX/logger/types/schemas/telem_fsm.fbs b/libMagAOX/logger/types/schemas/telem_fsm.fbs new file mode 100644 index 000000000..5fff8b1e5 --- /dev/null +++ b/libMagAOX/logger/types/schemas/telem_fsm.fbs @@ -0,0 +1,20 @@ +namespace MagAOX.logger; + +table Telem_fsm_fb +{ + P1V2:double; + P2V2:double; + P24V:double; + P2V5:double; + P3V3A:double; + P6V:double; + P5V:double; + P3V3D:double; + P4V3:double; + N5V:double; + N6V:double; + P150V:double; +} + +root_type Telem_fsm_fb; + diff --git a/libMagAOX/logger/types/telem.cpp b/libMagAOX/logger/types/telem.cpp index f5d7f2425..a3c00a5fc 100644 --- a/libMagAOX/logger/types/telem.cpp +++ b/libMagAOX/logger/types/telem.cpp @@ -3,7 +3,7 @@ * \author Jared R. Males (jaredmales@gmail.com) * * \ingroup logger_types_files - * + * */ #include #include "../generated/logTypes.hpp" @@ -24,6 +24,7 @@ timespec telem_dmmodes::lastRecord = {0,0}; timespec telem_dmspeck::lastRecord = {0,0}; timespec telem_drivetemps::lastRecord = {0,0}; timespec telem_fgtimings::lastRecord = {0,0}; +timespec telem_fsm::lastRecord = {0,0}; timespec telem_fxngen::lastRecord = {0,0}; timespec telem_loopgain::lastRecord = {0,0}; timespec telem_observer::lastRecord = {0,0}; diff --git a/libMagAOX/logger/types/telem_fsm.hpp b/libMagAOX/logger/types/telem_fsm.hpp new file mode 100644 index 000000000..072dc79e3 --- /dev/null +++ b/libMagAOX/logger/types/telem_fsm.hpp @@ -0,0 +1,120 @@ +/** \file telem_fsm.hpp + * \brief The MagAO-X logger telem_fsm log type. + * \author Irina Stefan (istefan@arizona.edu) + * + * \ingroup logger_types_files + * + * History: + * - 2023-11-22 - Created by IS + */ +#ifndef logger_types_telem_fsm_hpp +#define logger_types_telem_fsm_hpp + +#include "generated/telem_fsm_generated.h" +#include "flatbuffer_log.hpp" + +namespace MagAOX +{ +namespace logger +{ + + +/// Log entry recording FSM telemetry +/** \ingroup logger_types + */ +struct telem_fsm : public flatbuffer_log +{ + + static const flatlogs::eventCodeT eventCode = eventCodes::TELEM_FSM; + static const flatlogs::logPrioT defaultLevel = flatlogs::logPrio::LOG_TELEM; + + static timespec lastRecord; ///< The time of the last time this log was recorded. Used by the telemetry system. + + ///The type of the input message + struct messageT : public fbMessage + { + ///Construct from components + messageT(const double & P1V2, + const double & P2V2, + const double & P24V, + const double & P2V5, + const double & P3V3A, + const double & P6V, + const double & P5V, + const double & P3V3D, + const double & P4V3, + const double & N5V, + const double & N6V, + const double & P150V + ) + { + auto fp = CreateTelem_fsm_fb(builder, P1V2, P2V2, P24V, P2V5, P3V3A, P6V, P5V, P3V3D, P4V3, N5V, N6V, P150V); + builder.Finish(fp); + } + }; + + static bool verify( flatlogs::bufferPtrT & logBuff, ///< [in] Buffer containing the flatbuffer serialized message. + flatlogs::msgLenT len ///< [in] length of msgBuffer. + ) + { + auto verifier = flatbuffers::Verifier( (uint8_t*) flatlogs::logHeader::messageBuffer(logBuff), static_cast(len)); + return VerifyTelem_fsm_fbBuffer(verifier); + } + + ///Get the message format for human consumption. + static std::string msgString( void * msgBuffer, /**< [in] Buffer containing the flatbuffer serialized message.*/ + flatlogs::msgLenT len /**< [in] [unused] length of msgBuffer.*/ + ) + { + static_cast(len); // unused by most log types + + auto fbs = GetTelem_fsm_fb(msgBuffer); + + std::string msg = "P1V2: "; + msg += std::to_string(fbs->P1V2()) + " V "; + + msg += "P2V2: "; + msg += std::to_string(fbs->P2V2()) + " V "; + + msg += "P24V: "; + msg += std::to_string(fbs->P24V()) + " V "; + + msg += "P2V5: "; + msg += std::to_string(fbs->P2V5()) + " V "; + + msg += "P3V3A: "; + msg += std::to_string(fbs->P3V3A()) + " V "; + + msg += "P6V: "; + msg += std::to_string(fbs->P6V()) + " V "; + + msg += "P5V: "; + msg += std::to_string(fbs->P5V()) + " V "; + + msg += "P3V3D: "; + msg += std::to_string(fbs->P3V3D()) + " V "; + + msg += "P4V3: "; + msg += std::to_string(fbs->P4V3()) + " V "; + + msg += "N5V: "; + msg += std::to_string(fbs->N5V()) + " V "; + + msg += "N6V: "; + msg += std::to_string(fbs->N6V()) + " V "; + + msg += "P150V: "; + msg += std::to_string(fbs->P150V()) + " V "; + + return msg; + + } + +}; //telem_fsm + + + +} //namespace logger +} //namespace MagAOX + +#endif //logger_types_telem_fsm_hpp diff --git a/tests/tests.list b/tests/tests.list index 8a6df3df0..601ea9e3b 100644 --- a/tests/tests.list +++ b/tests/tests.list @@ -24,3 +24,5 @@ ../apps/zaberLowLevel/tests/zaberLowLevel_test ../apps/zaberCtrl/tests/zaberCtrl_test +../apps/fsmCtrl/tests/fsmCtrl_test +../apps/fsmCtrl/tests/binaryUart_test