Поскольку приведенному выше CommActions.Action совершенно все равно, с какими коммуникационными потоками работать, то на основе этого модуля был мной реализован простенький FTP-клиент, программа чата по TCP/IP и связь с микропроцессорной сетью по COM-порту в рамках подсистемы Mate. Во всех трех случаях я наследовал только CommAction.Engine, реализуя метод Do, после чего составлял нужную мне объектную композицию. Приведу ниже пример из подсистемы Mate.

## Код:
```
MODULE MateEngine;
(** Copyright (c) Alexander Iljin, 2006. **)

   IMPORT
      Log,
      CommAction, CommBuffer,
      MateBytes;

   CONST
      crcSize = 1; (* Число байт в контрольной сумме. *)
      headSize = 4; (* Число байт в заголовке пакета = 4: node, device, param, type. Этот заголовок, по сути, есть предположение о протоколе более высокого уровня. Это предположение помогает нам сэкономить память в процессе работы и накладывает ограничение на реализацию протокола верхнего уровня (если тот попытается создать слишком большой пакет, то наткнется на ошибку времени выполнения). *)
      maxPacketLength* = headSize + 127 + crcSize; (** Максимальная длина раскодированного пакета, в байтах: 4 байта заголовок, до 127 байт данных и контрольная сумма. *)
      maxNumPackets = 10; (* Максимальное число пакетов, которое может быть вычерпано из буфера и обработано за один вызов HexEngine.Do. Ограничение помогает наладить кооперативную многозадачность. *)
      startPacketChar = ':';
      endPacketChar = 0DX;
      minHexPacketLength = 1 + 2 * (1 + crcSize) + 1; (* Минимальный размер hex-пакета в байтах (символах) согласно протоколу. Такой пакет состоит из одного байта данных и контрольной суммы. Данная константа вводится для отбраковывания пакетов на этапе приема, поэтому никаких предположений о минимальной длине пакета для протокола верхнего уровня мы здесь не делаем. *)

   TYPE
      Packet* = ARRAY maxPacketLength OF BYTE;
      
      (*
      Задача данного движка - получение и отправка пакетов в hex-формате. Формат таков:
      ":"<hh>...<hh>0DX
      Начинается пакет с символа двоеточия, заканчивается символом 0DX (либо любым другим символом с кодом из диапазона от 0X до 0DX включительно). Между этими символами заключен пакет, состоящий из символов '0'..'9' и 'A'..'F', то есть, шестнадцатиричные цифры. Число цифр должно быть четным, в противном случае пакет считается испорченным и игнорируется. Игнорируются также все пакеты, внутри которых встретились какие-либо символы помимо шестнадцатиричных цифр.
      После успешного приема пакета проверяется контрольная сумма. Суть проверки заключается в том, что составляющие пакет байты суммируются как беззнаковые целые числа без контроля переполнения. Старшая часть результата отбрасывается, анализируется только младший байт. Если он равен нулю, то пакет принят успешно, в противном случае пакет игнорируется.
       *)

      HexEngine* = POINTER TO RECORD (CommAction.Engine)
         rider: CommBuffer.Rider; (* собственный райдер для чтения буфера приема *)
         rxPacket: Packet; (* Копия принятого пакета. Ссылка на этот пакет передается в Receive. *)
         rxPacketSize: INTEGER; (* Длина принятого пакета. *)
         crcErrors-, badPackets-, goodPackets-: INTEGER; (* статистика работы *)
         hook-: Hook
      END;

      (** Хук - это способ прикрепиться к движку, т.е. отправлять и получать пакеты. Хук следует унаследовать в клиентском коде, реализовав при этом процедуру Receive. Прикрепляется к движку хук с помощью вызова HexEngine.SetHook(Hook). **)

      Hook* = POINTER TO ABSTRACT RECORD
         engine: HexEngine
      END;



   (* Hook *)

   (** Эту фунцию вызывает движок, передавая в нее новые полученные данные. Наследник должен реализовать здесь обработку принятого пакета. **)
   PROCEDURE (h: Hook) Receive* (VAR p: Packet; size: INTEGER), NEW, ABSTRACT;

   (** Эта функция позволяет отправлять данные через хук. **)
   PROCEDURE (h: Hook) Send* (IN p: Packet; size: INTEGER), NEW;
      VAR 
         coded: ARRAY 1 + 2 * (maxPacketLength + crcSize) + 1 OF BYTE; (* примечание: crcSize здесь намеренно учтен дважды. Первый раз в составе maxPacketLength, и второй раз явно. *)
         crc, i, pos: INTEGER;

      PROCEDURE AppendX (x: INTEGER);
      (* Добавляем младший байт переменной x в конец массива coded. Конец массива обозначается индексом i. *)
      BEGIN
         IF x < 0 THEN INC(x, 256) END;
         INC(crc, x);
         coded[i] := MateBytes.Byte2Hex(x DIV 16);
         INC(i);
         coded[i] := MateBytes.Byte2Hex(x MOD 16);
         INC(i)
      END AppendX;

   BEGIN
      ASSERT(h.engine # NIL, 20);
      ASSERT(size > 0, 21);
      ASSERT(size <= maxPacketLength + crcSize, 22);
      i := 0;
      coded[i] := SHORT(ORD(startPacketChar));
      INC(i);
      crc := 0;
      pos := 0;
      WHILE pos < size DO
         AppendX(p[pos]);
         INC(pos)
      END;
      AppendX(-SHORT(SHORT(crc)));
      ASSERT(SHORT(SHORT(crc)) = 0, 60);
      coded[i] := SHORT(ORD(endPacketChar));
      INC(i);
      h.engine.sendBuf.Append(coded, 0, i)
   END Send;


   (* HexEngine *)

   PROCEDURE (e: HexEngine) PacketReceived (), NEW;
   BEGIN
      ASSERT(e.hook # NIL, 100);
      INC(e.goodPackets);
      e.hook.Receive(e.rxPacket, e.rxPacketSize)
   END PacketReceived;


   PROCEDURE (e: HexEngine) ReadRxPacket (VAR r: CommBuffer.Rider; size: INTEGER): BOOLEAN, NEW;
   (* Прочитаем из указанного райдера пакет длиной size, раскодируем, проверим контрольную сумму и положим в e.rxPacket. Если контрольная сумма верна, возвращаем TRUE. *)
      VAR
         crc: INTEGER; (* контрольная сумма *)
         x: INTEGER; (* очередной байт пакета *)

   BEGIN
      ASSERT(size >= minHexPacketLength, 20);
      ASSERT(size <= 1+2*(maxPacketLength+1)+1, 21);
      ASSERT(~ODD(size), 22);
      ASSERT(r.Next() = startPacketChar, 23); (* пропустим первый символ *)
      DEC(size);
      crc := 0;
      e.rxPacketSize := 0;
      WHILE size > 2 + 1 DO
         x := ASH(MateBytes.Hex2Byte(r.Next()), 4) + MateBytes.Hex2Byte(r.Next());
         DEC(size, 2);
         INC(crc, x);
         e.rxPacket[e.rxPacketSize] := SHORT(SHORT(x));
         INC(e.rxPacketSize)
      END;
      x := ASH(MateBytes.Hex2Byte(r.Next()), 4) + MateBytes.Hex2Byte(r.Next());
      DEC(size, 2);
      INC(crc, x);
      ASSERT(size = 1, 60); (* один завершающий байт должен остаться *)
      ASSERT(~r.Eof(), 61);
      ASSERT(r.Next() <= endPacketChar, 62);
      IF SHORT(SHORT(crc)) = 0 THEN
         RETURN TRUE
      ELSE
         INC(e.crcErrors);
         RETURN FALSE
      END
   END ReadRxPacket;


   PROCEDURE (e: HexEngine) Do- ();
      VAR
         i, numPackets: INTEGER; (* сколько пакетов обработано *)
         endOfPacket, invalidCharacter: BOOLEAN;
         ch: SHORTCHAR;
   BEGIN
      (* Принцип обработки поступающих данных таков. Уже есть буфер, в котором данные хранятся. Это значит, что нам не нужно делать локальную копию той части пакета, которая была получена. Мы можем последовательно читать буфер. Данные в буфере содержатся в том порядке, в котором они были получены. Мы можем удалять данные из начала буфера. Это значит, что при вхождении в данную процедуру в начале буфера находятся данные, которые нужно обработать. Мы удаляем данные из буфера только тогда, когда обнаружим, что в буфере находится пакет данных целиком. Это значит, что при каждом вхождении в данный обработчик мы проверяем, что у нас в начале буфера. Если там признак начала пакета, значит это либо новые данные, либо часть пакета, оставшаяся с прошлой обработки, когда пакет был принят не до конца. Если в начале буфера нет признака начала пакета, значит это какой-то мусор, не укладывающийся в протокол. Такой мусор нужно игнорировать. Само появление мусора не является фатальной ошибкой. Например, мы могли подключить программу в тот момент, когда кто-то передавал данные из середины пакета. *)
      numPackets := 0;
      e.rider := e.recvBuf.NewRider(e.rider);
      WHILE ~e.rider.Eof() & (numPackets < maxNumPackets) DO
         (* удалим мусор до символа начала пакета *)
         i := 0;
         WHILE ~e.rider.Eof() & (e.rider.Next() # startPacketChar) DO
            INC(i)
         END;
         e.recvBuf.Delete(i); (* Удаляем всё до символа начала пакета. Сам символ не трогаем: он нам понадобится на случай повторной обработки этого же пакета, если в текущем сеансе обработки пакет получен не полностью. *)

         (* Осталось ли что-то еще в буфере? Если да, значит предыдущий цикл завершил работу, прочитав признак начала пакета, и все данные до начала пакета удалены. Теперь нужно прочитать остаток пакета в поисках признака конца, заодно подсчитаем число байт в пакете. *)
         IF ~e.rider.Eof() THEN
            (* проверим, получен ли пакет целиком, и сколько в нем символов *)
            i := 1; (* один символ есть - символ начала пакета, посмотрим, что там дальше *)
            invalidCharacter := FALSE;
            REPEAT
               ch := e.rider.Next();
               INC(i);
               endOfPacket := (ch >= 0X) & (ch <= 0DX);
               IF ~endOfPacket THEN
                  invalidCharacter := ~( ((ch >= '0') & (ch <= '9')) OR ((ch >= 'A') & (ch <= 'F')) )
               END
            UNTIL endOfPacket OR invalidCharacter OR e.rider.Eof();
            IF invalidCharacter THEN
               (* пакет выброшен, потому что в нем содержались непристойные символы *)
               INC(e.badPackets);
               e.recvBuf.Delete(i)
            ELSIF i > (1 + 2*(maxPacketLength) + 1) THEN
               (* пакет выброшен, так как превышает максимально допустимую длину *)
               INC(e.badPackets);
               e.recvBuf.Delete(i)
            ELSIF endOfPacket THEN
               IF i < minHexPacketLength THEN
                  (* пакет выброшен, так как слишком короток, чтобы содержать данные *)
                  INC(e.badPackets)
               ELSIF ODD(i) THEN
                  (* пакет выброшен, так как число байт в нем должно быть четным *)
                  INC(e.badPackets)
               ELSE
                  (* пакет получен целиком, скопируем его и удалим из буфера *)
                  INC(numPackets);
                  e.rider := e.recvBuf.NewRider(e.rider);
                  IF e.ReadRxPacket(e.rider, i) THEN
                     e.PacketReceived()
                  END
               END;
               e.recvBuf.Delete(i)
            END
         END
      END;
      (* Прекращаем работу в том случае, если поток уже закрыт и в текущем сеансе обработки буфера приема мы не достигли максимального числа пакетов. Это значит, что если в буфере и остаются какие-то остатки данных, но они не тянут еще на один цельный пакет. *)
      IF e.streamClosed & (numPackets < maxNumPackets) THEN
         e.finished := TRUE
      END
   END Do;


   (* Miscellaneous *)
   
   PROCEDURE (e: HexEngine) SetHook* (h: Hook), NEW;
   BEGIN
      e.hook := h;
      IF e.hook # NIL THEN
         e.hook.engine := e
      END
   END SetHook;

END MateEngine.
```
