1 /**
2 * radio.cpp
3 * This file is part of the YATE Project http://YATE.null.ro
4 *
5 * Generic radio interface
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 <yateradio.h>
24 #include <string.h>
25
26 /*
27 #ifndef htobe32
28 #include <byteswap.h>
29
30 #ifdef LITTLE_ENDIAN
31
32 #define htobe16(x) __bswap_16 (x)
33 #define htole16(x) (x)
34 #define be16toh(x) __bswap_16 (x)
35 #define le16toh(x) (x)
36 #define htobe32(x) __bswap_32 (x)
37 #define htole32(x) (x)
38 #define be32toh(x) __bswap_32 (x)
39 #define le32toh(x) (x)
40 #define htobe64(x) __bswap_64 (x)
41 #define htole64(x) (x)
42 #define be64toh(x) __bswap_64 (x)
43 #define le64toh(x) (x)
44
45 #else
46
47 #define htobe16(x) (x)
48 #define htole16(x) __bswap_16 (x)
49 #define be16toh(x) (x)
50 #define le16toh(x) __bswap_16 (x)
51 #define htobe32(x) (x)
52 #define htole32(x) __bswap_32 (x)
53 #define be32toh(x) (x)
54 #define le32toh(x) __bswap_32 (x)
55 #define htobe64(x) (x)
56 #define htole64(x) __bswap_64 (x)
57 #define be64toh(x) (x)
58 #define le64toh(x) __bswap_64 (x)
59
60 #endif
61
62 #endif // htobe32
63 */
64 #include <sys/endian.h>
65
66
67 //#define DEBUG_RADIO_READ
68
69 #ifdef DEBUG_RADIO_READ
70 #define DebugRadioRead(args...) Debug(this,DebugAll,args)
71 #else
72 #ifdef _WINDOWS
73 #define DebugRadioRead do { break; } while
74 #else
75 #define DebugRadioRead(arg...)
76 #endif
77 #endif // DEBUG_RADIO_READ
78
79
80 using namespace TelEngine;
81
82 #define MAKE_NAME(x) {#x, RadioInterface::x}
83 static const TokenDict s_errorName[] = {
84 MAKE_NAME(HardwareIOError),
85 MAKE_NAME(NotInitialized),
86 MAKE_NAME(NotSupported),
87 MAKE_NAME(NotCalibrated),
88 MAKE_NAME(TooEarly),
89 MAKE_NAME(TooLate),
90 MAKE_NAME(OutOfRange),
91 MAKE_NAME(NotExact),
92 MAKE_NAME(DataLost),
93 MAKE_NAME(Saturation),
94 MAKE_NAME(RFHardwareFail),
95 MAKE_NAME(RFHardwareChange),
96 MAKE_NAME(EnvironmentalFault),
97 MAKE_NAME(InvalidPort),
98 MAKE_NAME(Pending),
99 MAKE_NAME(Cancelled),
100 MAKE_NAME(Failure),
101 MAKE_NAME(Timeout),
102 MAKE_NAME(HardwareNotAvailable),
103 MAKE_NAME(InsufficientSpeed),
104 MAKE_NAME(NoError),
105 {0,0}
106 };
107 #undef MAKE_NAME
108
RadioCapability()109 RadioCapability::RadioCapability()
110 {
111 ::memset(this,0,sizeof(*this));
112 }
113
114
115 // Calculate the length of a sample in elements
sampleLen()116 static inline unsigned int sampleLen()
117 {
118 return 2;
119 }
120
121 // Calculate the number of 'float' elements in given number of samples
samples2floats(unsigned int nSamples)122 static inline unsigned int samples2floats(unsigned int nSamples)
123 {
124 return nSamples * sampleLen();
125 }
126
127 // Calculate the length in bytes of a given number of samples
samples2bytes(unsigned int nSamples)128 static inline unsigned int samples2bytes(unsigned int nSamples)
129 {
130 return samples2floats(nSamples) * sizeof(float);
131 }
132
133 // Advance float buffer by samples
advanceSamples(float * buf,unsigned int nSamples)134 static inline float* advanceSamples(float* buf, unsigned int nSamples)
135 {
136 return buf + samples2floats(nSamples);
137 }
138
resetSamples(float * buf,unsigned int nSamples)139 static inline void resetSamples(float* buf, unsigned int nSamples)
140 {
141 ::memset(buf,0,samples2bytes(nSamples));
142 }
143
copySamples(float * dest,unsigned int destOffs,float * src,unsigned int srcOffs,unsigned int nSamples)144 static inline void copySamples(float* dest, unsigned int destOffs,
145 float* src, unsigned int srcOffs, unsigned int nSamples)
146 {
147 ::memcpy(advanceSamples(dest,destOffs),advanceSamples(src,srcOffs),
148 samples2bytes(nSamples));
149 }
150
151 // Move samples from buf to offset (in the same buffer)
152 // Copy data in the backward direction to avoid overlap
moveSamples(float * buf,unsigned int offs,unsigned int nSamples)153 static inline void moveSamples(float* buf, unsigned int offs, unsigned int nSamples)
154 {
155 if (!nSamples)
156 return;
157 unsigned int cp = samples2floats(nSamples);
158 float* src = buf + cp - 1;
159 float* dest = advanceSamples(buf,offs) + cp - 1;
160 for (float* last = src - cp; src != last; --dest, --src)
161 *dest = *src;
162 resetSamples(buf,offs);
163 }
164
165
dump(String & buf)166 String& RadioReadBufs::dump(String& buf)
167 {
168 return buf.printf("\r\n-----\r\ncrt:\t%u(%u)\t%u\t(%p)\r\naux:\t%u(%u)\t%u\t(%p)"
169 "\r\nextra:\t\t%u\t(%p)\r\n-----",
170 valid(crt),crt.valid,crt.offs,crt.samples,
171 valid(aux),aux.valid,aux.offs,aux.samples,
172 extra.offs,extra.samples);
173 }
174
175
RadioInterface(const char * name)176 RadioInterface::RadioInterface(const char* name)
177 : m_lastErr(0), m_totalErr(0), m_radioCaps(0), m_name(name),
178 m_mutex(false,"RadioInterface")
179 {
180 debugName(m_name);
181 ::memset(m_pendingCode,0,sizeof(m_pendingCode));
182 }
183
184 // Poll for a pending operation
pollPending(unsigned int oper,unsigned int waitMs)185 unsigned int RadioInterface::pollPending(unsigned int oper, unsigned int waitMs)
186 {
187 if (oper >= PendingCount)
188 return 0;
189 if (waitMs && (Pending == m_pendingCode[oper])) {
190 unsigned int intervals = (waitMs + Thread::idleMsec() - 1) / Thread::idleMsec();
191 while ((Pending == m_pendingCode[oper]) && intervals && !Thread::check(false)) {
192 Thread::idle();
193 intervals--;
194 }
195 }
196 Lock lck(m_mutex);
197 if (Pending == m_pendingCode[oper])
198 return Pending;
199 unsigned int code = m_pendingCode[oper];
200 m_pendingCode[oper] = 0;
201 return code;
202 }
203
204 // NOTE: This method assumes a single port is used
205 // E.g.: a sample is an I/Q pair
206 // If multiple ports are handled the sampleLen() function should handle the number of ports
read(uint64_t & when,RadioReadBufs & bufs,unsigned int & skippedBufs)207 unsigned int RadioInterface::read(uint64_t& when, RadioReadBufs& bufs,
208 unsigned int& skippedBufs)
209 {
210 String tmp;
211 DebugRadioRead(">>> read: ts=" FMT64U " buf_samples=%u [%p]%s",
212 when,bufs.bufSamples(),this,bufs.dump(tmp).c_str());
213 // Switch buffers
214 if (bufs.full(bufs.crt) && !bufs.aux.offs) {
215 bufs.crt.reset();
216 DebugRadioRead("read reset crt [%p]",this);
217 }
218 else if ((!bufs.crt.offs && bufs.aux.offs) || bufs.full(bufs.crt)) {
219 bool emptyCrt = (bufs.crt.offs == 0);
220 RadioBufDesc buf = bufs.crt;
221 bufs.crt = bufs.aux;
222 if (emptyCrt || !bufs.extra.offs) {
223 bufs.aux.samples = buf.samples;
224 bufs.aux.reset();
225 }
226 else {
227 bufs.aux = bufs.extra;
228 bufs.extra.samples = buf.samples;
229 bufs.extra.reset();
230 }
231 // Adjust timestamp with data already in buffer
232 when += bufs.crt.offs;
233 if (bufs.full(bufs.crt)) {
234 if (!bufs.valid(bufs.crt)) {
235 skippedBufs = 1;
236 bufs.crt.reset();
237 }
238 DebugRadioRead("<<< read ts=" FMT64U " (crt full) [%p]%s",
239 when,this,bufs.dump(tmp).c_str());
240 return 0;
241 }
242 DebugRadioRead("read moved aux to crt [%p]%s",
243 this,bufs.dump(tmp).c_str());
244 }
245 skippedBufs = 0;
246 unsigned int avail = bufs.bufSamples() - bufs.crt.offs;
247 unsigned int rdSamples = avail;
248 uint64_t ts = when;
249 float* rdBuf = advanceSamples(bufs.crt.samples,bufs.crt.offs);
250 unsigned int code = recv(ts,rdBuf,rdSamples);
251 DebugRadioRead("read: code=%u read=%u/%u [%p]",code,rdSamples,avail,this);
252 if (code || !rdSamples)
253 return code;
254 if (when == ts) {
255 when += rdSamples;
256 bufs.crt.offs += rdSamples;
257 bufs.crt.valid += rdSamples;
258 DebugRadioRead("<<< read ts=" FMT64U " OK [%p]%s",
259 when,this,bufs.dump(tmp).c_str());
260 return 0;
261 }
262 // This should never happen !!!
263 if (ts < when) {
264 Debug(this,DebugFail,
265 "Read timestamp in the past by " FMT64U " at " FMT64U " [%p]",
266 (when - ts),when,this);
267 return TooEarly;
268 }
269 // Timestamp is in the future
270 uint64_t diff = ts - when;
271 if (when)
272 Debug(this,DebugNote,
273 "Read timestamp in the future by " FMT64U " at " FMT64U " [%p]",
274 diff,when,this);
275 if (diff <= avail) {
276 // The timestamp difference is inside available space
277 // Read samples + NULLs won't exceed current + auxiliary buffer
278 bufs.extra.reset();
279 // We may copy some data
280 unsigned int cpSamples = avail - diff;
281 if (cpSamples > rdSamples)
282 cpSamples = rdSamples;
283 // Copy data to auxiliary buffer if valid
284 // Do nothing if invalid: we will ignore it at next read
285 bufs.aux.reset(rdSamples - cpSamples);
286 if (bufs.aux.offs && bufs.valid(bufs.aux))
287 copySamples(bufs.aux.samples,0,rdBuf,cpSamples,bufs.aux.offs);
288 // Adjust available (used) space: copy samples + NULLs
289 avail = diff + cpSamples;
290 bufs.crt.reset(bufs.crt.offs + avail,bufs.crt.valid + cpSamples);
291 if (bufs.valid(bufs.crt)) {
292 if (cpSamples)
293 moveSamples(rdBuf,diff,cpSamples);
294 else
295 resetSamples(rdBuf,avail);
296 }
297 else if (bufs.full(bufs.crt)) {
298 // Not enough valid samples in full buffer: skip it
299 skippedBufs++;
300 bufs.crt.reset();
301 }
302 // Adjust timestamp
303 when += avail;
304 }
305 else {
306 // Timestamp is outside buffer
307 uint64_t delta = diff - avail;
308 skippedBufs = delta / bufs.bufSamples();
309 if (skippedBufs)
310 when += skippedBufs * bufs.bufSamples();
311 // Advance current timestamp
312 // Reset data in current buffer or skip it
313 when += avail;
314 bufs.crt.offs = bufs.bufSamples();
315 if (bufs.valid(bufs.crt))
316 resetSamples(rdBuf,avail);
317 else {
318 // Not enough valid samples in full buffer: skip it
319 skippedBufs++;
320 bufs.crt.reset();
321 }
322 // Setup the auxiliary buffers
323 // Set data if valid
324 // Do nothing with data if invalid: we will ignore them on subsequent reads
325 unsigned int nullSamples = delta % bufs.bufSamples();
326 unsigned int len = nullSamples + rdSamples;
327 if (len <= bufs.bufSamples()) {
328 bufs.aux.reset(len,rdSamples);
329 bufs.extra.reset();
330 }
331 else {
332 bufs.aux.reset(bufs.bufSamples(),bufs.bufSamples() - nullSamples);
333 bufs.extra.reset(rdSamples - bufs.aux.valid);
334 }
335 if (bufs.valid(bufs.aux)) {
336 resetSamples(bufs.aux.samples,nullSamples);
337 copySamples(bufs.aux.samples,nullSamples,rdBuf,0,bufs.aux.valid);
338 }
339 if (bufs.extra.offs && bufs.valid(bufs.extra))
340 copySamples(bufs.extra.samples,0,rdBuf,bufs.aux.valid,bufs.extra.valid);
341 }
342 DebugRadioRead("<<< read (ts in future): ts=" FMT64U " skipped_bufs=%u [%p]%s",
343 when,skippedBufs,this,bufs.dump(tmp).c_str());
344 return 0;
345 }
346
toString() const347 const String& RadioInterface::toString() const
348 {
349 return m_name;
350 }
351
completeDevInfo(NamedList & p,bool full,bool retData)352 void RadioInterface::completeDevInfo(NamedList& p, bool full, bool retData)
353 {
354 if (retData)
355 p.setParam(new NamedPointer("interface",this,m_name));
356 else
357 p.addParam("interface",m_name);
358 }
359
setError(NamedList & p,unsigned int code,const char * str)360 void RadioInterface::setError(NamedList& p, unsigned int code, const char* str)
361 {
362 if (!code)
363 return;
364 p.setParam(YSTRING("code"),String(code));
365 p.setParam(YSTRING("reason"),errorName(code));
366 if (TelEngine::null(str))
367 p.clearParam(YSTRING("error"));
368 else
369 p.setParam(YSTRING("error"),str);
370 unsigned int tmp = code & NoAutoRestartMask;
371 p.setParam(YSTRING("canretry"),String::boolText(!tmp));
372 if (!tmp)
373 p.setParam(YSTRING("fatal"),String::boolText(code & FatalErrorMask));
374 }
375
errorNameDict()376 const TokenDict* RadioInterface::errorNameDict()
377 {
378 return s_errorName;
379 }
380
381
382 //
383 // RadioDataFile
384 //
RadioDataFile(const char * name,bool dropOnError)385 RadioDataFile::RadioDataFile(const char* name, bool dropOnError)
386 : String(name),
387 m_littleEndian(true),
388 m_dropOnError(dropOnError),
389 m_chunkSize(0),
390 m_writeBuf(256)
391 {
392 m_littleEndian = m_header.m_littleEndian;
393 }
394
~RadioDataFile()395 RadioDataFile::~RadioDataFile()
396 {
397 terminate();
398 }
399
elementSize(const RadioDataDesc & data)400 static inline unsigned int elementSize(const RadioDataDesc& data)
401 {
402 switch (data.m_elementType) {
403 case RadioDataDesc::Float:
404 return sizeof(float);
405 case RadioDataDesc::Int16:
406 return sizeof(int16_t);
407 }
408 return 0;
409 }
410
411 // Open a file for read/write. Terminate current data dump if any.
412 // For file write: read the file header
open(const char * fileName,const RadioDataDesc * data,DebugEnabler * dbg,int * error)413 bool RadioDataFile::open(const char* fileName, const RadioDataDesc* data,
414 DebugEnabler* dbg, int* error)
415 {
416 terminate(dbg);
417 if (TelEngine::null(fileName))
418 return false;
419 const char* fileOper = 0;
420 String hdrError;
421 while (true) {
422 m_writeBuf.resize(sizeof(m_header));
423 uint8_t* d = m_writeBuf.data(0);
424 // Write
425 if (data) {
426 fileOper = "write";
427 if ((elementSize(*data) * data->m_sampleLen * data->m_ports) == 0) {
428 hdrError = "Invalid header data";
429 break;
430 }
431 m_header = *data;
432 if (!m_file.openPath(fileName,true,false,true,false,true,true)) {
433 fileOper = "open";
434 break;
435 }
436 ::memcpy(d,m_header.m_signature,sizeof(m_header.m_signature));
437 d += sizeof(m_header.m_signature);
438 *d++ = m_header.m_elementType;
439 *d++ = m_header.m_sampleLen;
440 *d++ = m_header.m_ports;
441 *d++ = m_header.m_tsType;
442 *d++ = m_header.m_littleEndian ? 0 : 1;
443 int wr = m_file.writeData(m_writeBuf.data(),m_writeBuf.length());
444 if (wr == (int)m_writeBuf.length())
445 fileOper = 0;
446 else if (wr >= 0)
447 hdrError = "Incomplete header write";
448 break;
449 }
450 // Read
451 fileOper = "open";
452 if (!m_file.openPath(fileName))
453 break;
454 fileOper = "read";
455 int rd = m_file.readData(d,m_writeBuf.length());
456 if (rd == (int)m_writeBuf.length()) {
457 ::memcpy(m_header.m_signature,d,sizeof(m_header.m_signature));
458 d += sizeof(m_header.m_signature);
459 m_header.m_elementType = *d++;
460 m_header.m_sampleLen = *d++;
461 m_header.m_ports = *d++;
462 m_header.m_tsType = *d++;
463 if (*d == 0 || *d == 1) {
464 m_header.m_littleEndian = (*d == 0);
465 fileOper = 0;
466 break;
467 }
468 hdrError = "Invalid endiannes value";
469 }
470 else if (rd >= 0)
471 hdrError = "Invalid file size";
472 break;
473 }
474 if (!fileOper) {
475 m_chunkSize = elementSize(m_header) * m_header.m_sampleLen * m_header.m_ports;
476 if (dbg)
477 Debug(dbg,DebugAll,"RadioDataFile[%s] opened file '%s' [%p]",
478 c_str(),fileName,this);
479 return true;
480 }
481 if (error)
482 *error = hdrError ? 0 : m_file.error();
483 if (!hdrError) {
484 hdrError = m_file.error();
485 String tmp;
486 Thread::errorString(tmp,m_file.error());
487 hdrError.append(tmp," - ");
488 }
489 Debug(dbg,DebugNote,"RadioDataFile[%s] file '%s' %s %s failed: %s [%p]",
490 c_str(),fileName,(data ? "OUT" : "IN"),fileOper,hdrError.safe(),this);
491 terminate();
492 return false;
493 }
494
write(uint64_t ts,const void * buf,uint32_t len,DebugEnabler * dbg,int * error)495 bool RadioDataFile::write(uint64_t ts, const void* buf, uint32_t len,
496 DebugEnabler* dbg, int* error)
497 {
498 int e = 0;
499 if (error)
500 *error = 0;
501 else
502 error = &e;
503 if (!(buf && len))
504 return false;
505 if (m_chunkSize && (len % m_chunkSize) != 0)
506 return ioError(true,dbg,0,"Invalid buffer length");
507 m_writeBuf.resize(len + 12);
508 uint8_t* d = m_writeBuf.data(0);
509 if (m_littleEndian == m_header.m_littleEndian) {
510 *(uint32_t*)d = len;
511 *(uint64_t*)(d + 4) = ts;
512 }
513 else if (m_littleEndian) {
514 *(uint32_t*)d = htobe32(len);
515 *(uint64_t*)(d + 4) = htobe64(ts);
516 }
517 else {
518 *(uint32_t*)d = htole32(len);
519 *(uint64_t*)(d + 4) = htole64(ts);
520 }
521 ::memcpy(d + 12,buf,len);
522 int wr = m_file.writeData(m_writeBuf.data(),m_writeBuf.length());
523 if (wr != (int)m_writeBuf.length())
524 return ioError(true,dbg,error,wr >= 0 ? "Incomplete write" : 0);
525 #ifdef XDEBUG
526 String sHdr, s;
527 sHdr.hexify(d,12,' ');
528 s.hexify(d + 12,wr - 12,' ');
529 Debug(dbg,DebugAll,"RadioDataFile[%s] wrote %d hdr=%s data=%s [%p]",
530 c_str(),wr,sHdr.c_str(),s.c_str(),this);
531 #endif
532 return true;
533 }
534
535 // Read a record from file
read(uint64_t & ts,DataBlock & buffer,DebugEnabler * dbg,int * error)536 bool RadioDataFile::read(uint64_t& ts, DataBlock& buffer, DebugEnabler* dbg, int* error)
537 {
538 int e = 0;
539 if (error)
540 *error = 0;
541 else
542 error = &e;
543 uint8_t hdr[12];
544 int rd = m_file.readData(hdr,sizeof(hdr));
545 // EOF ?
546 if (rd == 0) {
547 buffer.resize(0);
548 return true;
549 }
550 if (rd != sizeof(hdr))
551 return ioError(false,dbg,error,rd > 0 ? "Incomplete read (invalid size?)" : 0);
552 uint32_t len = 0;
553 uint32_t* u = (uint32_t*)hdr;
554 if (m_littleEndian == m_header.m_littleEndian)
555 len = *u;
556 else if (m_littleEndian)
557 len = be32toh(*u);
558 else
559 len = le32toh(*u);
560 uint64_t* p = (uint64_t*)&hdr[4];
561 if (m_littleEndian == m_header.m_littleEndian)
562 ts = *p;
563 else if (m_littleEndian)
564 len = be64toh(*p);
565 else
566 len = le64toh(*p);
567 buffer.resize(len);
568 if (!len)
569 return ioError(false,dbg,0,"Empty record");
570 rd = m_file.readData((void*)buffer.data(),len);
571 if (rd != (int)len)
572 return ioError(false,dbg,error,rd > 0 ? "Incomplete read (invalid size?)" : 0);
573 #ifdef XDEBUG
574 String sHdr, s;
575 sHdr.hexify(hdr,sizeof(hdr),' ');
576 s.hexify((void*)buffer.data(),rd,' ');
577 Debug(dbg,DebugAll,"RadioDataFile[%s] read %d hdr=%s data=%s [%p]",
578 c_str(),rd + (int)sizeof(hdr),sHdr.c_str(),s.c_str(),this);
579 #endif
580 return true;
581 }
582
terminate(DebugEnabler * dbg)583 void RadioDataFile::terminate(DebugEnabler* dbg)
584 {
585 if (dbg && valid())
586 Debug(dbg,DebugAll,"RadioDataFile[%s] closing file [%p]",c_str(),this);
587 m_file.terminate();
588 }
589
590 // Convert endiannes
fixEndian(DataBlock & buf,unsigned int bytes)591 bool RadioDataFile::fixEndian(DataBlock& buf, unsigned int bytes)
592 {
593 if (!bytes)
594 return false;
595 unsigned int n = buf.length() / bytes;
596 if (bytes == 2) {
597 for (uint16_t* p = (uint16_t*)buf.data(); n; n--, p++)
598 #ifdef LITTLE_ENDIAN
599 *p = be16toh(*p);
600 #else
601 *p = le16toh(*p);
602 #endif
603 return true;
604 }
605 if (bytes == 4) {
606 for (uint32_t* p = (uint32_t*)buf.data(); n; n--, p++)
607 #ifdef LITTLE_ENDIAN
608 *p = be32toh(*p);
609 #else
610 *p = le32toh(*p);
611 #endif
612 return true;
613 }
614 if (bytes == 8) {
615 for (uint64_t* p = (uint64_t*)buf.data(); n; n--, p++)
616 #ifdef LITTLE_ENDIAN
617 *p = be64toh(*p);
618 #else
619 *p = le64toh(*p);
620 #endif
621 return true;
622 }
623 return false;
624 }
625
ioError(bool send,DebugEnabler * dbg,int * error,const char * extra)626 bool RadioDataFile::ioError(bool send, DebugEnabler* dbg, int* error, const char* extra)
627 {
628 String s = extra;
629 if (error) {
630 String tmp;
631 Thread::errorString(tmp,m_file.error());
632 tmp.printf("(%d - %s)",m_file.error(),tmp.c_str());
633 s.append(tmp," ");
634 }
635 Debug(dbg,DebugNote,"RadioDataFile[%s] file %s failed: %s [%p]",
636 c_str(),(send ? "write" : "read"),s.safe(),this);
637 if (error) {
638 *error = m_file.error();
639 if (m_dropOnError)
640 terminate();
641 }
642 return false;
643 }
644
645 /* vi: set ts=8 sw=4 sts=4 noet enc=utf-8: */
646