1 /*
2  *
3  *  Copyright (C) 1994-2018, OFFIS e.V.
4  *  All rights reserved.  See COPYRIGHT file for details.
5  *
6  *  This software and supporting documentation were developed by
7  *
8  *    OFFIS e.V.
9  *    R&D Division Health
10  *    Escherweg 2
11  *    D-26121 Oldenburg, Germany
12  *
13  *
14  *  Module:  dcmnet
15  *
16  *  Author:  Andrew Hewett
17  *
18  *  Purpose: Verification Service Class User (C-ECHO operation)
19  *
20  */
21 
22 #include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
23 
24 #define INCLUDE_CSTDLIB
25 #define INCLUDE_CSTDIO
26 #define INCLUDE_CSTRING
27 #define INCLUDE_CSTDARG
28 #include "dcmtk/ofstd/ofstdinc.h"
29 
30 #include "dcmtk/dcmnet/dimse.h"
31 #include "dcmtk/dcmnet/diutil.h"
32 #include "dcmtk/dcmnet/dcmtrans.h"    /* for dcmSocketSend/ReceiveTimeout */
33 #include "dcmtk/dcmdata/dcfilefo.h"
34 #include "dcmtk/dcmdata/dcdict.h"
35 #include "dcmtk/dcmdata/dcuid.h"
36 #include "dcmtk/dcmdata/cmdlnarg.h"
37 #include "dcmtk/ofstd/ofconapp.h"
38 #include "dcmtk/dcmdata/dcuid.h"      /* for dcmtk version name */
39 #include "dcmtk/dcmtls/tlsopt.h"      /* for DcmTLSOptions */
40 
41 #ifdef WITH_ZLIB
42 #include <zlib.h>                     /* for zlibVersion() */
43 #endif
44 
45 #ifdef PRIVATE_ECHOSCU_DECLARATIONS
46 PRIVATE_ECHOSCU_DECLARATIONS
47 #else
48 #define OFFIS_CONSOLE_APPLICATION "echoscu"
49 #endif
50 
51 static OFLogger echoscuLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION);
52 
53 static char rcsid[] = "$dcmtk: " OFFIS_CONSOLE_APPLICATION " v"
54   OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $";
55 
56 /* default application titles */
57 #define APPLICATIONTITLE     "ECHOSCU"
58 #define PEERAPPLICATIONTITLE "ANY-SCP"
59 
60 
61 /* exit codes for this command line tool */
62 /* (common codes are defined in "ofexit.h" included from "ofconapp.h") */
63 // network errors
64 #define EXITCODE_ASSOCIATION_ABORTED    70
65 
66 static T_DIMSE_BlockingMode opt_blockMode = DIMSE_BLOCKING;
67 static int opt_dimse_timeout = 0;
68 
69 static OFCondition cecho(T_ASC_Association * assoc, unsigned long num_repeat);
70 
71 /* DICOM standard transfer syntaxes */
72 static const char* transferSyntaxes[] = {
73       UID_LittleEndianImplicitTransferSyntax, /* default xfer syntax first */
74       UID_LittleEndianExplicitTransferSyntax,
75       UID_BigEndianExplicitTransferSyntax,
76       UID_JPEGProcess1TransferSyntax,
77       UID_JPEGProcess2_4TransferSyntax,
78       UID_JPEGProcess3_5TransferSyntax,
79       UID_JPEGProcess6_8TransferSyntax,
80       UID_JPEGProcess7_9TransferSyntax,
81       UID_JPEGProcess10_12TransferSyntax,
82       UID_JPEGProcess11_13TransferSyntax,
83       UID_JPEGProcess14TransferSyntax,
84       UID_JPEGProcess15TransferSyntax,
85       UID_JPEGProcess16_18TransferSyntax,
86       UID_JPEGProcess17_19TransferSyntax,
87       UID_JPEGProcess20_22TransferSyntax,
88       UID_JPEGProcess21_23TransferSyntax,
89       UID_JPEGProcess24_26TransferSyntax,
90       UID_JPEGProcess25_27TransferSyntax,
91       UID_JPEGProcess28TransferSyntax,
92       UID_JPEGProcess29TransferSyntax,
93       UID_JPEGProcess14SV1TransferSyntax,
94       UID_RLELosslessTransferSyntax,
95       UID_DeflatedExplicitVRLittleEndianTransferSyntax,
96       UID_JPEGLSLosslessTransferSyntax,
97       UID_JPEGLSLossyTransferSyntax,
98       UID_JPEG2000LosslessOnlyTransferSyntax,
99       UID_JPEG2000TransferSyntax,
100       UID_JPEG2000Part2MulticomponentImageCompressionLosslessOnlyTransferSyntax,
101       UID_JPEG2000Part2MulticomponentImageCompressionTransferSyntax,
102       UID_MPEG2MainProfileAtMainLevelTransferSyntax,
103       UID_MPEG2MainProfileAtHighLevelTransferSyntax,
104       UID_MPEG4HighProfileLevel4_1TransferSyntax,
105       UID_MPEG4BDcompatibleHighProfileLevel4_1TransferSyntax,
106       UID_MPEG4HighProfileLevel4_2_For2DVideoTransferSyntax,
107       UID_MPEG4HighProfileLevel4_2_For3DVideoTransferSyntax,
108       UID_MPEG4StereoHighProfileLevel4_2TransferSyntax,
109       UID_HEVCMainProfileLevel5_1TransferSyntax,
110       UID_HEVCMain10ProfileLevel5_1TransferSyntax
111 };
112 
113 // ********************************************
114 
115 /* helper macro for converting stream output to a string */
116 #define CONVERT_TO_STRING(output, string) \
117     optStream.str(""); \
118     optStream.clear(); \
119     optStream << output << OFStringStream_ends; \
120     OFSTRINGSTREAM_GETOFSTRING(optStream, string)
121 
122 #define SHORTCOL 4
123 #define LONGCOL 19
124 
125 int
main(int argc,char * argv[])126 main(int argc, char *argv[])
127 {
128   OFOStringStream optStream;
129   int result = EXITCODE_NO_ERROR;
130 
131   const char *     opt_peer                = NULL;
132   OFCmdUnsignedInt opt_port                = 104;
133   const char *     opt_peerTitle           = PEERAPPLICATIONTITLE;
134   const char *     opt_ourTitle            = APPLICATIONTITLE;
135   OFCmdUnsignedInt opt_maxReceivePDULength = ASC_DEFAULTMAXPDU;
136   OFCmdUnsignedInt opt_repeatCount         = 1;
137   OFBool           opt_abortAssociation    = OFFalse;
138   OFCmdUnsignedInt opt_numXferSyntaxes     = 1;
139   OFCmdUnsignedInt opt_numPresentationCtx  = 1;
140   OFCmdUnsignedInt maxXferSyntaxes         = OFstatic_cast(OFCmdUnsignedInt, (DIM_OF(transferSyntaxes)));
141   int              opt_acse_timeout        = 30;
142   OFCmdSignedInt   opt_socket_timeout      = 60;
143   DcmTLSOptions    tlsOptions(NET_REQUESTOR);
144 
145   T_ASC_Network *net;
146   T_ASC_Parameters *params;
147   DIC_NODENAME peerHost;
148   T_ASC_Association *assoc;
149   OFString temp_str;
150 
151   OFStandard::initializeNetwork();
152 #ifdef WITH_OPENSSL
153   DcmTLSTransportLayer::initializeOpenSSL();
154 #endif
155 
156   char tempstr[20];
157   OFConsoleApplication app(OFFIS_CONSOLE_APPLICATION , "DICOM verification (C-ECHO) SCU", rcsid);
158   OFCommandLine cmd;
159 
160   cmd.setParamColumn(LONGCOL + SHORTCOL + 4);
161   cmd.addParam("peer", "hostname of DICOM peer");
162   cmd.addParam("port", "tcp/ip port number of peer");
163 
164   cmd.setOptionColumns(LONGCOL, SHORTCOL);
165   cmd.addGroup("general options:", LONGCOL, SHORTCOL + 2);
166    cmd.addOption("--help",              "-h",      "print this help text and exit", OFCommandLine::AF_Exclusive);
167    cmd.addOption("--version",                      "print version information and exit", OFCommandLine::AF_Exclusive);
168    OFLog::addOptions(cmd);
169 
170   cmd.addGroup("network options:");
171     cmd.addSubGroup("application entity titles:");
172       cmd.addOption("--aetitle",        "-aet", 1, "[a]etitle: string", "set my calling AE title (default: " APPLICATIONTITLE ")");
173       cmd.addOption("--call",           "-aec", 1, "[a]etitle: string", "set called AE title of peer (default: " PEERAPPLICATIONTITLE ")");
174     cmd.addSubGroup("association negotiation debugging:");
175       OFString opt5 = "[n]umber: integer (1..";
176       sprintf(tempstr, "%ld", OFstatic_cast(long, maxXferSyntaxes));
177       opt5 += tempstr;
178       opt5 += ")";
179       cmd.addOption("--propose-ts",     "-pts", 1, opt5.c_str(), "propose n transfer syntaxes");
180       cmd.addOption("--propose-pc",     "-ppc", 1, "[n]umber: integer (1..128)", "propose n presentation contexts");
181 
182     cmd.addSubGroup("other network options:");
183       cmd.addOption("--timeout",        "-to",  1, "[s]econds: integer (default: unlimited)", "timeout for connection requests");
184       CONVERT_TO_STRING("[s]econds: integer (default: " << opt_socket_timeout << ")", optString1);
185       cmd.addOption("--socket-timeout", "-ts",  1, optString1.c_str(), "timeout for network socket (0 for none)");
186       CONVERT_TO_STRING("[s]econds: integer (default: " << opt_acse_timeout << ")", optString2);
187       cmd.addOption("--acse-timeout",   "-ta",  1, optString2.c_str(), "timeout for ACSE messages");
188       cmd.addOption("--dimse-timeout",  "-td",  1, "[s]econds: integer (default: unlimited)", "timeout for DIMSE messages");
189 
190       CONVERT_TO_STRING("[n]umber of bytes: integer (" << ASC_MINIMUMPDUSIZE << ".." << ASC_MAXIMUMPDUSIZE << ")", optString3);
191       CONVERT_TO_STRING("set max receive pdu to n bytes (default: " << opt_maxReceivePDULength << ")", optString4);
192       cmd.addOption("--max-pdu",        "-pdu", 1, optString3.c_str(), optString4.c_str());
193       cmd.addOption("--repeat",                 1, "[n]umber: integer", "repeat n times");
194       cmd.addOption("--abort",                     "abort association instead of releasing it");
195 
196     // add TLS specific command line options if (and only if) we are compiling with OpenSSL
197     tlsOptions.addTLSCommandlineOptions(cmd);
198 
199     /* evaluate command line */
200     prepareCmdLineArgs(argc, argv, OFFIS_CONSOLE_APPLICATION);
201     if (app.parseCommandLine(cmd, argc, argv))
202     {
203       /* check exclusive options first */
204       if (cmd.hasExclusiveOption())
205       {
206         if (cmd.findOption("--version"))
207         {
208           app.printHeader(OFTrue /*print host identifier*/);
209           COUT << OFendl << "External libraries used:";
210 #if !defined(WITH_ZLIB) && !defined(WITH_OPENSSL)
211           COUT << " none" << OFendl;
212 #else
213           COUT << OFendl;
214 #endif
215 #ifdef WITH_ZLIB
216           COUT << "- ZLIB, Version " << zlibVersion() << OFendl;
217 #endif
218           // print OpenSSL version if (and only if) we are compiling with OpenSSL
219           tlsOptions.printLibraryVersion();
220           return EXITCODE_NO_ERROR;
221         }
222 
223         // check if the command line contains the --list-ciphers option
224         if (tlsOptions.listOfCiphersRequested(cmd))
225         {
226             tlsOptions.printSupportedCiphersuites(app, COUT);
227             return EXITCODE_NO_ERROR;
228         }
229       }
230 
231       /* command line parameters */
232 
233       cmd.getParam(1, opt_peer);
234       app.checkParam(cmd.getParamAndCheckMinMax(2, opt_port, 1, 65535));
235 
236       OFLog::configureFromCommandLine(cmd, app);
237 
238       if (cmd.findOption("--aetitle")) app.checkValue(cmd.getValue(opt_ourTitle));
239       if (cmd.findOption("--call")) app.checkValue(cmd.getValue(opt_peerTitle));
240 
241       if (cmd.findOption("--timeout"))
242       {
243         OFCmdSignedInt opt_timeout = 0;
244         app.checkValue(cmd.getValueAndCheckMin(opt_timeout, 1));
245         dcmConnectionTimeout.set(OFstatic_cast(Sint32, opt_timeout));
246       }
247 
248       if (cmd.findOption("--socket-timeout"))
249         app.checkValue(cmd.getValueAndCheckMin(opt_socket_timeout, -1));
250       // always set the timeout values since the global default might be different
251       dcmSocketSendTimeout.set(OFstatic_cast(Sint32, opt_socket_timeout));
252       dcmSocketReceiveTimeout.set(OFstatic_cast(Sint32, opt_socket_timeout));
253 
254       if (cmd.findOption("--acse-timeout"))
255       {
256         OFCmdSignedInt opt_timeout = 0;
257         app.checkValue(cmd.getValueAndCheckMin(opt_timeout, 1));
258         opt_acse_timeout = OFstatic_cast(int, opt_timeout);
259       }
260 
261       if (cmd.findOption("--dimse-timeout"))
262       {
263         OFCmdSignedInt opt_timeout = 0;
264         app.checkValue(cmd.getValueAndCheckMin(opt_timeout, 1));
265         opt_dimse_timeout = OFstatic_cast(int, opt_timeout);
266         opt_blockMode = DIMSE_NONBLOCKING;
267       }
268 
269       if (cmd.findOption("--max-pdu")) app.checkValue(cmd.getValueAndCheckMinMax(opt_maxReceivePDULength, ASC_MINIMUMPDUSIZE, ASC_MAXIMUMPDUSIZE));
270       if (cmd.findOption("--repeat")) app.checkValue(cmd.getValueAndCheckMin(opt_repeatCount, 1));
271       if (cmd.findOption("--abort")) opt_abortAssociation=OFTrue;
272       if (cmd.findOption("--propose-ts")) app.checkValue(cmd.getValueAndCheckMinMax(opt_numXferSyntaxes, 1, maxXferSyntaxes));
273       if (cmd.findOption("--propose-pc")) app.checkValue(cmd.getValueAndCheckMinMax(opt_numPresentationCtx, 1, 128));
274 
275       // evaluate (most of) the TLS command line options (if we are compiling with OpenSSL)
276       tlsOptions.parseArguments(app, cmd);
277 
278     }
279 
280     /* print resource identifier */
281     OFLOG_DEBUG(echoscuLogger, rcsid << OFendl);
282 
283     /* make sure data dictionary is loaded */
284     if (!dcmDataDict.isDictionaryLoaded())
285     {
286         OFLOG_WARN(echoscuLogger, "no data dictionary loaded, check environment variable: "
287             << DCM_DICT_ENVIRONMENT_VARIABLE);
288     }
289 
290     /* initialize network, i.e. create an instance of T_ASC_Network*. */
291     OFCondition cond = ASC_initializeNetwork(NET_REQUESTOR, 0, opt_acse_timeout, &net);
292     if (cond.bad()) {
293         OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
294         exit(1);
295     }
296 
297     /* initialize association parameters, i.e. create an instance of T_ASC_Parameters*. */
298     cond = ASC_createAssociationParameters(&params, opt_maxReceivePDULength);
299     if (cond.bad()) {
300         OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
301         exit(1);
302     }
303 
304     /* create a secure transport layer if requested and OpenSSL is available */
305     cond = tlsOptions.createTransportLayer(net, params, app, cmd);
306     if (cond.bad()) {
307         OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
308         exit(1);
309     }
310 
311 #ifdef PRIVATE_ECHOSCU_CODE
312     PRIVATE_ECHOSCU_CODE
313 #endif
314 
315     /* sets this application's title and the called application's title in the params */
316     /* structure. The default values to be set here are "STORESCU" and "ANY-SCP". */
317     ASC_setAPTitles(params, opt_ourTitle, opt_peerTitle, NULL);
318 
319     /* Figure out the presentation addresses and copy the */
320     /* corresponding values into the association parameters.*/
321     sprintf(peerHost, "%s:%d", opt_peer, OFstatic_cast(int, opt_port));
322     ASC_setPresentationAddresses(params, OFStandard::getHostName().c_str(), peerHost);
323 
324     /* Set the presentation contexts which will be negotiated */
325     /* when the network connection will be established */
326     int presentationContextID = 1; /* odd byte value 1, 3, 5, .. 255 */
327     for (unsigned long ii=0; ii<opt_numPresentationCtx; ii++)
328     {
329         cond = ASC_addPresentationContext(params, presentationContextID, UID_VerificationSOPClass,
330                  transferSyntaxes, OFstatic_cast(int, opt_numXferSyntaxes));
331         presentationContextID += 2;
332         if (cond.bad())
333         {
334             OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
335             exit(1);
336         }
337     }
338 
339     /* dump presentation contexts if required */
340     OFLOG_DEBUG(echoscuLogger, "Request Parameters:" << OFendl << ASC_dumpParameters(temp_str, params, ASC_ASSOC_RQ));
341 
342     /* create association, i.e. try to establish a network connection to another */
343     /* DICOM application. This call creates an instance of T_ASC_Association*. */
344     OFLOG_INFO(echoscuLogger, "Requesting Association");
345     cond = ASC_requestAssociation(net, params, &assoc);
346     if (cond.bad()) {
347         if (cond == DUL_ASSOCIATIONREJECTED)
348         {
349             T_ASC_RejectParameters rej;
350 
351             ASC_getRejectParameters(params, &rej);
352             OFLOG_FATAL(echoscuLogger, "Association Rejected:" << OFendl << ASC_printRejectParameters(temp_str, &rej));
353             exit(1);
354         } else {
355             OFLOG_FATAL(echoscuLogger, "Association Request Failed: " << DimseCondition::dump(temp_str, cond));
356             exit(1);
357         }
358     }
359 
360     /* dump the presentation contexts which have been accepted/refused */
361     OFLOG_DEBUG(echoscuLogger, "Association Parameters Negotiated:" << OFendl << ASC_dumpParameters(temp_str, params, ASC_ASSOC_AC));
362 
363     /* count the presentation contexts which have been accepted by the SCP */
364     /* If there are none, finish the execution */
365     if (ASC_countAcceptedPresentationContexts(params) == 0) {
366         OFLOG_FATAL(echoscuLogger, "No Acceptable Presentation Contexts");
367         exit(1);
368     }
369 
370     /* dump general information concerning the establishment of the network connection if required */
371     OFLOG_INFO(echoscuLogger, "Association Accepted (Max Send PDV: " << assoc->sendPDVLength << ")");
372 
373     /* do the real work, i.e. send a number of C-ECHO-RQ messages to the DICOM application */
374     /* this application is connected with and handle corresponding C-ECHO-RSP messages. */
375     cond = cecho(assoc, opt_repeatCount);
376 
377     /* tear down association, i.e. terminate network connection to SCP */
378     if (cond == EC_Normal)
379     {
380         if (opt_abortAssociation) {
381             OFLOG_INFO(echoscuLogger, "Aborting Association");
382             cond = ASC_abortAssociation(assoc);
383             if (cond.bad())
384             {
385                 OFLOG_FATAL(echoscuLogger, "Association Abort Failed: " << DimseCondition::dump(temp_str, cond));
386                 exit(1);
387             }
388         } else {
389             /* release association */
390             OFLOG_INFO(echoscuLogger, "Releasing Association");
391             cond = ASC_releaseAssociation(assoc);
392             if (cond.bad())
393             {
394                 OFLOG_FATAL(echoscuLogger, "Association Release Failed: " << DimseCondition::dump(temp_str, cond));
395                 exit(1);
396             }
397         }
398     }
399     else if (cond == DUL_PEERREQUESTEDRELEASE)
400     {
401         OFLOG_FATAL(echoscuLogger, "Protocol Error: Peer requested release (Aborting)");
402         OFLOG_INFO(echoscuLogger, "Aborting Association");
403         cond = ASC_abortAssociation(assoc);
404         result = EXITCODE_ASSOCIATION_ABORTED;// return an error code at the end of main
405         if (cond.bad()) {
406             OFLOG_FATAL(echoscuLogger, "Association Abort Failed: " << DimseCondition::dump(temp_str, cond));
407             exit(1);
408         }
409     }
410     else if (cond == DUL_PEERABORTEDASSOCIATION)
411     {
412         OFLOG_INFO(echoscuLogger, "Peer Aborted Association");
413     }
414     else
415     {
416         OFLOG_ERROR(echoscuLogger, "Echo SCU Failed: " << DimseCondition::dump(temp_str, cond));
417         OFLOG_INFO(echoscuLogger, "Aborting Association");
418         cond = ASC_abortAssociation(assoc);
419         result = EXITCODE_ASSOCIATION_ABORTED; // return an error code at the end of main
420         if (cond.bad()) {
421             OFLOG_FATAL(echoscuLogger, "Association Abort Failed: " << DimseCondition::dump(temp_str, cond));
422             exit(1);
423         }
424     }
425 
426     /* destroy the association, i.e. free memory of T_ASC_Association* structure. This */
427     /* call is the counterpart of ASC_requestAssociation(...) which was called above. */
428     cond = ASC_destroyAssociation(&assoc);
429     if (cond.bad()) {
430         OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
431         exit(1);
432     }
433 
434     /* drop the network, i.e. free memory of T_ASC_Network* structure. This call */
435     /* is the counterpart of ASC_initializeNetwork(...) which was called above. */
436     cond = ASC_dropNetwork(&net);
437     if (cond.bad()) {
438         OFLOG_FATAL(echoscuLogger, DimseCondition::dump(temp_str, cond));
439         exit(1);
440     }
441 
442     OFStandard::shutdownNetwork();
443 
444     cond = tlsOptions.writeRandomSeed();
445     if (cond.bad()) {
446         // failure to write back the random seed is a warning, not an error
447         OFLOG_WARN(echoscuLogger, DimseCondition::dump(temp_str, cond));
448     }
449 
450     return result;
451 }
452 
453 static OFCondition
echoSCU(T_ASC_Association * assoc)454 echoSCU(T_ASC_Association * assoc)
455     /*
456      * This function will send a C-ECHO-RQ over the network to another DICOM application
457      * and handle the response.
458      *
459      * Parameters:
460      *   assoc - [in] The association (network connection to another DICOM application).
461      */
462 {
463     DIC_US msgId = assoc->nextMsgID++;
464     DIC_US status;
465     DcmDataset *statusDetail = NULL;
466 
467     /* dump information if required */
468     OFLOG_INFO(echoscuLogger, "Sending Echo Request (MsgID " << msgId << ")");
469 
470     /* send C-ECHO-RQ and handle response */
471     OFCondition cond = DIMSE_echoUser(assoc, msgId, opt_blockMode, opt_dimse_timeout, &status, &statusDetail);
472 
473     /* depending on if a response was received, dump some information */
474     if (cond.good()) {
475         OFLOG_INFO(echoscuLogger, "Received Echo Response (" << DU_cechoStatusString(status) << ")");
476     } else {
477         OFString temp_str;
478         OFLOG_ERROR(echoscuLogger, "Echo Failed: " << DimseCondition::dump(temp_str, cond));
479     }
480 
481     /* check for status detail information, there should never be any */
482     if (statusDetail != NULL) {
483         OFLOG_DEBUG(echoscuLogger, "Status Detail (should never be any):" << OFendl << DcmObject::PrintHelper(*statusDetail));
484         delete statusDetail;
485     }
486 
487     /* return result value */
488     return cond;
489 }
490 
491 static OFCondition
cecho(T_ASC_Association * assoc,unsigned long num_repeat)492 cecho(T_ASC_Association * assoc, unsigned long num_repeat)
493     /*
494      * This function will send num_repeat C-ECHO-RQ messages to the DICOM application
495      * this application is connected with and handle corresponding C-ECHO-RSP messages.
496      *
497      * Parameters:
498      *   assoc      - [in] The association (network connection to another DICOM application).
499      *   num_repeat - [in] The amount of C-ECHO-RQ messages which shall be sent.
500      */
501 {
502     OFCondition cond = EC_Normal;
503     unsigned long n = num_repeat;
504 
505     /* as long as no error occurred and the counter does not equal 0 */
506     /* send an C-ECHO-RQ and handle the response */
507     while (cond.good() && n--) cond = echoSCU(assoc);
508 
509     return cond;
510 }
511