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