| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | ///////////////////////////////////////////////////////////////////////////////////
 | 
					
						
							|  |  |  | // Copyright (C) 2015 Edouard Griffiths, F4EXB                                   //
 | 
					
						
							|  |  |  | //                                                                               //
 | 
					
						
							|  |  |  | // This program is free software; you can redistribute it and/or modify          //
 | 
					
						
							|  |  |  | // it under the terms of the GNU General Public License as published by          //
 | 
					
						
							|  |  |  | // the Free Software Foundation as version 3 of the License, or                  //
 | 
					
						
							|  |  |  | //                                                                               //
 | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful,               //
 | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of                //
 | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                  //
 | 
					
						
							|  |  |  | // GNU General Public License V3 for more details.                               //
 | 
					
						
							|  |  |  | //                                                                               //
 | 
					
						
							|  |  |  | // You should have received a copy of the GNU General Public License             //
 | 
					
						
							|  |  |  | // along with this program. If not, see <http://www.gnu.org/licenses/>.          //
 | 
					
						
							|  |  |  | ///////////////////////////////////////////////////////////////////////////////////
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include <QUdpSocket>
 | 
					
						
							|  |  |  | #include <QDebug>
 | 
					
						
							| 
									
										
										
										
											2016-02-20 23:02:34 +01:00
										 |  |  | #include <QTimer>
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | #include <unistd.h>
 | 
					
						
							| 
									
										
										
										
											2016-05-12 23:45:27 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-17 19:42:26 +01:00
										 |  |  | #include "dsp/dspcommands.h"
 | 
					
						
							|  |  |  | #include "dsp/dspengine.h"
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | #include "sdrdaemonudphandler.h"
 | 
					
						
							| 
									
										
										
										
											2016-10-11 01:17:55 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | #include <device/devicesourceapi.h>
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | #include "sdrdaemoninput.h"
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-10-11 01:17:55 +02:00
										 |  |  | SDRdaemonUDPHandler::SDRdaemonUDPHandler(SampleSinkFifo *sampleFifo, MessageQueue *outputMessageQueueToGUI, DeviceSourceAPI *devieAPI) : | 
					
						
							| 
									
										
										
										
											2016-05-16 02:14:36 +02:00
										 |  |  |     m_deviceAPI(devieAPI), | 
					
						
							| 
									
										
										
										
											2016-02-20 03:41:20 +01:00
										 |  |  | 	m_sdrDaemonBuffer(m_rateDivider), | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 	m_dataSocket(0), | 
					
						
							|  |  |  | 	m_dataAddress(QHostAddress::LocalHost), | 
					
						
							| 
									
										
										
										
											2016-03-26 21:40:54 +01:00
										 |  |  | 	m_remoteAddress(QHostAddress::LocalHost), | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 	m_dataPort(9090), | 
					
						
							|  |  |  | 	m_dataConnected(false), | 
					
						
							|  |  |  | 	m_udpBuf(0), | 
					
						
							|  |  |  | 	m_udpReadBytes(0), | 
					
						
							|  |  |  | 	m_sampleFifo(sampleFifo), | 
					
						
							|  |  |  | 	m_samplerate(0), | 
					
						
							|  |  |  | 	m_centerFrequency(0), | 
					
						
							|  |  |  | 	m_tv_sec(0), | 
					
						
							|  |  |  | 	m_tv_usec(0), | 
					
						
							|  |  |  | 	m_outputMessageQueueToGUI(outputMessageQueueToGUI), | 
					
						
							|  |  |  | 	m_tickCount(0), | 
					
						
							| 
									
										
										
										
											2016-02-20 23:02:34 +01:00
										 |  |  | 	m_samplesCount(0), | 
					
						
							| 
									
										
										
										
											2016-03-15 06:13:52 +01:00
										 |  |  | 	m_timer(0), | 
					
						
							| 
									
										
										
										
											2016-03-15 18:06:02 +01:00
										 |  |  |     m_throttlems(SDRDAEMON_THROTTLE_MS), | 
					
						
							|  |  |  |     m_readLengthSamples(0), | 
					
						
							|  |  |  |     m_readLength(0), | 
					
						
							| 
									
										
										
										
											2016-03-16 18:46:16 +01:00
										 |  |  |     m_throttleToggle(false), | 
					
						
							| 
									
										
										
										
											2016-03-19 06:14:06 +01:00
										 |  |  |     m_rateDivider(1000/SDRDAEMON_THROTTLE_MS), | 
					
						
							|  |  |  | 	m_autoCorrBuffer(false) | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | { | 
					
						
							| 
									
										
										
										
											2016-02-20 03:41:20 +01:00
										 |  |  |     m_udpBuf = new char[SDRdaemonBuffer::m_udpPayloadSize]; | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | SDRdaemonUDPHandler::~SDRdaemonUDPHandler() | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	stop(); | 
					
						
							|  |  |  | 	delete[] m_udpBuf; | 
					
						
							| 
									
										
										
										
											2016-03-16 18:46:16 +01:00
										 |  |  | #ifdef USE_INTERNAL_TIMER
 | 
					
						
							|  |  |  |     if (m_timer) { | 
					
						
							|  |  |  |         delete m_timer; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | #endif
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void SDRdaemonUDPHandler::start() | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2016-02-20 23:02:34 +01:00
										 |  |  | 	qDebug("SDRdaemonUDPHandler::start"); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 	if (!m_dataSocket) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		m_dataSocket = new QUdpSocket(this); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (!m_dataConnected) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		if (m_dataSocket->bind(m_dataAddress, m_dataPort)) | 
					
						
							|  |  |  | 		{ | 
					
						
							| 
									
										
										
										
											2016-02-20 23:02:34 +01:00
										 |  |  | 			qDebug("SDRdaemonUDPHandler::start: bind data socket to %s:%d", m_dataAddress.toString().toStdString().c_str(),  m_dataPort); | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 			connect(m_dataSocket, SIGNAL(readyRead()), this, SLOT(dataReadyRead()), Qt::QueuedConnection); // , Qt::QueuedConnection
 | 
					
						
							|  |  |  | 			m_dataConnected = true; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		else | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			qWarning("SDRdaemonUDPHandler::start: cannot bind data port %d", m_dataPort); | 
					
						
							|  |  |  | 			m_dataConnected = false; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2016-02-23 22:24:25 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Need to notify the DSP engine to actually start
 | 
					
						
							|  |  |  | 	DSPSignalNotification *notif = new DSPSignalNotification(m_samplerate, m_centerFrequency * 1000); // Frequency in Hz for the DSP engine
 | 
					
						
							| 
									
										
										
										
											2016-05-16 02:14:36 +02:00
										 |  |  | 	m_deviceAPI->getDeviceInputMessageQueue()->push(notif); | 
					
						
							| 
									
										
										
										
											2016-03-15 18:06:02 +01:00
										 |  |  |     m_elapsedTimer.start(); | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void SDRdaemonUDPHandler::stop() | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	qDebug("SDRdaemonUDPHandler::stop"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (m_dataConnected) { | 
					
						
							|  |  |  | 		disconnect(m_dataSocket, SIGNAL(readyRead()), this, SLOT(dataReadyRead())); | 
					
						
							|  |  |  | 		m_dataConnected = false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (m_dataSocket) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		delete m_dataSocket; | 
					
						
							|  |  |  | 		m_dataSocket = 0; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-20 23:02:34 +01:00
										 |  |  | void SDRdaemonUDPHandler::configureUDPLink(const QString& address, quint16 port) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	qDebug("SDRdaemonUDPHandler::configureUDPLink: %s:%d", address.toStdString().c_str(), port); | 
					
						
							|  |  |  | 	bool addressOK = m_dataAddress.setAddress(address); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if (!addressOK) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		qWarning("SDRdaemonUDPHandler::configureUDPLink: invalid address %s. Set to localhost.", address.toStdString().c_str()); | 
					
						
							|  |  |  | 		m_dataAddress = QHostAddress::LocalHost; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	stop(); | 
					
						
							|  |  |  | 	m_dataPort = port; | 
					
						
							|  |  |  | 	start(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | void SDRdaemonUDPHandler::dataReadyRead() | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	while (m_dataSocket->hasPendingDatagrams()) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		qint64 pendingDataSize = m_dataSocket->pendingDatagramSize(); | 
					
						
							| 
									
										
										
										
											2016-03-26 21:40:54 +01:00
										 |  |  | 		m_udpReadBytes = m_dataSocket->readDatagram(m_udpBuf, pendingDataSize, &m_remoteAddress, 0); | 
					
						
							| 
									
										
										
										
											2016-02-18 22:26:47 +01:00
										 |  |  | 		processData(); | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void SDRdaemonUDPHandler::processData() | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	if (m_udpReadBytes < 0) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		qDebug() << "SDRdaemonThread::processData: read failed"; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	else if (m_udpReadBytes > 0) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		m_sdrDaemonBuffer.updateBlockCounts(m_udpReadBytes); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-25 20:13:34 +02:00
										 |  |  | 		if (m_sdrDaemonBuffer.readMeta(m_udpBuf)) | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 		{ | 
					
						
							|  |  |  | 			const SDRdaemonBuffer::MetaData& metaData =  m_sdrDaemonBuffer.getCurrentMeta(); | 
					
						
							|  |  |  | 			bool change = false; | 
					
						
							|  |  |  | 			m_tv_sec = metaData.m_tv_sec; | 
					
						
							|  |  |  | 			m_tv_usec = metaData.m_tv_usec; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-22 12:10:13 +01:00
										 |  |  | 			uint32_t sampleRate = m_sdrDaemonBuffer.getSampleRate(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (m_samplerate != sampleRate) | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 			{ | 
					
						
							| 
									
										
										
										
											2016-02-22 12:10:13 +01:00
										 |  |  | 				setSamplerate(sampleRate); | 
					
						
							|  |  |  | 				m_samplerate = sampleRate; | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 				change = true; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (m_centerFrequency != metaData.m_centerFrequency) | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				m_centerFrequency = metaData.m_centerFrequency; | 
					
						
							|  |  |  | 				change = true; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if (change) | 
					
						
							|  |  |  | 			{ | 
					
						
							| 
									
										
										
										
											2016-02-18 00:33:04 +01:00
										 |  |  | 				DSPSignalNotification *notif = new DSPSignalNotification(m_samplerate, m_centerFrequency * 1000); // Frequency in Hz for the DSP engine
 | 
					
						
							| 
									
										
										
										
											2016-05-16 02:14:36 +02:00
										 |  |  | 				m_deviceAPI->getDeviceInputMessageQueue()->push(notif); | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 				SDRdaemonInput::MsgReportSDRdaemonStreamData *report = SDRdaemonInput::MsgReportSDRdaemonStreamData::create( | 
					
						
							| 
									
										
										
										
											2016-02-22 15:03:16 +01:00
										 |  |  | 					m_sdrDaemonBuffer.getSampleRateStream(), | 
					
						
							| 
									
										
										
										
											2016-02-17 19:42:26 +01:00
										 |  |  | 					m_samplerate, | 
					
						
							| 
									
										
										
										
											2016-02-20 10:10:11 +01:00
										 |  |  | 					m_centerFrequency * 1000, // Frequency in Hz for the GUI
 | 
					
						
							| 
									
										
										
										
											2016-02-17 19:42:26 +01:00
										 |  |  | 					m_tv_sec, | 
					
						
							|  |  |  | 					m_tv_usec); | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 				m_outputMessageQueueToGUI->push(report); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		else if (m_sdrDaemonBuffer.isSync()) | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			m_sdrDaemonBuffer.writeData(m_udpBuf, m_udpReadBytes); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void SDRdaemonUDPHandler::setSamplerate(uint32_t samplerate) | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  | 	qDebug() << "SDRdaemonUDPHandler::setSamplerate:" | 
					
						
							|  |  |  | 			<< " new:" << samplerate | 
					
						
							|  |  |  | 			<< " old:" << m_samplerate; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	m_samplerate = samplerate; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-20 23:02:34 +01:00
										 |  |  | void SDRdaemonUDPHandler::connectTimer(const QTimer* timer) | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | { | 
					
						
							|  |  |  | 	qDebug() << "SDRdaemonUDPHandler::connectTimer"; | 
					
						
							| 
									
										
										
										
											2016-03-16 18:46:16 +01:00
										 |  |  | #ifdef USE_INTERNAL_TIMER
 | 
					
						
							|  |  |  | #warning "Uses internal timer"
 | 
					
						
							|  |  |  |     m_timer = new QTimer(); | 
					
						
							| 
									
										
										
										
											2016-03-17 09:17:02 +01:00
										 |  |  |     m_timer->start(50); | 
					
						
							| 
									
										
										
										
											2016-03-16 22:40:54 +01:00
										 |  |  |     m_throttlems = m_timer->interval(); | 
					
						
							|  |  |  |     connect(m_timer, SIGNAL(timeout()), this, SLOT(tick())); | 
					
						
							| 
									
										
										
										
											2016-03-16 18:46:16 +01:00
										 |  |  | #else
 | 
					
						
							| 
									
										
										
										
											2016-03-16 22:40:54 +01:00
										 |  |  |     m_throttlems = timer->interval(); | 
					
						
							|  |  |  |     connect(timer, SIGNAL(timeout()), this, SLOT(tick())); | 
					
						
							| 
									
										
										
										
											2016-03-16 18:46:16 +01:00
										 |  |  | #endif
 | 
					
						
							|  |  |  |     m_rateDivider = 1000 / m_throttlems; | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | void SDRdaemonUDPHandler::tick() | 
					
						
							|  |  |  | { | 
					
						
							| 
									
										
										
										
											2016-03-15 18:06:02 +01:00
										 |  |  |     // auto throttling
 | 
					
						
							|  |  |  |     int throttlems = m_elapsedTimer.restart(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (throttlems != m_throttlems) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         m_throttlems = throttlems; | 
					
						
							|  |  |  |         m_readLengthSamples = (m_sdrDaemonBuffer.getSampleRate() * (m_throttlems+(m_throttleToggle ? 1 : 0))) / 1000; | 
					
						
							|  |  |  |         m_throttleToggle = !m_throttleToggle; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-03-15 06:13:52 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-03-19 06:14:06 +01:00
										 |  |  |     if (m_autoCorrBuffer) { | 
					
						
							|  |  |  |     	m_readLengthSamples += m_sdrDaemonBuffer.getRWBalanceCorrection(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-07-13 03:31:19 +02:00
										 |  |  |     m_readLength = m_readLengthSamples * SDRdaemonBuffer::m_iqSampleSize; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 	// read samples directly feeding the SampleFifo (no callback)
 | 
					
						
							| 
									
										
										
										
											2016-03-15 18:06:02 +01:00
										 |  |  |     m_sampleFifo->write(reinterpret_cast<quint8*>(m_sdrDaemonBuffer.readData(m_readLength)), m_readLength); | 
					
						
							|  |  |  |     m_samplesCount += m_readLengthSamples; | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	if (m_tickCount < m_rateDivider) | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		m_tickCount++; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	else | 
					
						
							|  |  |  | 	{ | 
					
						
							|  |  |  | 		m_tickCount = 0; | 
					
						
							|  |  |  | 		SDRdaemonInput::MsgReportSDRdaemonStreamTiming *report = SDRdaemonInput::MsgReportSDRdaemonStreamTiming::create( | 
					
						
							|  |  |  | 			m_tv_sec, | 
					
						
							| 
									
										
										
										
											2016-02-23 14:57:40 +01:00
										 |  |  | 			m_tv_usec, | 
					
						
							| 
									
										
										
										
											2016-02-23 18:09:20 +01:00
										 |  |  | 			m_sdrDaemonBuffer.isSyncLocked(), | 
					
						
							| 
									
										
										
										
											2016-02-23 18:19:35 +01:00
										 |  |  | 			m_sdrDaemonBuffer.getFrameSize(), | 
					
						
							| 
									
										
										
										
											2016-03-20 14:40:40 +01:00
										 |  |  | 			m_sdrDaemonBuffer.getBufferLengthInSecs(), | 
					
						
							| 
									
										
										
										
											2016-02-23 18:29:08 +01:00
										 |  |  | 			m_sdrDaemonBuffer.isLz4Compressed(), | 
					
						
							| 
									
										
										
										
											2016-02-23 19:27:47 +01:00
										 |  |  | 			m_sdrDaemonBuffer.getCompressionRatio(), | 
					
						
							|  |  |  | 			m_sdrDaemonBuffer.getLz4DataCRCOK(), | 
					
						
							| 
									
										
										
										
											2016-03-17 15:41:48 +01:00
										 |  |  |             m_sdrDaemonBuffer.getLz4SuccessfulDecodes(), | 
					
						
							|  |  |  |             m_sdrDaemonBuffer.getBufferGauge()); | 
					
						
							| 
									
										
										
										
											2016-02-17 02:22:05 +01:00
										 |  |  | 		m_outputMessageQueueToGUI->push(report); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 |