Чтобы иметь возможность работать с COM-портом как с потоком нужно немного расширить стандартный модуль CommV24, добавив в него процедуру NewStream в соответствии со спецификацией из CommStreams. Вот моя реализация:
## Код:
```
MODULE CommV24;
(**
   project   = "BlackBox"
   organization   = "www.oberon.ch"
   contributors   = "Oberon microsystems", Alexander Iljin
   version   = "System/Rsrc/About"
   copyright   = "System/Rsrc/About"
   license   = "Docu/BB-License"
   changes   = "
   - 20061007, ai, ParseParams published. Fixed ParseParams handling last parameter.
   - 20061007, ai, Deleted ASSERT(101) from procedure Available.
   - 20060919, ai, Added support for virtual devices ("\\.\" prefix).
   - 20060916, ai, Implemented non-blocking access via this module using CommStreams.Stream.
   - 20060916, ai, Added inDTRon, inRTSon support.
   - 20060916, ai, Some parameter restrictions enforced with ASSERTs. Added IsConnected.
   - 20060916, ai, All active connections are closed upon module unload.
   - 20060915, ai, SYSTEM module highlighted in bold.
   - 20060915, ai, Output to StdLog deleted.
   "
   issues   = ""

**)

   IMPORT SYSTEM, WinApi, CommStreams, Strings;

   CONST
      bits4* = 0; bits5* = 1; bits6* = 2; bits7* = 3; stop15* = 4; stop2* = 5; even* = 6; odd* = 7; inXonXoff* = 8; outXonXoff* = 9; inRTS* = 10; inDTR* = 11; outCTS* = 12; outDSR* = 13; inDTRon* = 14; inRTSon* = 15;

   TYPE
      Connection* = POINTER TO LIMITED RECORD
         hnd: WinApi.HANDLE;   (* # 0: open *)
         opts: SET
      END;

      Stream = POINTER TO LIMITED RECORD (CommStreams.Stream)
         conn: Connection;
         remoteAdr: CommStreams.Adr;
         maxWrite: INTEGER
      END;

      ConnectionChain = POINTER TO RECORD
         conn: Connection;
         next: ConnectionChain
      END;

   VAR root: ConnectionChain;

   PROCEDURE IsConnected (c: Connection): BOOLEAN;
   BEGIN
      ASSERT(c # NIL, 20);
      RETURN c.hnd # 0
   END IsConnected;

   PROCEDURE Remove (c: Connection);
      VAR cc, prev: ConnectionChain;
   BEGIN
      ASSERT(~IsConnected(c), 21); (* only closed connections should be removed from the chain *)
      ASSERT(root # NIL, 100); (* if a connection was created, it should be in the list *)
      IF c = root.conn THEN
         root := root.next
      ELSE
         ASSERT(root.next # NIL, 100); (* if a connection was created, it should be in the list *)
         prev := root;
         cc := root.next;
         WHILE (cc # NIL) & (cc.conn # c) DO
            prev := cc;
            cc := cc.next
         END;
         ASSERT((cc # NIL) & (cc.conn = c), 100); (* if a connection was created, it should be in the list *)
         prev.next := cc.next
      END
   END Remove;

   PROCEDURE Close* (c: Connection);
      VAR res: INTEGER;
   BEGIN
      ASSERT(c # NIL, 20);
      IF IsConnected(c) THEN
         res := WinApi.CloseHandle(c.hnd);
         ASSERT(res # 0, 100);
         c.hnd := 0;
         Remove(c)
      END
   END Close;

   PROCEDURE Open* (device: ARRAY OF CHAR; baud: INTEGER; opts: SET; OUT conn: Connection);
      VAR c: Connection; h: WinApi.HANDLE; res: INTEGER; dcb: WinApi.DCB; to: WinApi.COMMTIMEOUTS;
         s: ARRAY WinApi.MAX_PATH OF CHAR;
         cc: ConnectionChain;
   BEGIN
      conn := NIL; (* Alexander Iljin: this was not ensured *)
      ASSERT(opts * {even, odd} # {even, odd}, 20);
      IF bits4 IN opts THEN ASSERT(opts * {bits5, bits6, bits7} = {}, 21) END;
      IF bits5 IN opts THEN ASSERT(opts * {bits4, bits6, bits7} = {}, 21) END;
      IF bits6 IN opts THEN ASSERT(opts * {bits4, bits5, bits7} = {}, 21) END;
      IF bits7 IN opts THEN ASSERT(opts * {bits4, bits5, bits6} = {}, 21) END;
      ASSERT(opts * {inDTR, inDTRon} # {inDTR, inDTRon}, 22);
      ASSERT(opts * {inRTS, inRTSon} # {inRTS, inRTSon}, 23);
      IF device[0] # "\" THEN s := "\\.\" + device
      ELSE s := device$
      END;
      h := WinApi.CreateFileW(
         s,  WinApi.GENERIC_READ +  WinApi.GENERIC_WRITE,
         {}, NIL,  WinApi.OPEN_EXISTING, {}, 0
      );
      IF h # -1 THEN
         dcb.DCBlength := SIZE(WinApi.DCB);
         res := WinApi.GetCommState(h, dcb);
         IF res # 0 THEN
            dcb.BaudRate := baud;
            dcb.fBits0 := {0};   (* binary *)
            IF opts * {even, odd} # {} THEN INCL(dcb.fBits0, 1) END;   (* check parity *)
            IF outCTS IN opts THEN INCL(dcb.fBits0, 2) END;   (* CTS out flow control *)
            IF outDSR IN opts THEN INCL(dcb.fBits0, 3) END;   (* DSR out flow control *)
            IF inDTR IN opts THEN INCL(dcb.fBits0, 5)            (* DTR flow control handshake *)
            ELSIF inDTRon IN opts THEN INCL(dcb.fBits0, 4)      (* DTR enable*)
            END;
            IF outXonXoff IN opts THEN INCL(dcb.fBits0, 8) END;   (* Xon/Xoff out flow control *)
            IF inXonXoff IN opts THEN INCL(dcb.fBits0, 9) END;   (* Xob/Xoff in flow control *)
            IF inRTS IN opts THEN INCL(dcb.fBits0, 13)            (* RTS flow control handshake *)
            ELSIF inRTSon IN opts THEN INCL(dcb.fBits0, 12)   (* RTS enable*)
            END;
            IF bits4 IN opts THEN dcb.ByteSize := 4X
            ELSIF bits5 IN opts THEN dcb.ByteSize := 5X
            ELSIF bits6 IN opts THEN dcb.ByteSize := 6X
            ELSIF bits7 IN opts THEN dcb.ByteSize := 7X
            ELSE dcb.ByteSize := 8X
            END;
            IF stop15 IN opts THEN dcb.StopBits := 1X
            ELSIF stop2 IN opts THEN dcb.StopBits := 2X
            ELSE dcb.StopBits := 0X
            END;
            IF even IN opts THEN dcb.Parity := 2X
            ELSIF odd IN opts THEN dcb.Parity := 1X
            ELSE dcb.Parity := 0X
            END;
            res := WinApi.SetCommState(h, dcb);
            IF res # 0 THEN
               to.ReadIntervalTimeout := 0;
               to.ReadTotalTimeoutMultiplier := 0;
               to.ReadTotalTimeoutConstant := 0;
               to.WriteTotalTimeoutMultiplier := 0;
               to.WriteTotalTimeoutConstant := 0;
               res := WinApi.SetCommTimeouts(h, to);
               IF res # 0 THEN
                  NEW(c);
                  c.hnd := h;
                  c.opts := opts;
                  NEW(cc);
                  cc.conn := c;
                  cc.next := root;
                  root := cc;
                  conn := c
               END
            END
         END
      END
   END Open;

   PROCEDURE SendByte* (c: Connection; x: BYTE);
      VAR res, written: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      written := 0;
      res := WinApi.WriteFile(c.hnd, SYSTEM.ADR(x), 1, written, NIL);
      ASSERT(res # 0, 100);
      ASSERT(written = 1, 101)
   END SendByte;

   PROCEDURE SendBytes* (c: Connection; IN x: ARRAY OF BYTE; beg, len: INTEGER);
      VAR res, written: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      ASSERT(LEN(x) >= beg + len, 22);
      ASSERT(len > 0, 23);
      written := 0;
      res := WinApi.WriteFile(c.hnd, SYSTEM.ADR(x) + beg, len, written, NIL);
      ASSERT(res # 0, 100);
      ASSERT(written = len, 101)
   END SendBytes;

   PROCEDURE Available* (c: Connection): INTEGER;
      VAR res: INTEGER; errors: SET; status: WinApi.COMSTAT;
   BEGIN
      ASSERT(IsConnected(c), 21);
      errors := {};
      status.cbInQue := 0;
      res := WinApi.ClearCommError(c.hnd, errors, status);
      ASSERT(res # 0, 100);
      RETURN status.cbInQue
   END Available;

   PROCEDURE ReceiveByte* (c: Connection; OUT x: BYTE);
      VAR res, read: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      read := 0;
      res := WinApi.ReadFile(c.hnd, SYSTEM.ADR(x), 1, read, NIL);
      ASSERT(res # 0, 100);
      ASSERT(read = 1, 101)
   END ReceiveByte;

   PROCEDURE ReceiveBytes* (c: Connection; OUT x: ARRAY OF BYTE; beg, len: INTEGER);
      VAR res, read: INTEGER;
   BEGIN
      IF len = 0 THEN RETURN END;
      ASSERT(IsConnected(c), 21);
      ASSERT(LEN(x) >= beg + len, 22);
      ASSERT(len > 0, 23);
      read := 0;
      res := WinApi.ReadFile(c.hnd, SYSTEM.ADR(x) + beg, len, read, NIL);
      ASSERT(res # 0, 100);
      ASSERT(read = len, 101)
   END ReceiveBytes;

   PROCEDURE SetBuffers* (c: Connection; inpBufSize, outBufSize: INTEGER);
      VAR res: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      res := WinApi.SetupComm(c.hnd, inpBufSize, outBufSize);
      ASSERT(res # 0, 100)
   END SetBuffers;

   PROCEDURE SetDTR* (c: Connection; on: BOOLEAN);
      VAR res: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      ASSERT(~(inDTR IN c.opts), 22);
      IF on THEN res := WinApi.EscapeCommFunction(c.hnd, WinApi.SETDTR)
      ELSE res := WinApi.EscapeCommFunction(c.hnd, WinApi.CLRDTR)
      END;
      ASSERT(res # 0, 100)
   END SetDTR;

   PROCEDURE SetRTS* (c: Connection; on: BOOLEAN);
      VAR res: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      ASSERT(~(inRTS IN c.opts), 22);
      IF on THEN res := WinApi.EscapeCommFunction(c.hnd, WinApi.SETRTS)
      ELSE res := WinApi.EscapeCommFunction(c.hnd, WinApi.CLRRTS)
      END;
      ASSERT(res # 0, 100)
   END SetRTS;

   PROCEDURE SetBreak* (c: Connection; on: BOOLEAN);
      VAR res: INTEGER;
   BEGIN
      ASSERT(IsConnected(c), 21);
      IF on THEN res := WinApi.EscapeCommFunction(c.hnd, WinApi.SETBREAK)
      ELSE res := WinApi.EscapeCommFunction(c.hnd, WinApi.CLRBREAK)
      END;
      ASSERT(res # 0, 100)
   END SetBreak;

   PROCEDURE CTSState* (c: Connection): BOOLEAN;
      VAR res: INTEGER; s: SET;
   BEGIN
      ASSERT(IsConnected(c), 21);
      s := {};
      res := WinApi.GetCommModemStatus(c.hnd, s);
      ASSERT(res # 0, 100);
      RETURN s * WinApi.MS_CTS_ON # {}
   END CTSState;

   PROCEDURE DSRState* (c: Connection): BOOLEAN;
      VAR res: INTEGER; s: SET;
   BEGIN
      ASSERT(IsConnected(c), 21);
      s := {};
      res := WinApi.GetCommModemStatus(c.hnd, s);
      ASSERT(res # 0, 100);
      RETURN s * WinApi.MS_DSR_ON # {}
   END DSRState;

   PROCEDURE CDState* (c: Connection): BOOLEAN;
      VAR res: INTEGER; s: SET;
   BEGIN
      ASSERT(IsConnected(c), 21);
      s := {};
      res := WinApi.GetCommModemStatus(c.hnd, s);
      ASSERT(res # 0, 100);
      RETURN s * WinApi.MS_RLSD_ON # {}
   END CDState;


   (* Stream*)

   PROCEDURE (s: Stream) RemoteAdr* (): CommStreams.Adr;
      VAR str: CommStreams.Adr;
   BEGIN
      NEW(str, LEN(s.remoteAdr));
      str^ := s.remoteAdr$;
      RETURN str
   END RemoteAdr;

   PROCEDURE (s: Stream) IsConnected* (): BOOLEAN;
   BEGIN
      RETURN s.conn # NIL
   END IsConnected;

   PROCEDURE (s: Stream) WriteBytes* (IN x: ARRAY OF BYTE; beg, len: INTEGER; OUT written: INTEGER);
   BEGIN
      IF s.IsConnected() THEN
         IF len > s.maxWrite THEN written := s.maxWrite
         ELSE written := len
         END;
         SendBytes(s.conn, x, beg, written)
      ELSE
         written := 0
      END
   END WriteBytes;

   PROCEDURE (s: Stream) ReadBytes* (VAR x: ARRAY OF BYTE; beg, len: INTEGER; OUT read: INTEGER);
   BEGIN
      IF s.IsConnected() THEN
         read := Available(s.conn);
         IF read > len THEN read := len END;
         ReceiveBytes(s.conn, x, beg, read)
      ELSE
         read := 0
      END
   END ReadBytes;

   PROCEDURE (s: Stream) Close*;
   BEGIN
      IF s.IsConnected() THEN
         Close(s.conn);
         s.conn := NIL
      END
   END Close;

   PROCEDURE ParseParams* (IN params: ARRAY OF CHAR; OUT device: ARRAY OF CHAR; OUT baud: INTEGER; OUT opts: SET): INTEGER;
      CONST
         maxParamLen = 15;
         paramDelimiter = ',';
      TYPE
         Param = POINTER TO ARRAY maxParamLen OF CHAR;
      VAR
         par: Param;
         i, res, badParamIndex, paramsLen: INTEGER;
         read, tooLong: BOOLEAN;

      PROCEDURE NextPar(p: Param; OUT read, tooLong: BOOLEAN);
         VAR pInd: INTEGER;
      BEGIN
         pInd := 0;
         WHILE (i < paramsLen) & (params[i] # paramDelimiter) & (pInd < maxParamLen - 1) DO
            p[pInd] := params[i];
            INC(pInd);
            INC(i)
         END;
         IF (i < paramsLen) & (params[i] # paramDelimiter) THEN
            tooLong := TRUE;
            (* a parameter is too long to fit in par, let's just skip it *)
            REPEAT
               INC(i)
            UNTIL ~((i < paramsLen) & (params[i] # paramDelimiter));
            pInd := 0
         ELSE
            tooLong := FALSE
         END;
         p[pInd] := 0X;
         IF i >= paramsLen THEN
            read := pInd > 0
         ELSIF params[i] = paramDelimiter THEN
            INC(i); (* skip the delimiter on next reading *)
            read := TRUE
         ELSE
            read := FALSE
         END;
         IF read THEN
            INC(badParamIndex)
         END
      END NextPar;

   BEGIN
      i := 0;
      badParamIndex := 0;
      device := 'COM1';
      baud := 9600;
      opts := {};
      paramsLen := LEN(params$);
      NEW(par);
      NextPar(par, read, tooLong);
      IF read THEN
         IF tooLong THEN RETURN badParamIndex END;
         IF par^ # '' THEN device := par$ END
      END;
      NextPar(par, read, tooLong);
      WHILE read DO
         IF tooLong THEN RETURN badParamIndex END;
         IF par$ = ''   THEN (* ignore empty parameters *)
         ELSIF par$ = '1'   THEN
         ELSIF par$ = '1.5'   THEN INCL(opts, stop15)
         ELSIF par$ = '2'   THEN INCL(opts, stop2)
         ELSIF par$ = '4'   THEN INCL(opts, bits4)
         ELSIF par$ = '5'   THEN INCL(opts, bits5)
         ELSIF par$ = '6'   THEN INCL(opts, bits6)
         ELSIF par$ = '7'   THEN INCL(opts, bits7)
         ELSIF par$ = '8'   THEN
         ELSIF par$ = 'even'   THEN INCL(opts, even)
         ELSIF par$ = 'odd'   THEN INCL(opts, odd)
         ELSIF par$ = 'DTR'   THEN INCL(opts, inDTR)
         ELSIF par$ = 'DTRon'   THEN INCL(opts, inDTRon)
         ELSIF par$ = 'DTRoff'   THEN
         ELSIF par$ = 'RTS'   THEN INCL(opts, inRTS)
         ELSIF par$ = 'RTSon'   THEN INCL(opts, inRTSon)
         ELSIF par$ = 'RTSoff'   THEN
         ELSIF par$ = 'CTS'   THEN INCL(opts, outCTS)
         ELSIF par$ = 'DSR'   THEN INCL(opts, outDSR)
         ELSIF par$ = 'inXonXoff'   THEN INCL(opts, inXonXoff)
         ELSIF par$ = 'outXonXoff'   THEN INCL(opts, outXonXoff)
         ELSE (* otherwise it should be a baud rate *)
            Strings.StringToInt(par, baud, res);
            IF (res # 0) OR (baud <= 8) THEN
               RETURN badParamIndex (* unknown parameter or invalid baud rate *)
            END
         END;
         NextPar(par, read, tooLong)
      END;
      RETURN 0
   END ParseParams;

   PROCEDURE NewStream* (localAdr, remoteAdr: ARRAY OF CHAR; OUT s: CommStreams.Stream; OUT res: INTEGER);
      VAR
         str: Stream;
         device: ARRAY 256 OF CHAR;
         baud: INTEGER;
         opts: SET;
   BEGIN
      NEW(str);
      NEW(str.remoteAdr, LEN(remoteAdr$) + 1);
      str.remoteAdr^ := remoteAdr$;
      IF ParseParams(str.remoteAdr, device, baud, opts) = 0 THEN
         Open(device, baud, opts, str.conn);
         IF str.conn # NIL THEN
            (* Here we calculate the maximum bytes written in a single call to WriteBytes. This is the granularity of blocking, since WriteBytes actually blocks. It just won't try to send more than s.maxWrite bytes at once. Although several calls to WriteBytes will block for a longer period of time.
            The value here is the baud DIV 1000. In the default configuration (9600 baud, 8 data bits, 1 stop bit, no parity check) this equals 9 bytes which it takes less than 10 msec to send. If you want to block for longer, you may call WriteBytes twice in a succession, which would give you a sum of 20 msec, and so on. If baud = 19200, you would send 19 bytes for the same 10 msec. *)
            str.maxWrite := baud DIV 1000;
            IF str.maxWrite < 1 THEN str.maxWrite := 1 END;
            s := str;
            res := CommStreams.done
         ELSE
            s := NIL;
            res := CommStreams.remoteAdrInUse
         END
      ELSE
         res := CommStreams.invalidRemoteAdr
      END
   END NewStream;


   PROCEDURE Init;
   BEGIN
      root := NIL
   END Init;
   
   PROCEDURE CloseAll;
   BEGIN
      WHILE root # NIL DO
         Close(root.conn)
      END
   END CloseAll;

BEGIN
   Init
CLOSE
   CloseAll
END CommV24.
```
