home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / os2prgc.zip / sterm.c < prev    next >
C/C++ Source or Header  |  1995-03-06  |  20KB  |  620 lines

  1. /*****************************************************************************
  2.  
  3.   STERM -- A simple multi-threaded terminal communications programming
  4.            example.
  5.  
  6.   Copyright (C) 1995 by Craig Morrison, All Rights Reserved.
  7.  
  8.   As with the other modules in this package, if you use it, please
  9.   display my copyright notice in a prominent and conspicuous manner.
  10.  
  11.   What you see here is the combination of information from many sources:
  12.  
  13.     Jeff Garzik         OS2TERM
  14.     Jim Gilliland       OS2COM15
  15.     Peter Fitzsimmons   OS2PROG echo (I *think* I got it right this
  16.                                       time Peter.)
  17.  
  18.   While Jeff had the right idea, his terminal program really smacked of
  19.   MeSsy-DOS programming by the never-ending polling of the keyboard and
  20.   comm port. In short, it required too much CPU time. DosEnterCritSec()
  21.   and DosExitCritSec() should _not_ be made available to application
  22.   programmers. While they make contentions easy to handle and at first
  23.   seem to improve performance, they have an overall impact on the
  24.   application itself. Using them defeats the purpose of writing a
  25.   multi-threaded program in the first place, as they halt all threads in
  26.   a process.
  27.  
  28.   Jim's code was almost perfect, except for one small problem, it was
  29.   all 16-bit.
  30.  
  31.   What I have done here is take ideas from both sets of code along with
  32.   a lot of advice from Peter Fitzsimmons (read from the OS2PROG echo) and
  33.   produced a truly OS/2 friendly terminal program. Polling is kept to an
  34.   absolute bare minimum.
  35.  
  36.   Peter should be happy with this example. :-)
  37.  
  38.   Lots of good stuff here. Both Mutex and Event semaphores are used to
  39.   handle resource contentions in a friendly manner. 4 different threads
  40.   are used; The main thread handles getting the whole mess up and
  41.   running, then it blocks, waiting for the keyboard thread to end. The
  42.   keyboard thread, keyboard_read(), handles all the keystrokes from the
  43.   local console and determines when we exit. The comm read thread,
  44.   async_read(), takes care of getting data from the port into our
  45.   receive buffer. And lastly, the comm port processing thread,
  46.   async_process(), takes care of handling any special cases of data
  47.   coming from the comm port.
  48.  
  49.   There is one small problem with the way the receive buffer is handled.
  50.   If the code that removes characters from the buffer gets held up too
  51.   long there is a possibility of the head of the buffer over-running the
  52.   tail. A sufficiently large buffer should keep this from occuring.
  53.  
  54.   In a perfect world we would also get and save the original device
  55.   control block before we change it. Then just before we exit, restore
  56.   the device control block leaving things the way we found them. But hey,
  57.   it ain't a perfect world and it's not anything MODE won't fix. :-}
  58.  
  59.  *****************************************************************************/
  60. #define INCL_DOS
  61. #define INCL_DOSDEVIOCTL
  62. #define INCL_SUB
  63. #define INCL_NOPMAPI
  64. #include <os2.h>
  65. #include "str.h"
  66. #include "file_io.h"
  67.  
  68. #define CBSTACK             8192
  69. #define BAUD_RATE           38400
  70. #define COMM_BUFFER_SIZE    4096
  71. #define COMM_RXBUFFER_SIZE  128
  72. #define MAX_DIALERS         5
  73. #define BANNER              "\x1b[2J\r\nSTERM v1.00, Craig Morrison\r\n" \
  74.                             "Alt-X to exit, Alt-Z for list of commands\r\n\n"
  75. #define USAGE               "Usage: STERM <portnumber>\r\n"
  76. #define NO_KEYPROC          "Can't create keyboard processing thread!\r\n"
  77. #define NO_COMMPROC         "Can't create comm port processing thread!\r\n"
  78. #define NO_COMMTID          "Can't create comm port read thread!\r\n"
  79. #define NO_DCB              "Can't set device control block info!\r\n"
  80. #define NO_LINESET          "Can't set line settings!\r\n"
  81. #define NO_COMMPORT         "Can't open com port!\r\n"
  82. #define CMD_STR             "\r\nCommands:\r\n\nAlt-C  Clear Console.\r\n" \
  83.                             "Alt-D  Dial from directory.\r\n" \
  84.                             "Alt-H  Hang up with DTR.\r\n" \
  85.                             "Alt-X  Quit STERM.\r\nAlt-Z  This list.\r\n" \
  86.                             "\r\n"
  87. #define THANK_YOU           "\r\nThank you for using STERM.\r\nSee ya!\r\n"
  88. #define HANG_UP             "\r\nToggling DTR to hangup..."
  89. #define CLICK               "<CLICK!>\r\n"
  90. #define NO_DIALSTRINGS      "Your dialing directory is empty.\r\n"
  91. #define ERR_TOOHIGH         "Input out of range.\r\n"
  92. #define ERR_NODIALER        "That slot is empty.\r\n"
  93. #define DIAL_PROMPT         "\r\nSelect (ENTER alone aborts): "
  94. #define DIR_HEADER          "\r\n\nDialing Directory:\r\n\n"
  95. #define ANSI_RCP            "\x1b[u"
  96.  
  97. char buffer[COMM_BUFFER_SIZE];
  98. char dialStr[MAX_DIALERS][128];
  99. int head = 0, tail = 0;
  100. HFILE hCom = NULLHANDLE;
  101. TID kbdProcTID = (TID)-1;
  102. TID comProcTID = (TID)-1;
  103. TID comReceiveTID = (TID)-1;
  104. HMTX comSem = NULLHANDLE;
  105. HEV comReadyHEV = NULLHANDLE;
  106.  
  107. /* send one character to the comm port */
  108. void charOutPort(char ch)
  109. {
  110.     ULONG BytesWritten;
  111.  
  112.     DosWrite(hCom, &ch, sizeof(ch), &BytesWritten);
  113. }
  114.  
  115. /* send an entire string to the comm port */
  116. void strOutPort(PSZ s)
  117. {
  118.     ULONG BytesWritten;
  119.  
  120.     DosWrite(hCom, s, lenstr(s), &BytesWritten);
  121. }
  122.  
  123. /*****************************************************************************
  124.  
  125.   Dial a number from our dialing directory.
  126.  
  127.  *****************************************************************************/
  128. void DialFromDirectory(void)
  129. {
  130.     char s[128];
  131.     PSZ p;
  132.     ULONG i;
  133.     KBDKEYINFO  ki;
  134.  
  135.     if (dialStr[0][0]==0)
  136.     {
  137.         VioWrtTTY(NO_DIALSTRINGS, lenstr(NO_DIALSTRINGS), 0L);
  138.         return;
  139.     }
  140.     else
  141.     {
  142.         VioWrtTTY(DIR_HEADER, lenstr(DIR_HEADER), 0L);
  143.         for (i=0; i<MAX_DIALERS; i++)
  144.         {
  145.             if (dialStr[i][0]==0)
  146.                 break;
  147.             else
  148.             {
  149.                 ultoasc(i, s);
  150.                 catstr(s, ")  ");
  151.                 VioWrtTTY(s, lenstr(s), 0L);
  152.                 VioWrtTTY(dialStr[i], lenstr(dialStr[i]), 0L);
  153.                 VioWrtTTY("\r\n", 2, 0L);
  154.             }
  155.         }
  156.         VioWrtTTY(DIAL_PROMPT, lenstr(DIAL_PROMPT), 0L);
  157.  
  158.         s[0] = 0;
  159.         i = 0;
  160.         for (;;)
  161.         {
  162.             KbdCharIn(&ki, IO_WAIT, 0);
  163.             if (!(ki.fbStatus & 0x02))
  164.             {
  165.                 switch(ki.chChar)
  166.                 {
  167.                     case '0':
  168.                     case '1':
  169.                     case '2':
  170.                     case '3':
  171.                     case '4':
  172.                     case '5':
  173.                     case '6':
  174.                     case '7':
  175.                     case '8':
  176.                     case '9':
  177.                         if (i<2)
  178.                         {
  179.                             s[i] = ki.chChar;
  180.                             VioWrtTTY(&s[i], 1, 0L);
  181.                             i++;
  182.                         }
  183.                         break;
  184.  
  185.                     case '\b':
  186.                         if (i)
  187.                         {
  188.                             VioWrtTTY("\b \b", 3, 0L);
  189.                             i--;
  190.                         }
  191.                         break;
  192.                 }
  193.                 s[i] = 0;
  194.  
  195.                 if (ki.chChar=='\r')
  196.                     break;
  197.             }
  198.         }
  199.         VioWrtTTY("\r\n\n", 2, 0L);
  200.  
  201.         if (lenstr(s))
  202.         {
  203.             i = asctoul(s);
  204.             if (i>=MAX_DIALERS)
  205.                 VioWrtTTY(ERR_TOOHIGH, lenstr(ERR_TOOHIGH), 0L);
  206.             else if (dialStr[i][0]==0)
  207.                 VioWrtTTY(ERR_NODIALER, lenstr(ERR_NODIALER), 0L);
  208.             else
  209.             {
  210.                 strOutPort("atdt");
  211.                 cpystrn(s, dialStr[i], 128);
  212.                 p = charinstr(s, ' ');
  213.                 if (p)
  214.                     *p = 0;
  215.                 strOutPort(s);
  216.                 strOutPort("\r");
  217.             }
  218.         }
  219.     }
  220. }
  221.  
  222. /* Raise or lower DTR signal, based on input */
  223. void dtr(int i)
  224. {
  225.     MODEMSTATUS ms;
  226.     UINT data;
  227.  
  228.     ms.fbModemOn = i ? DTR_ON : 0;
  229.     ms.fbModemOff = i ? 255 : DTR_OFF;
  230.     DosDevIOCtl(hCom, IOCTL_ASYNC, ASYNC_SETMODEMCTRL, &ms,
  231.                 sizeof(ms), NULL, &data, sizeof (data), NULL);
  232. }
  233.  
  234. /* Removes one character from our port buffer, as an aside, it also watches */
  235. /* for an ANSI DSR sequence, when detected a RCP is sent to the port (many  */
  236. /* BBS software packages use a DSR to detect ANSI capabilities)             */
  237. char charInPort(void)
  238. {
  239.     unsigned long cbOut;
  240.     char c = 0;
  241.     static char dsr = 0;
  242.  
  243.     DosRequestMutexSem(comSem, SEM_INDEFINITE_WAIT);
  244.     if (head != tail)
  245.     {
  246.         c = buffer[tail++];
  247.         if (tail == COMM_BUFFER_SIZE)
  248.             tail = 0;
  249.         if (c==27)
  250.             dsr = c;
  251.         else if (dsr)
  252.         {
  253.             if ((dsr==27) && (c=='['))
  254.                 dsr = c;
  255.             else if ((dsr=='[') && (c=='6'))
  256.                 dsr = c;
  257.             else if ((dsr=='6') && (c=='n'))
  258.             {
  259.                 DosWrite(hCom, ANSI_RCP, lenstr(ANSI_RCP), &cbOut);
  260.                 dsr = 0;
  261.             }
  262.             else
  263.                 dsr = 0;
  264.         }
  265.     }
  266.     DosReleaseMutexSem(comSem);
  267.  
  268.     return(c);
  269. }
  270.  
  271. /*****************************************************************************
  272.  
  273.   This is probably the most crucial of the threads. Its job is to read
  274.   incoming data from the comm port and stow it away for later retrieval.
  275.  
  276.   Note the use of a mutex semaphore to keep the async_process thread from
  277.   modifying the buffer while we are messing with it.
  278.  
  279.   We post to an event semaphore to unblock the async_process thread if it
  280.   is waiting for something to come in from the port.
  281.  
  282.  *****************************************************************************/
  283. VOID async_read(PVOID p)
  284. {
  285.     ULONG BytesRead, i;
  286.     char ch[COMM_RXBUFFER_SIZE];
  287.  
  288.     p = p;  /* shutup compiler */
  289.  
  290.     for (;;)    /* It keeps on going and going and going and going... */
  291.     {
  292.         /* Try to read in COMM_RXBUFFER_SIZE bytes           */
  293.         /* Since we have the port set to wait-for-something  */
  294.         /* read timeout processing, as soon as the device    */
  295.         /* driver gets some data for us, it coughs it up     */
  296.         if (!DosRead(hCom, ch, COMM_RXBUFFER_SIZE, &BytesRead))
  297.         {
  298.             if (BytesRead)
  299.             {
  300.                 /* block here if someone else is mucking with the buffer */
  301.                 DosRequestMutexSem(comSem, SEM_INDEFINITE_WAIT);
  302.  
  303.                 /* put all the characters that came in, into the buffer */
  304.                 for (i=0; i<BytesRead; i++)
  305.                 {
  306.                     buffer[head] = ch[i];
  307.                     head++;
  308.                     if (head == COMM_BUFFER_SIZE)
  309.                       head = 0;
  310.                 }
  311.  
  312.                 /* release mutex semaphore so others can get at the buffer */
  313.                 DosReleaseMutexSem(comSem);
  314.  
  315.                 /* post to the event sem in case they are blocked waiting */
  316.                 DosPostEventSem(comReadyHEV);
  317.             }
  318.         }
  319.     }
  320. }
  321.  
  322. /*****************************************************************************
  323.  
  324.   This thread actually does something with the data that has come in from the
  325.   comm port.
  326.  
  327.  *****************************************************************************/
  328. VOID async_process(PVOID p)
  329. {
  330.     char ch;
  331.     char s[2] = " ";
  332.     ULONG evCount;
  333.     BOOL cbChars;
  334.  
  335.     p = p;  /* shut compiler */
  336.  
  337.     for (;;)    /* loop and loop and loop and loop... */
  338.     {
  339.         /* We need to see if anything is waiting in the buffer       */
  340.         /* so we first request the mutex semaphore so the async_read */
  341.         /* will block and leave the buffer pointers alone            */
  342.  
  343.         DosRequestMutexSem(comSem, SEM_INDEFINITE_WAIT);
  344.         cbChars = (head != tail);
  345.         DosReleaseMutexSem(comSem);
  346.  
  347.         /* if nothing is waiting, block on an event semaphore until we */
  348.         /* get something in from the comm port                         */
  349.         if (!cbChars)
  350.         {
  351.             DosWaitEventSem(comReadyHEV, SEM_INDEFINITE_WAIT);
  352.             DosResetEventSem(comReadyHEV, &evCount);
  353.         }
  354.  
  355.         /* get a character and process it */
  356.         switch (ch = charInPort())
  357.         {
  358.             case 0:
  359.             case 255: /* ignore null characters */
  360.                 break;
  361.  
  362.             case 12: /* formfeed char = clear screen */
  363.                 VioScrollUp(0, 0, 65535, 65535, 65535, s, 0);
  364.                 VioSetCurPos(0, 0, 0);
  365.                 break;
  366.  
  367.             default:    /* everything else gets written out */
  368.                 s[0] = ch;
  369.                 VioWrtTTY(s, 1, 0L);
  370.                 break;
  371.         }
  372.     }
  373. }
  374.  
  375. /*****************************************************************************
  376.  
  377.   This thread reads and processes all keyboard input.
  378.  
  379.  *****************************************************************************/
  380. VOID keyboard_read(PVOID p)
  381. {
  382.     KBDKEYINFO  ki;
  383.  
  384.     p = p;
  385.  
  386.     for(;;)
  387.     {
  388.         KbdCharIn(&ki, IO_WAIT, 0);
  389.  
  390.         if (ki.fbStatus & 0x02) /* extended character? */
  391.         {
  392.             switch (ki.chScan)
  393.             {
  394.                 case 0x20:  /* alt-d - dial number */
  395.                     DialFromDirectory();
  396.                     break;
  397.  
  398.                 case 0x2E:  /* alt-c - clear screen */
  399.                     VioWrtTTY("\x1b[2J", 4, 0L);
  400.                     break;
  401.  
  402.                 case 0x23:  /* alt-h - drop DTR to hang up */
  403.                     VioWrtTTY(HANG_UP, lenstr(HANG_UP), 0);
  404.                     dtr(0);
  405.                     DosSleep(500);
  406.                     dtr(1);
  407.                     VioWrtTTY(CLICK, lenstr(CLICK), 0);
  408.                     break;
  409.  
  410.                 case 0x2D:  /* alt-x - exit program */
  411.                     VioWrtTTY(THANK_YOU, lenstr(THANK_YOU), 0);
  412.                     DosExit(EXIT_THREAD, 0);
  413.                     break;
  414.  
  415.                 case 0x2C:  /* alt-z - print menu */
  416.                     VioWrtTTY(CMD_STR, lenstr(CMD_STR), 0);
  417.                     break;
  418.             }
  419.         }
  420.         else
  421.             charOutPort((unsigned char) ki.chChar);
  422.     }
  423. }
  424.  
  425. /*****************************************************************************
  426.  
  427.   Sets the bit rate for the comm port.
  428.  
  429.  *****************************************************************************/
  430. void set_baud(unsigned int rate)
  431. {
  432.     USHORT Baud = (USHORT) rate;
  433.  
  434.     if ((rate <= 57600) && (rate >= 50))
  435.         DosDevIOCtl(hCom, IOCTL_ASYNC, ASYNC_SETBAUDRATE,
  436.                     &Baud, sizeof(USHORT), NULL, NULL, 0L, NULL);
  437. }
  438.  
  439. /*****************************************************************************
  440.  
  441.   Tidies things up before we quit.
  442.  
  443.  *****************************************************************************/
  444. void closeport(void)
  445. {
  446.     if (comReceiveTID!=(TID)-1)
  447.     {
  448.         DosKillThread(comReceiveTID);
  449.         DosWaitThread(&comReceiveTID, DCWW_WAIT);
  450.     }
  451.  
  452.     if (comProcTID!=(TID)-1)
  453.     {
  454.         DosKillThread(comProcTID);
  455.         DosWaitThread(&comProcTID, DCWW_WAIT);
  456.     }
  457.  
  458.     if (kbdProcTID!=(TID)-1)
  459.     {
  460.         DosKillThread(kbdProcTID);
  461.         DosWaitThread(&kbdProcTID, DCWW_WAIT);
  462.     }
  463.  
  464.     if (comReadyHEV)
  465.         DosCloseEventSem(comReadyHEV);
  466.  
  467.     if (comSem)
  468.         DosCloseMutexSem(comSem);
  469.  
  470.     if (hCom)
  471.         DosClose(hCom);
  472.  
  473.     CleanUpFileIO();
  474. }
  475.  
  476. /*****************************************************************************
  477.  
  478.   Opens the comm port, sets the parameters for port communications and
  479.   starts up the threads to handle the port and keyboard.
  480.  
  481.  *****************************************************************************/
  482. void initport(char port_num)
  483. {
  484.     char s[10] = "COMx";
  485.     APIRET rc;
  486.     ULONG action;
  487.     LINECONTROL lctl;
  488.     DCBINFO dcb;
  489.  
  490.     InitFileIO();
  491.     DosCreateMutexSem(NULL, &comSem, DC_SEM_SHARED, FALSE);
  492.     DosCreateEventSem(NULL, &comReadyHEV, DC_SEM_SHARED, 0L);
  493.  
  494.     s[3] = port_num;
  495.     if (DosOpen(s, &hCom, &action, 0L, 0, FILE_OPEN,
  496.                 OPEN_ACCESS_READWRITE | OPEN_SHARE_DENYREADWRITE |
  497.                 OPEN_FLAGS_FAIL_ON_ERROR, 0L))
  498.     {
  499.         VioWrtTTY(NO_COMMPORT, lenstr(NO_COMMPORT), 0);
  500.         closeport();
  501.         DosExit(EXIT_PROCESS, 1);
  502.     }
  503.  
  504.     /* set line to no parity, 8 databits, 1 stop bit */
  505.     lctl.bParity = 0;
  506.     lctl.bDataBits = 8;
  507.     lctl.bStopBits = 0;
  508.     lctl.fTransBreak = 0;
  509.     if (DosDevIOCtl(hCom, IOCTL_ASYNC, ASYNC_SETLINECTRL,
  510.                     &lctl, sizeof(LINECONTROL), NULL, NULL, 0L, NULL))
  511.     {
  512.         VioWrtTTY(NO_LINESET, lenstr(NO_LINESET), 0);
  513.         closeport();
  514.         DosExit(EXIT_PROCESS, 1);
  515.     }
  516.  
  517.     /* set device control block info */
  518.     dcb.usWriteTimeout = 100;               /* one second write timeout */
  519.     dcb.usReadTimeout = 100;                /* one second read timeout */
  520.     dcb.fbCtlHndShake = MODE_DTR_CONTROL;
  521.     dcb.fbFlowReplace = MODE_RTS_CONTROL;
  522.     dcb.fbTimeout = MODE_WAIT_READ_TIMEOUT; /* see the CP Online Reference */
  523.     dcb.bErrorReplacementChar = 0x00;
  524.     dcb.bBreakReplacementChar = 0x00;
  525.     dcb.bXONChar = 0x11;
  526.     dcb.bXOFFChar = 0x13;
  527.     if (DosDevIOCtl(hCom, IOCTL_ASYNC, ASYNC_SETDCBINFO, &dcb,
  528.                     sizeof(DCBINFO), 0L, NULL, 0L, NULL))
  529.     {
  530.         VioWrtTTY(NO_DCB, lenstr(NO_DCB), 0);
  531.         closeport();
  532.         DosExit(EXIT_PROCESS, 1);
  533.     }
  534.  
  535.     /* Initially our buffer is empty */
  536.     head = tail = 0;
  537.  
  538.     if (DosCreateThread(&comReceiveTID, (PFNTHREAD)async_read, 0, 0, CBSTACK))
  539.     {
  540.         VioWrtTTY(NO_COMMTID, lenstr(NO_COMMTID), 0);
  541.         closeport();
  542.         DosExit(EXIT_PROCESS, 1);
  543.     }
  544.     if (DosCreateThread(&comProcTID, (PFNTHREAD)async_process, 0, 0, CBSTACK))
  545.     {
  546.         VioWrtTTY(NO_COMMPROC, lenstr(NO_COMMPROC), 0);
  547.         closeport();
  548.         DosExit(EXIT_PROCESS, 1);
  549.     }
  550.     if (DosCreateThread(&kbdProcTID, (PFNTHREAD)keyboard_read, 0, 0, CBSTACK))
  551.     {
  552.         VioWrtTTY(NO_KEYPROC, lenstr(NO_KEYPROC), 0);
  553.         closeport();
  554.         DosExit(EXIT_PROCESS, 1);
  555.     }
  556.     dtr (1);
  557. }
  558.  
  559. /*****************************************************************************
  560.  
  561.   Program entry point, here's the scoop on what it does:
  562.  
  563.     1) ANSI support is turned ON.
  564.     2) Check for a command line arg, we need at least 1.
  565.     3) Initialize the port and start up all of our threads.
  566.     4) Set the bit rate for the port.
  567.     5) Read in our dialing directory.
  568.     6) Wait on the keyboard thread until it dies.
  569.     7) Close the port and tidy up.
  570.     8) Exit with return code 0.
  571.  
  572.  *****************************************************************************/
  573. void main(int argc, char *argv[])
  574. {
  575.     int i, f;
  576.     PSZ p;
  577.     ULONG ulAction;
  578.  
  579.     VioSetAnsi(ANSI_ON, 0L);
  580.  
  581.     VioWrtTTY(BANNER, lenstr(BANNER), 0);
  582.  
  583.     if (argc != 2)
  584.     {
  585.         VioWrtTTY(USAGE, lenstr(USAGE), 0);
  586.         DosExit(EXIT_PROCESS, 1);
  587.     }
  588.  
  589.     initport(argv[1][0]);
  590.     set_baud(BAUD_RATE);
  591.  
  592.     setmem(dialStr, 0, MAX_DIALERS*128);
  593.  
  594.     f = OpenFile("PHONBOOK.TXT", &ulAction, 0, 0,
  595.                  OPEN_ACTION_OPEN_IF_EXISTS,
  596.                  OPEN_ACCESS_READONLY | OPEN_SHARE_DENYNONE,
  597.                  NULL);
  598.     if (f)
  599.     {
  600.         for (i=0; i<MAX_DIALERS; i++)
  601.         {
  602.             if (LineRead(f, dialStr[i]))
  603.                 break;
  604.             else
  605.             {
  606.                 p = charinstr(dialStr[i], '\n');
  607.                 if (p)
  608.                     *p = 0;
  609.             }
  610.         }
  611.         CloseFile(f);
  612.     }
  613.  
  614.     DosWaitThread(&kbdProcTID, DCWW_WAIT);
  615.     kbdProcTID = (TID)-1;
  616.  
  617.     closeport();
  618.     DosExit(EXIT_PROCESS, 0);
  619. }
  620.