1 /**
2  * radiotest.cpp
3  * This file is part of the YATE Project http://YATE.null.ro
4  *
5  * Radio interface test
6  *
7  * Yet Another Telephony Engine - a fully featured software PBX and IVR
8  * Copyright (C) 2015 Null Team
9  * Copyright (C) 2015 LEGBA Inc
10  *
11  * This software is distributed under multiple licenses;
12  * see the COPYING file in the main directory for licensing
13  * information for this specific distribution.
14  *
15  * This use of this software may be subject to additional restrictions.
16  * See the LEGAL file in the main directory for details.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
21  */
22 
23 #include <yatephone.h>
24 #include <yateradio.h>
25 #include <yatemath.h>
26 
27 using namespace TelEngine;
28 namespace { // anonymous
29 
30 class RadioTestRecv;
31 class RadioTestModule;
32 
33 class RadioTestIO
34 {
35 public:
RadioTestIO(bool _tx)36     inline RadioTestIO(bool _tx)
37 	: tx(_tx), enabled(false), startTime(0), ts(0), transferred(0)
38 	{}
39     bool tx;
40     bool enabled;
41     uint64_t startTime;
42     uint64_t ts;
43     uint64_t transferred;
44 };
45 
46 class RadioTest : public Thread, public DebugEnabler
47 {
48     friend class RadioTestRecv;
49     friend class RadioTestModule;
50 public:
51     RadioTest(const NamedList& params, const NamedList& radioParams);
~RadioTest()52     ~RadioTest()
53 	{ terminated(m_started ? "destroyed" : 0); }
54     bool command(const String& cmd, const NamedList& params);
55     static bool start(const NamedList& params, const NamedList& radioParams);
56 
57 protected:
cleanup()58     virtual void cleanup()
59 	{ terminated(m_started ? "cleanup" : 0); }
60     virtual void run();
61     void terminated(const char* error);
62     void initPulse();
63     bool setTxData();
64     void regenerateTxData();
65     bool execute(const String& cmd, const String& param, bool fatal,
66 	const NamedList* params);
67     bool execute(const NamedList& cmds, const char* prefix);
68     bool write();
69     bool read();
70     void readTerminated(RadioTestRecv* th);
71     void readStop();
72     void hardCancelRecv();
updateTs(bool tx)73     inline void updateTs(bool tx) {
74 	    uint64_t ts = 0;
75 	    if ((tx ? m_radio->getTxTime(ts) : m_radio->getRxTime(ts)) == 0) {
76 		(tx ? m_tx.ts : m_rx.ts) = ts;
77 		Debug(this,DebugInfo,"Updated %s ts=" FMT64U " [%p]",
78 		    (tx ? "TX" : "RX"),ts,this);
79 	    }
80 	}
81     bool wait(const String& param);
82     void notify(const char* state, const char* error = 0, const NamedList* params = 0) const;
83 
84     RadioInterface* m_radio;
85     RadioTestRecv* m_recv;
86     bool m_started;
87     unsigned int m_repeat;
88     NamedList m_init;
89     NamedList m_params;
90     NamedList m_radioParams;
91     // TX
92     RadioTestIO m_tx;
93     bool m_txYield;
94     bool m_newTxData;
95     unsigned int m_phase;
96     unsigned int m_sendBufCount;
97     ComplexVector m_sendBufData;
98     // Pulse
99     unsigned int m_pulse;
100     ComplexVector m_pulseData;
101     // RX
102     RadioTestIO m_rx;
103     RadioReadBufs m_bufs;
104     uint64_t m_rxSkippedSamples;
105     DataBlock m_crt;
106     DataBlock m_aux;
107     DataBlock m_extra;
108 };
109 
110 class RadioTestRecv : public Thread
111 {
112 public:
RadioTestRecv(RadioTest * test,Thread::Priority prio)113     inline RadioTestRecv(RadioTest* test, Thread::Priority prio)
114 	: Thread("RadioTestRecv",prio), m_test(test)
115 	{}
~RadioTestRecv()116     ~RadioTestRecv()
117 	{ notify(); }
cleanup()118     void cleanup()
119 	{ notify(); }
start(RadioTest * test,Thread::Priority prio)120     static inline RadioTestRecv* start(RadioTest* test, Thread::Priority prio) {
121 	    RadioTestRecv* tmp = new RadioTestRecv(test,prio);
122 	    if (tmp->startup())
123 		return tmp;
124 	    delete tmp;
125 	    return 0;
126 	}
127 protected:
run()128     virtual void run() {
129 	    if (!m_test)
130 		return;
131 	    while (!Thread::check(false) && m_test->read())
132 		;
133 	    notify();
134 	}
notify()135     inline void notify() {
136 	    RadioTest* tmp = m_test;
137 	    m_test = 0;
138 	    if (tmp)
139 		tmp->readTerminated(this);
140 	}
141     RadioTest* m_test;
142 };
143 
144 class RadioTestModule : public Module
145 {
146 public:
147     RadioTestModule();
148     ~RadioTestModule();
149 
150 protected:
151     virtual void initialize();
152     virtual bool received(Message& msg, int id);
153     virtual bool commandComplete(Message& msg, const String& partLine,
154 	const String& partWord);
155     bool onCmdControl(Message& msg);
156     bool test(const String& cmd = String::empty(),
157 	const NamedList& params = NamedList::empty());
158     void processRadioDataFile(NamedList& params);
159 };
160 
161 INIT_PLUGIN(RadioTestModule);
162 static RadioTest* s_test = 0;
163 static Mutex s_testMutex(false,"RadioTest");
164 
threadIdleIntervals(unsigned int ms)165 static inline unsigned int threadIdleIntervals(unsigned int ms)
166 {
167     return (ms + Thread::idleMsec()) / Thread::idleMsec();
168 }
169 
validFloatSample(float val)170 static inline bool validFloatSample(float val)
171 {
172     return (val >= -1.0F) && (val <= 1.0F);
173 }
174 
encloseDashes(String & s,bool extra=false)175 static inline const char* encloseDashes(String& s, bool extra = false)
176 {
177     static const String s1 = "\r\n-----";
178     if (s)
179 	s = s1 + (extra ? "\r\n" : "") + s + s1;
180     return s.safe();
181 }
182 
samplesf2bytes(unsigned int samples)183 static inline unsigned int samplesf2bytes(unsigned int samples)
184 {
185     return samples * 2 * sizeof(float);
186 }
187 
dumpSamplesFloat(String & s,const DataBlock & buf,const char * fmt4,const char * fmt,const char * sep,unsigned int maxDump=0)188 static String& dumpSamplesFloat(String& s, const DataBlock& buf,
189     const char* fmt4, const char* fmt, const char* sep, unsigned int maxDump = 0)
190 {
191     unsigned int samples = buf.length() / (2 * sizeof(float));
192     const float* f = (const float*)buf.data();
193     if (!(f && samples))
194 	return s;
195     if (maxDump && maxDump < samples)
196 	samples = maxDump;
197     String tmp;
198     unsigned int a = samples / 4;
199     for (; a; a--, f += 8)
200 	s.append(tmp.printf(512,fmt4,f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7]),sep);
201     for (a = samples % 4; a; a--, f += 2)
202 	s.append(tmp.printf(fmt,f[0],f[1]),sep);
203     return s;
204 }
205 
dumpSamplesInt16(String & s,const DataBlock & buf,const char * fmt4,const char * fmt,const char * sep,unsigned int maxDump=0)206 static String& dumpSamplesInt16(String& s, const DataBlock& buf,
207     const char* fmt4, const char* fmt, const char* sep, unsigned int maxDump = 0)
208 {
209     unsigned int samples = buf.length() / (2 * sizeof(int16_t));
210     const int16_t* f = (const int16_t*)buf.data();
211     if (!(f && samples))
212 	return s;
213     if (maxDump && maxDump < samples)
214 	samples = maxDump;
215     String tmp;
216     unsigned int a = samples / 4;
217     for (; a; a--, f += 8)
218 	s.append(tmp.printf(512,fmt4,f[0],f[1],f[2],f[3],f[4],f[5],f[6],f[7]),sep);
219     for (a = samples % 4; a; a--, f += 2)
220 	s.append(tmp.printf(fmt,f[0],f[1]),sep);
221     return s;
222 }
223 
boolSetError(String & s,const char * e=0)224 static inline bool boolSetError(String& s, const char* e = 0)
225 {
226     s = e;
227     return false;
228 }
229 
230 // Parse a comma separated list of float values to complex vector
parseVector(String & error,const String & str,ComplexVector & buf)231 static bool parseVector(String& error, const String& str, ComplexVector& buf)
232 {
233     if (!str)
234 	return boolSetError(error,"empty");
235     ObjList* list = str.split(',');
236     unsigned int len = list->length();
237     if ((len < 2) || (len % 2) != 0)
238 	return boolSetError(error,"invalid length");
239     buf.resetStorage(len);
240     ObjList* o = list;
241     for (float* b = (float*)buf.data(); o; o = o->next(), b++) {
242 	if (!o->get())
243 	    continue;
244 	*b = (static_cast<String*>(o->get()))->toDouble();
245 	if (!validFloatSample(*b))
246 	    break;
247     }
248     TelEngine::destruct(list);
249     return (o == 0) ? true : boolSetError(error,"invalid data range");
250 }
251 
generateCircleQuarter(Complex * & c,float amplitude,float i,float q,unsigned int loops,float angle,float iSign,float qSign)252 static inline void generateCircleQuarter(Complex*& c, float amplitude, float i, float q,
253     unsigned int loops, float angle, float iSign, float qSign)
254 {
255     (c++)->set(i * amplitude,q * amplitude);
256     if (!loops)
257 	return;
258     float angleStep = M_PI_2 / (loops + 1);
259     if (angle)
260 	angleStep = -angleStep;
261     iSign *= amplitude;
262     qSign *= amplitude;
263     for (; loops; --loops, ++c) {
264 	angle += angleStep;
265 	c->set(iSign * ::cosf(angle),qSign * ::sinf(angle));
266     }
267 }
268 
269 // Parse a complex numbers pattern
270 // forcePeriodic=true: Force lenExtend=false and lenRequired=true for periodic
271 //                     patterns (like 'circle')
272 // lenExtend=true: Extend destination buffer to be minimum 'len'. 'lenRequired' is ignored
273 // lenRequired=true: 'len' MUST be a multiple of generated vector's length
buildVector(String & error,const String & pattern,ComplexVector & vector,unsigned int len=0,bool forcePeriodic=true,bool lenExtend=true,bool lenRequired=false,unsigned int * pLen=0)274 static bool buildVector(String& error, const String& pattern, ComplexVector& vector,
275     unsigned int len = 0, bool forcePeriodic = true, bool lenExtend = true,
276     bool lenRequired = false, unsigned int* pLen = 0)
277 {
278     if (!pattern)
279 	return boolSetError(error,"empty");
280     bool isPeriodic = false;
281     String p = pattern;
282     ComplexVector v;
283     // Check for circles
284     if (p.startSkip("circle",false)) {
285 	unsigned int cLen = 4;
286 	bool rev = false;
287 	float div = 1;
288 	if (!p || p == YSTRING("_reverse"))
289 	    // circle[_reverse]
290 	    rev = !p.null();
291 	else if (p.startSkip("_div_",false)) {
292 	    // circle_div[_reverse]_{divisor}
293 	    rev = p.startSkip("reverse_",false);
294 	    if (!p)
295 		return boolSetError(error);
296 	    div = p.toDouble();
297 	}
298 	else if (p.startSkip("_points_",false)) {
299 	    // circle_points[_reverse]_{value}[_div_{divisor}]
300 	    rev = p.startSkip("reverse_",false);
301 	    if (!p)
302 		return boolSetError(error);
303 	    int pos = p.find('_');
304 	    if (pos < 0)
305 		cLen = p.toInteger(0,0,0);
306 	    else {
307 		// Expecting div
308 		cLen = p.substr(0,pos).toInteger(0,0,0);
309 		p = p.substr(pos + 1);
310 		if (!(p.startSkip("div_",false) && p))
311 		    return boolSetError(error);
312 		div = p.toDouble();
313 	    }
314 	}
315 	else
316 	    return boolSetError(error);
317 	// Circle length MUST be a multiple of 4
318 	if (!cLen || (cLen % 4) != 0)
319 	    return boolSetError(error,"invalid circle length");
320 	if (div < 1)
321 	    return boolSetError(error,"invalid circle div");
322 	v.resetStorage(cLen);
323 	Complex* c = v.data();
324 	float amplitude = 1.0F / div;
325 	float direction = rev ? -1 : 1;
326 	unsigned int n = (cLen - 4) / 4;
327 	generateCircleQuarter(c,amplitude,1,0,n,0,1,direction);
328 	generateCircleQuarter(c,amplitude,0,direction,n,M_PI_2,-1,direction);
329 	generateCircleQuarter(c,amplitude,-1,0,n,0,-1,-direction);
330 	generateCircleQuarter(c,amplitude,0,-direction,n,M_PI_2,1,-direction);
331 	isPeriodic = true;
332     }
333     else if (pattern == YSTRING("zero")) {
334 	// Fill with 0
335 	vector.resetStorage(len ? len : 1);
336 	if (pLen)
337 	    *pLen = 1;
338 	return true;
339     }
340     else if (p.startSkip("fill_",false)) {
341 	// Fill with value: fill_{real}_{imag}
342 	int pos = p.find('_');
343 	if (pos < 1 || p.find('_',pos + 1) > 0)
344 	    return boolSetError(error);
345 	float re = p.substr(0,pos).toDouble();
346 	float im = p.substr(pos + 1).toDouble();
347 	if (validFloatSample(re) && validFloatSample(im)) {
348 	    vector.resetStorage(len ? len : 1);
349 	    vector.fill(Complex(re,im));
350 	    if (pLen)
351 		*pLen = 1;
352 	    return true;
353 	}
354 	return boolSetError(error,"invalid data range");
355     }
356     else if (!parseVector(error,pattern,v))
357 	// Parse list of values
358 	return false;
359     if (!v.length())
360 	return boolSetError(error,"empty result");
361     if (pLen)
362 	*pLen = v.length();
363     if (isPeriodic && forcePeriodic) {
364 	lenExtend = false;
365 	lenRequired = true;
366     }
367     // Try to extend data
368     if (!len || (len == v.length()) || !(lenExtend || lenRequired))
369 	vector = v;
370     else {
371 	if (lenExtend) {
372 	    if (len < v.length())
373 		len = v.length();
374 	    unsigned int rest = len % v.length();
375 	    if (rest)
376 		len += v.length() - rest;
377 	}
378 	else if ((len < v.length()) || ((len % v.length()) != 0))
379 	    return boolSetError(error,"required/actual length mismatch");
380 	vector.resetStorage(len);
381 	for (unsigned int i = 0; (i + v.length()) <= len; i += v.length())
382 	    vector.slice(i,v.length()).copy(v,v.length());
383     }
384     return true;
385 }
386 
387 
388 //
389 // RadioTest
390 //
RadioTest(const NamedList & params,const NamedList & radioParams)391 RadioTest::RadioTest(const NamedList& params, const NamedList& radioParams)
392     : Thread("RadioTest",Thread::priority(params["priority"])),
393     m_radio(0),
394     m_recv(0),
395     m_started(false),
396     m_repeat(0),
397     m_init(""),
398     m_params(params),
399     m_radioParams(radioParams),
400     m_tx(true),
401     m_txYield(true),
402     m_newTxData(false),
403     m_phase(0),
404     m_sendBufCount(0),
405     m_pulse(0),
406     m_rx(false),
407     m_rxSkippedSamples(0)
408 {
409     m_params.setParam("orig_test_name",params.c_str());
410     m_params.assign(__plugin.name() + "/" + params.c_str());
411     debugName(m_params);
412     debugChain(&__plugin);
413 }
414 
command(const String & cmd,const NamedList & params)415 bool RadioTest::command(const String& cmd, const NamedList& params)
416 {
417     Debug(this,DebugNote,"Unknown command '%s' [%p]",cmd.c_str(),this);
418     return false;
419 }
420 
start(const NamedList & params,const NamedList & radioParams)421 bool RadioTest::start(const NamedList& params, const NamedList& radioParams)
422 {
423     s_test = new RadioTest(params,radioParams);
424     if (s_test->startup())
425 	return true;
426     delete s_test;
427     Debug(&__plugin,DebugNote,"Failed to start test thread");
428     return false;
429 }
430 
run()431 void RadioTest::run()
432 {
433     readStop();
434     m_started = true;
435     notify("start");
436     m_init.clearParams();
437     bool ok = false;
438     unsigned int repeat = m_params.getIntValue("repeat",1,1);
439     Debug(this,DebugInfo,"Initializing repeat=%u [%p]",repeat,this);
440     String error;
441     while (true) {
442 #define TEST_FAIL_BREAK(str,warn) {\
443 	error = str; \
444 	if (warn) \
445 	    Debug(this,DebugNote,"%s [%p]",error.c_str(),this); \
446 	ok = false; \
447 	break; \
448     }
449 	// Init test data
450 	if (!m_params.getBoolValue("init_only")) {
451 	    m_tx.enabled = true;
452 	    m_txYield = m_params.getBoolValue("tx_yield",true);
453 	    if (!setTxData())
454 		TEST_FAIL_BREAK("Failed to set TX data",false);
455 	    m_sendBufCount = m_params.getIntValue("send_buffers",0,0);
456 	    if (m_sendBufCount)
457 		m_init.addParam("send_buffers",String(m_sendBufCount));
458 	    m_rx.enabled = !m_params.getBoolValue("sendonly");
459 	    if (m_rx.enabled) {
460 		unsigned int n = m_params.getIntValue("readsamples",256,1);
461 		m_bufs.reset(n,0);
462 		m_crt.assign(0,samplesf2bytes(m_bufs.bufSamples()));
463 		m_aux = m_crt;
464 		m_extra = m_crt;
465 		m_bufs.crt.samples = (float*)m_crt.data(0);
466 		m_bufs.aux.samples = (float*)m_aux.data(0);
467 		m_bufs.extra.samples = (float*)m_extra.data(0);
468 		m_init.addParam("readsamples",String(n));
469 	    }
470 	}
471 	// Create radio
472 	Message m("radio.create");
473 	m.setParam("module",__plugin.name());
474 	m.copyParams(m_radioParams);
475 	bool radioOk = Engine::dispatch(m);
476 	NamedPointer* np = YOBJECT(NamedPointer,m.getParam(YSTRING("interface")));
477 	m_radio = np ? YOBJECT(RadioInterface,np) : 0;
478 	if (!m_radio) {
479 	    error = m[YSTRING("error")];
480 	    error.printf("Failed to create radio interface: %s",
481 		error.safe(radioOk ? "Missing interface" : "Message not handled"));
482 	    TEST_FAIL_BREAK(error,true);
483 	}
484 	np->takeData();
485 
486 	NamedList files("");
487 	for (ObjList* o = m_params.paramList()->skipNull(); o; o = o->skipNext()) {
488 	    NamedString* ns = static_cast<NamedString*>(o->get());
489 	    if (!ns->name().startsWith("file:"))
490 		continue;
491 	    String file = *ns;
492 	    NamedList tmp("");
493 	    tmp.addParam("now",String(Time::secNow()));
494 	    tmp.replaceParams(file);
495 	    if (file && execute("devparam:" + ns->name().substr(5),file,true,0))
496 		files.addParam(ns->name(),file);
497 	}
498 	m_params.clearParam("file",':');
499 	m_params.copyParams(files);
500 
501 	if (!execute(m_params,"init:"))
502 	    TEST_FAIL_BREAK("Failed to execute initial command(s)",false);
503 	unsigned int status = m_radio->initialize(m_radioParams);
504 	if (status) {
505 	    if (RadioInterface::Pending == status) {
506 		unsigned int wait = m_params.getIntValue("wait_pending_init",0,0);
507 		if (wait)
508 		    status = m_radio->pollPending(RadioInterface::PendingInitialize,wait);
509 		else {
510 		    while (!Thread::check(false)) {
511 			status = m_radio->pollPending(RadioInterface::PendingInitialize);
512 			if (!status || RadioInterface::Pending != status)
513 			    break;
514 			Thread::idle();
515 		    }
516 		    if (Thread::check(false))
517 			status = RadioInterface::Cancelled;
518 		}
519 	    }
520 	    if (status) {
521 		if (status != RadioInterface::Cancelled)
522 		    error.printf("Failed to initialize radio interface: %u %s",
523 			status,RadioInterface::errorName(status));
524 		else if (Thread::check(false)) {
525 		    TEST_FAIL_BREAK("cancelled",false);
526 		}
527 		else
528 		    error = "device closed while initializing";
529 		TEST_FAIL_BREAK(error,true);
530 	    }
531 	}
532 	if (!execute(m_params,"cmd:"))
533 	    TEST_FAIL_BREAK("Failed to execute post init command(s)",false);
534 	if (!wait("wait_after_init"))
535 	    TEST_FAIL_BREAK("cancelled",false);
536 	ok = true;
537 	if (!(m_tx.enabled || m_rx.enabled))
538 	    break;
539 	if (m_rx.enabled) {
540 	    updateTs(false);
541 	    m_recv = RadioTestRecv::start(this,
542 		Thread::priority(m_params.getValue("recv_priority")));
543 	    if (!m_recv)
544 		TEST_FAIL_BREAK("Failed to start read data thread",true);
545 	}
546 	String s;
547 	m_init.dump(s,"\r\n");
548 	Debug(this,DebugInfo,"Starting [%p]%s",this,encloseDashes(s,true));
549 	// Run
550 	while (!Thread::check(false)) {
551 	    if (!write())
552 		TEST_FAIL_BREAK("Send data failed",false);
553 	    if (m_rx.enabled && !m_recv && !Thread::check(false))
554 		TEST_FAIL_BREAK("Read data thread abnormally terminated",true);
555 	}
556 	if (Thread::check(false))
557 	    error = "stopped";
558 	readStop();
559 #undef TEST_FAIL_BREAK
560 	break;
561     }
562     if (ok && (repeat > 1) && !Thread::check(false))
563 	m_repeat = --repeat;
564     terminated(error);
565 }
566 
terminated(const char * error)567 void RadioTest::terminated(const char* error)
568 {
569     readStop();
570     s_testMutex.lock();
571     if (s_test == this)
572 	s_test = 0;
573     s_testMutex.unlock();
574     TelEngine::destruct(m_radio);
575     if (!m_started)
576 	return;
577     m_started = false;
578     RadioTestIO* txrx[2] = {&m_tx,&m_rx};
579     uint64_t now = Time::now();
580     NamedList report("");
581     for (uint8_t i = 0; i < 2; i++) {
582 	RadioTestIO& io = *(txrx[i]);
583 	if (!io.enabled)
584 	    continue;
585 	String prefix = io.tx ? "tx_" : "rx_";
586 	report.addParam(prefix + "transferred",String(io.transferred));
587 	if (io.transferred) {
588 	    unsigned int sec = (unsigned int)((now - io.startTime) / 1000000);
589 	    if (sec)
590 		report.addParam(prefix + "samplerate",String(io.transferred / sec));
591 	}
592 	report.addParam(prefix + "ts",String(io.ts));
593     }
594     if (m_rxSkippedSamples)
595 	report.addParam("rx_skipped_samples",String(m_rxSkippedSamples));
596     String s;
597     report.dump(s,"\r\n");
598     Debug(this,DebugInfo,"Terminated [%p]%s",this,encloseDashes(s,true));
599     notify("stop",error,&report);
600     if (!m_repeat)
601 	return;
602     Debug(this,DebugNote,"Restarting repeat=%u [%p]",m_repeat,this);
603     Message* m = new Message("chan.control");
604     m->addParam("module",__plugin.name());
605     m->addParam("component",__plugin.name());
606     m->addParam("operation","restart");
607     m->addParam("name",m_params.getValue("orig_test_name"));
608     m->addParam("repeat",String(m_repeat));
609     m->copySubParams(m_params,"file:",false,true);
610     Engine::enqueue(m);
611 }
612 
setTxData()613 bool RadioTest::setTxData()
614 {
615     const String& pattern = m_params["txdata"];
616     if (!pattern) {
617 	Debug(this,DebugConf,"Missing tx data pattern [%p]",this);
618 	return false;
619     }
620     m_newTxData = true;
621     m_phase = 0;
622     m_pulse = 0;
623     String tp;
624     if (pattern == "two-circles") {
625 	m_sendBufData.resetStorage(m_params.getIntValue("txdata_length",819,50));
626 	m_init.addParam("txpattern",pattern);
627     }
628     else if (pattern == "pulse") {
629 	unsigned int samples = m_params.getIntValue("txdata_length",10000,50);
630 	m_sendBufData.resetStorage(samples);
631 	unsigned int defVal = (samples > 2) ? (samples - 2) : 2;
632 	m_pulse = m_params.getIntValue("pulse",defVal,2,10000000);
633 	String pattern = m_params.getValue("pulse_pattern","1,1,-1,-1");
634 	String e;
635 	bool ok = parseVector(e,pattern,m_pulseData);
636 	if (!ok || m_pulseData.length() < 2 || m_pulseData.length() > (m_pulse / 3)) {
637 	    bool sh = (m_pulseData.length() < 2);
638 	    Debug(this,DebugConf,"Invalid pulse_pattern '%s': %s [%p]",
639 		pattern.c_str(),e.safe(sh ? "too short" : "too long"),this);
640 	    return false;
641 	}
642 	m_init.addParam("txpattern",pattern);
643 	m_init.addParam("pulse",String(m_pulse));
644 	String s;
645 	unsigned int h = m_pulseData.length() > 10 ? 10 : m_pulseData.length();
646 	m_pulseData.head(h).dump(s,Math::dumpComplex," ","%g,%g");
647 	m_init.addParam("pulse_pattern",pattern);
648     }
649     else {
650 	m_newTxData = false;
651 	String e;
652 	if (!buildVector(e,pattern,m_sendBufData)) {
653 	    Debug(this,DebugConf,"Invalid tx data pattern '%s': %s [%p]",
654 		pattern.c_str(),e.safe("unknown"),this);
655 	    return false;
656 	}
657 	unsigned int len = m_sendBufData.length();
658 	int n = m_params.getIntValue("txdata_repeat");
659 	if (n > 0) {
660 	    ComplexVector tmp(m_sendBufData);
661 	    m_sendBufData.resetStorage(n * len);
662 	    for (unsigned int i = 0; i < m_sendBufData.length(); i += len)
663 		m_sendBufData.slice(i,len).copy(tmp,len);
664 	}
665 	m_init.addParam("txpattern",pattern.substr(0,50));
666 	String s;
667 	m_sendBufData.head(len > 20 ? 20 : len).dump(s,Math::dumpComplex,",","%g,%g");
668 	if (!s.startsWith(pattern))
669 	    m_init.addParam("txdata",s);
670     }
671     m_init.addParam("send_samples",String(m_sendBufData.length()));
672     return true;
673 }
674 
regenerateTxData()675 void RadioTest::regenerateTxData()
676 {
677     // Fs / 4 data
678     static const float s_cs4[4] = {1,0,-1,0};
679     // Fs / 8 data
680     static const float s_r2 = M_SQRT1_2;
681     static const float s_cs8[8] = {1,s_r2,0,-s_r2,-1,-s_r2,0,s_r2};
682 
683     Complex* last = 0;
684     Complex* c = m_sendBufData.data(0,m_sendBufData.length(),last);
685     if (m_pulse) {
686 	m_sendBufData.bzero();
687 	for (; c != last; ++c, ++m_phase) {
688 	    unsigned int idx = m_phase % m_pulse;
689 	    if (idx < m_pulseData.length())
690 		*c = m_pulseData[idx];
691 	}
692     }
693     else
694 	for (; c != last; ++c, ++m_phase)
695 	    c->set(0.5 * (s_cs4[m_phase % 4] + s_cs8[m_phase % 8]),
696 		-0.5 * (s_cs4[(m_phase + 1) % 4] + s_cs8[(m_phase + 2) % 8]));
697 }
698 
execute(const String & cmd,const String & param,bool fatal,const NamedList * params)699 bool RadioTest::execute(const String& cmd, const String& param, bool fatal,
700     const NamedList* params)
701 {
702     XDebug(this,DebugAll,"execute(%s,%s) [%p]",cmd.c_str(),param.c_str(),this);
703     unsigned int c = RadioInterface::Failure;
704     if (cmd == YSTRING("samplerate"))
705 	c = m_radio->setSampleRate(param.toInteger());
706     else if (cmd == YSTRING("filter"))
707 	c = m_radio->setFilter(param.toInteger());
708     else if (cmd == YSTRING("txfrequency"))
709 	c = m_radio->setTxFreq(param.toInteger());
710     else if (cmd == YSTRING("rxfrequency"))
711 	c = m_radio->setRxFreq(param.toInteger());
712     else if (cmd == YSTRING("loopback"))
713 	c = m_radio->setLoopback(param);
714     else if (cmd == YSTRING("calibrate"))
715 	c = m_radio->calibrate();
716     else if (cmd.startsWith("devparam:")) {
717 	NamedList tmp("");
718 	if (params)
719 	    tmp.copySubParams(*params,cmd + "_");
720 	tmp.setParam("cmd:" + cmd,param);
721 	c = m_radio->setParams(tmp);
722     }
723     else {
724 	Debug(this,DebugNote,"Unhandled command '%s' [%p]",cmd.c_str(),this);
725 	return true;
726     }
727     if (c == 0 || !fatal)
728 	return true;
729     Debug(this,DebugNote,"'%s' failed with %u '%s' [%p]",cmd.c_str(),c,
730 	RadioInterface::errorName(c),this);
731     return false;
732 }
733 
execute(const NamedList & cmds,const char * prefix)734 bool RadioTest::execute(const NamedList& cmds, const char* prefix)
735 {
736     for (const ObjList* o = cmds.paramList()->skipNull(); o; o = o->skipNext()) {
737 	const NamedString* ns = static_cast<const NamedString*>(o->get());
738 	String s = ns->name();
739 	if (s.startSkip(prefix,false) &&
740 	    !execute(s,*ns,cmds.getBoolValue(s + "_fatal",true),&cmds))
741 	    return false;
742     }
743     return true;
744 }
745 
write()746 bool RadioTest::write()
747 {
748     if (!m_tx.startTime)
749 	m_tx.startTime = Time::now();
750     if (m_newTxData)
751 	regenerateTxData();
752     else if (m_txYield)
753 	Thread::yield(false);
754     if (!m_tx.ts)
755 	updateTs(true);
756     unsigned int code = m_radio->send(m_tx.ts,(float*)m_sendBufData.data(),
757 	m_sendBufData.length());
758     if (!code) {
759 	m_tx.ts += m_sendBufData.length();
760 	m_tx.transferred += m_sendBufData.length();
761 	if (!m_sendBufCount)
762 	    return true;
763 	m_sendBufCount--;
764 	return m_sendBufCount > 0;
765     }
766     if (code != RadioInterface::Cancelled)
767 	Debug(this,DebugNote,"Send error: %u '%s' [%p]",
768 	    code,RadioInterface::errorName(code),this);
769     return false;
770 }
771 
read()772 bool RadioTest::read()
773 {
774     if (!m_rx.startTime)
775 	m_rx.startTime = Time::now();
776     if (!m_rx.ts)
777 	updateTs(false);
778     unsigned int skippedBuffs = 0;
779     unsigned int code = m_radio->read(m_rx.ts,m_bufs,skippedBuffs);
780     if (skippedBuffs) {
781 	uint64_t skipped = skippedBuffs * m_bufs.bufSamples();
782 	m_rxSkippedSamples += skipped;
783 	m_rx.transferred += skipped;
784     }
785     if (!code) {
786 	if (m_bufs.full(m_bufs.crt))
787 	    m_rx.transferred += m_bufs.bufSamples();
788 	return true;
789     }
790     if (code != RadioInterface::Cancelled)
791 	Debug(this,DebugNote,"Recv error: %u '%s' [%p]",
792 	    code,RadioInterface::errorName(code),this);
793     return false;
794 }
795 
readTerminated(RadioTestRecv * th)796 void RadioTest::readTerminated(RadioTestRecv* th)
797 {
798     Lock lck(s_testMutex);
799     if (m_recv == th)
800 	m_recv = 0;
801 }
802 
readStop()803 void RadioTest::readStop()
804 {
805     if (!m_recv)
806 	return;
807     Lock lck(s_testMutex);
808     if (!m_recv)
809 	return;
810     m_recv->cancel();
811     lck.drop();
812     // Wait for 5 seconds before hard cancelling
813     unsigned int n = threadIdleIntervals(5000);
814     while (m_recv && n) {
815 	Thread::idle();
816 	n--;
817     }
818     lck.acquire(s_testMutex);
819     if (m_recv)
820 	hardCancelRecv();
821 }
822 
hardCancelRecv()823 void RadioTest::hardCancelRecv()
824 {
825     if (!m_recv)
826 	return;
827     Debug(this,DebugWarn,"Hard cancelling read data thread (%p) [%p]",m_recv,this);
828     m_recv->cancel(true);
829     m_recv = 0;
830 }
831 
wait(const String & param)832 bool RadioTest::wait(const String& param)
833 {
834     unsigned int wait = m_params.getIntValue(param,0,0);
835     if (!wait)
836 	return true;
837     Debug(this,DebugInfo,"Waiting '%s' %ums [%p]",param.c_str(),wait,this);
838     unsigned int n = threadIdleIntervals(wait);
839     for (; n && !Thread::check(false); n--)
840 	Thread::idle();
841     return n == 0;
842 }
843 
notify(const char * state,const char * error,const NamedList * params) const844 void RadioTest::notify(const char* state, const char* error, const NamedList* params) const
845 {
846     const String& id = m_params[YSTRING("notify")];
847     if (!id)
848 	return;
849     Message* m = new Message(m_params.getValue(YSTRING("notifymsg"),"chan.notify"));
850     m->addParam("module",__plugin.name());
851     m->addParam("test_name",debugName());
852     m->addParam("id",id);
853     m->addParam("state",state);
854     m->addParam("error",error,false);
855     if (params)
856 	m->copyParams(*params);
857     Engine::enqueue(m);
858 }
859 
860 
861 //
862 // RadioTestModule
863 //
RadioTestModule()864 RadioTestModule::RadioTestModule()
865     : Module("radiotest","misc")
866 {
867     Output("Loaded module Radio Test");
868 }
869 
~RadioTestModule()870 RadioTestModule::~RadioTestModule()
871 {
872     Output("Unloading module Radio Test");
873     if (s_test)
874 	Debug(this,DebugWarn,"Exiting while test is running!!!");
875 }
876 
initialize()877 void RadioTestModule::initialize()
878 {
879     Output("Initializing module Radio Test");
880     if (!relayInstalled(Halt)) {
881 	setup();
882 	installRelay(Halt,120);
883 	installRelay(Control);
884     }
885 }
886 
received(Message & msg,int id)887 bool RadioTestModule::received(Message& msg, int id)
888 {
889     if (id == Control) {
890 	if (msg[YSTRING("component")] == name())
891 	    return onCmdControl(msg);
892 	return false;
893     }
894     else if (id == Halt)
895 	test();
896     return Module::received(msg,id);
897 }
898 
commandComplete(Message & msg,const String & partLine,const String & partWord)899 bool RadioTestModule::commandComplete(Message& msg, const String& partLine,
900     const String& partWord)
901 {
902     if (partLine == YSTRING("control")) {
903 	itemComplete(msg.retValue(),name(),partWord);
904 	return false;
905     }
906     String tmp = partLine;
907     if (tmp.startSkip("control") && tmp == name()) {
908 	// Complete commands
909 	itemComplete(msg.retValue(),"start",partWord);
910 	itemComplete(msg.retValue(),"exec",partWord);
911 	itemComplete(msg.retValue(),"stop",partWord);
912 	itemComplete(msg.retValue(),"radiodatafile",partWord);
913 	itemComplete(msg.retValue(),"help",partWord);
914 	return false;
915     }
916     return Module::commandComplete(msg,partLine,partWord);
917 }
918 
onCmdControl(Message & msg)919 bool RadioTestModule::onCmdControl(Message& msg)
920 {
921     static const char* s_help =
922 	"\r\ncontrol radiotest {start [name=conf_sect_name]|stop|exec}"
923 	"\r\n  Test commands"
924 	"\r\ncontrol radiotest radiodatafile [sect=conf_sect_name]"
925 	"\r\n  Read radio data file. Process it according to given section parameters.";
926 
927     const String& cmd = msg[YSTRING("operation")];
928     if (cmd == YSTRING("help")) {
929 	msg.retValue() << s_help;
930 	return true;
931     }
932     if (cmd == YSTRING("radiodatafile")) {
933 	processRadioDataFile(msg);
934 	return true;
935     }
936     return test(cmd,msg);
937 }
938 
939 // control module_name test oper={start|stop|.....} params...
test(const String & cmd,const NamedList & list)940 bool RadioTestModule::test(const String& cmd, const NamedList& list)
941 {
942     static bool s_exec = false;
943 
944     Lock lck(s_testMutex);
945     while (s_exec) {
946 	lck.drop();
947 	Thread::idle();
948 	if (Thread::check(false))
949 	    return false;
950 	lck.acquire(s_testMutex);
951     }
952     bool bRet = true;
953     bool start = (cmd == YSTRING("start"));
954     bool restart = !start && (cmd == YSTRING("restart"));
955     if (start || restart || !cmd || cmd == YSTRING("stop")) {
956 	// Stop the test
957 	while (s_test) {
958 	    s_test->cancel();
959 	    if (s_test->m_recv)
960 		s_test->m_recv->cancel();
961 	    lck.drop();
962 	    // Wait for 5 seconds before hard cancelling
963 	    unsigned int n = threadIdleIntervals(5000);
964 	    while (s_test && n) {
965 		Thread::idle();
966 		n--;
967 	    }
968 	    lck.acquire(s_testMutex);
969 	    if (!s_test)
970 		break;
971 	    s_test->hardCancelRecv();
972 	    Debug(this,DebugWarn,"Hard cancelling test thread (%p)",s_test);
973 	    s_test->cancel(true);
974 	    s_test = 0;
975 	}
976 	while (start || restart) {
977 	    bRet = false;
978 	    const String& over = start ? list[YSTRING("override")] : String::empty();
979 	    const char* n = 0;
980 	    if (start)
981 		n = list.getValue("test_name","test");
982 	    NamedList params(list.getValue(YSTRING("name"),n));
983 	    if (!params) {
984 		Debug(this,DebugNote,"Failed to start test: missing name");
985 		break;
986 	    }
987 	    Configuration cfg(Engine::configFile(name()),!over);
988 	    NamedList* sect = cfg.getSection(params);
989 	    if (sect) {
990 		const char* inc = sect->getValue(YSTRING("include"));
991 		if (inc)
992 		    params.copyParams(*cfg.createSection(inc));
993 		params.copyParams(*sect);
994 	    }
995 	    else if (!over) {
996 		Debug(this,DebugNote,"Failed to start test '%s': missing config section",
997 		    params.c_str());
998 		break;
999 	    }
1000 	    const char* radioSect = params.getValue("radio_section","radio");
1001 	    NamedList radioParams(*cfg.createSection(radioSect));
1002 	    if (over) {
1003 		// Boolean true: copy all parameters
1004 		// Otherwise: it indicates a prefix
1005 		if (over.toBoolean())
1006 		    params.copyParams(list);
1007 		else
1008 		    params.copySubParams(list,over,true,true);
1009 		radioParams.copySubParams(list,"radio_",true,true);
1010 	    }
1011 	    else if (restart) {
1012 		unsigned int repeat = list.getIntValue("repeat",0,0);
1013 		if (!repeat)
1014 		    break;
1015 		params.setParam("repeat",String(repeat));
1016 		params.clearParam("file",':');
1017 		params.copySubParams(list,"file:",false,true);
1018 	    }
1019 	    params.setParam(YSTRING("first"),String::boolText(start));
1020 	    bRet = RadioTest::start(params,radioParams);
1021 	    break;
1022 	}
1023     }
1024     else if (s_test)
1025 	bRet = s_test->command(cmd,list);
1026     else
1027 	Debug(this,DebugInfo,"Test is not running");
1028     s_exec = false;
1029     return bRet;
1030 }
1031 
processRadioDataFile(NamedList & params)1032 void RadioTestModule::processRadioDataFile(NamedList& params)
1033 {
1034     Configuration cfg(Engine::configFile(name()));
1035     const char* s = params.getValue("sect","radiodatafile");
1036     NamedList* p = cfg.getSection(s);
1037     if (!p) {
1038 	Debug(this,DebugNote,
1039 	    "Can't handle radio data file process: no section '%s' in config",s);
1040 	return;
1041     }
1042     const char* file = p->getValue("input");
1043     if (!file) {
1044 	Debug(this,DebugNote,"Radio data file process sect='%s': missing file",s);
1045 	return;
1046     }
1047     RadioDataFile d("RadioTest");
1048     if (!d.open(file,0,this))
1049 	return;
1050     const RadioDataDesc& desc = d.desc();
1051     String error;
1052 #define RADIO_FILE_ERROR(what,value) { \
1053     error << what << " " << value; \
1054     break; \
1055 }
1056     while (true) {
1057 	if (desc.m_signature[2])
1058 	    RADIO_FILE_ERROR("unhandled version",desc.m_signature[2]);
1059 	if (desc.m_sampleLen != 2)
1060 	    RADIO_FILE_ERROR("unhandled sample length",desc.m_sampleLen);
1061 	if (desc.m_ports != 1)
1062 	    RADIO_FILE_ERROR("unhandled ports",desc.m_ports);
1063 	String fmt;
1064 	unsigned int sz = 0;
1065 	switch (desc.m_elementType) {
1066 	    case RadioDataDesc::Float:
1067 		fmt = p->getValue(YSTRING("fmt-float"),"%+g%+gj");
1068 		sz = sizeof(float);
1069 		break;
1070 	    case RadioDataDesc::Int16:
1071 		fmt = p->getValue(YSTRING("fmt-int"),"%+d%+dj");
1072 		sz = sizeof(int16_t);
1073 		break;
1074 	    default:
1075 		RADIO_FILE_ERROR("unhandled element type",desc.m_elementType);
1076 	}
1077 	if (error)
1078 	    break;
1079 	File fOut;
1080 	const String& output = (*p)[YSTRING("output")];
1081 	if (output && !fOut.openPath(output,true,false,true,false,false,true)) {
1082 	    String tmp;
1083 	    Thread::errorString(tmp,fOut.error());
1084 	    error.printf("Failed to open output file '%s' - %d %s",
1085 		output.c_str(),fOut.error(),tmp.c_str());
1086 	    break;
1087 	}
1088 	const String* sepParam = p->getParam(YSTRING("separator"));
1089 	const char* sep = sepParam ? sepParam->c_str() : " ";
1090 	bool dumpData = fOut.valid() ? true : p->getBoolValue(YSTRING("dumpdata"),true);
1091 	unsigned int dumpStart = p->getIntValue(YSTRING("recstart"),1,1);
1092 	unsigned int dumpCount = p->getIntValue(YSTRING("reccount"),0,0);
1093 	unsigned int dumpMax = p->getIntValue(YSTRING("recsamples"),0,0);
1094 	const String& recFmt = (*p)[YSTRING("recformat")];
1095 	Debug(this,DebugAll,"Processing radio data file '%s'",file);
1096 	NamedList special("");
1097 	special.addParam("newline","\r\n");
1098 	special.addParam("tab","\t");
1099 	special.replaceParams(fmt);
1100 	String fmt4 = fmt + sep + fmt + sep + fmt + sep + fmt;
1101 	uint64_t ts = 0;
1102 	DataBlock buf;
1103 	unsigned int n = 0;
1104 	uint64_t oldTs = 0;
1105 	bool first = true;
1106 	unsigned int sampleBytes = sz * 2;
1107 	while (!Thread::check(false) && d.read(ts,buf,this) && buf.length()) {
1108 	    n++;
1109 	    if ((buf.length() % sampleBytes) != 0) {
1110 		error.printf("record=%u len=%u - length is not a multiple of samples",
1111 		    n,buf.length());
1112 		break;
1113 	    }
1114 	    if (n < dumpStart)
1115 		continue;
1116 	    String str;
1117 	    if (dumpData) {
1118 		if (!d.sameEndian() && !d.fixEndian(buf,sz))
1119 		    RADIO_FILE_ERROR("unhandled endiannes for element type",desc.m_elementType);
1120 		switch (desc.m_elementType) {
1121 		    case RadioDataDesc::Float:
1122 			dumpSamplesFloat(str,buf,fmt4,fmt,sep,dumpMax);
1123 			break;
1124 		    case RadioDataDesc::Int16:
1125 			dumpSamplesInt16(str,buf,fmt4,fmt,sep,dumpMax);
1126 			break;
1127 		    default:
1128 			RADIO_FILE_ERROR("unhandled element type",desc.m_elementType);
1129 		}
1130 		if (error)
1131 		    break;
1132 	    }
1133 	    int64_t delta = 0;
1134 	    if (first)
1135 		first = false;
1136 	    else
1137 		delta = ts - oldTs;
1138 	    oldTs = ts;
1139 	    unsigned int samples = buf.length() / sampleBytes;
1140 	    if (fOut.valid()) {
1141 		if (str) {
1142 		    if (recFmt) {
1143 			NamedList nl("");
1144 			nl.addParam("timestamp",String(ts));
1145 			nl.addParam("data",str);
1146 			nl.addParam("ts-delta",String(delta));
1147 			nl.addParam("samples",String(samples));
1148 			nl.addParam("newline","\r\n");
1149 			nl.addParam("separator",sep);
1150 			str = recFmt;
1151 			nl.replaceParams(str);
1152 		    }
1153 		    else
1154 			str += sep;
1155 		    int wr = str ? fOut.writeData(str.c_str(),str.length()) : str.length();
1156 		    if (wr != (int)str.length()) {
1157 			String tmp;
1158 			Thread::errorString(tmp,fOut.error());
1159 			error.printf("Failed to write (%d/%u) output file '%s' - %d %s",
1160 			    wr,str.length(),output.c_str(),fOut.error(),tmp.c_str());
1161 			break;
1162 		    }
1163 		}
1164 	    }
1165 	    else
1166 		Output("%u: TS=" FMT64U " bytes=%u samples=%u delta=" FMT64 "%s",
1167 		    n,ts,buf.length(),samples,delta,encloseDashes(str,true));
1168 	    if (dumpCount) {
1169 		dumpCount--;
1170 		if (!dumpCount)
1171 		    break;
1172 	    }
1173 	}
1174 	break;
1175     }
1176 #undef RADIO_FILE_ERROR
1177     if (error)
1178 	Debug(this,DebugNote,"Processing radio data file '%s': %s",file,error.c_str());
1179 }
1180 
1181 }; // anonymous namespace
1182 
1183 /* vi: set ts=8 sw=4 sts=4 noet enc=utf-8: */
1184