Потеря данных TComPort при попытке прочитать данные ASTM 1391 с использованием TComDataPacket

Я использую TComPort а также TComDataPacket общаться с различными медицинскими инструментами в одном из моих приложений. У меня есть несколько строк кода в TComDataPacket.OnCustomStart а также TComDataPacket.OnCustomEnd отметить начало и конец пакета данных в зависимости от типа прибора. Для простых пакетов, которые имеют фиксированную пару начальных и конечных символов (например, STX/ETX) все отлично работает.

Я попытался добавить поддержку протокола ASTM 1391, используя тот же метод. Пакеты ASTM 1391 состоят из ENQодин или несколько пакетов, начинающихся с STX и заканчивая CR LF, и один EOT отметить конец передачи данных. И в ответ на ENQ а также CR LF, а также ACK должны быть отправлены обратно. Очень простая схема диалога между прибором и компьютером будет выглядеть так:

  • INST: ENQ
  • ВЕДУЩИЙ: ACK
  • INST: STX..............CR LF
  • ВЕДУЩИЙ: ACK
  • INST: STX...................CR LF
  • ВЕДУЩИЙ: ACK
  • INST: STX....................................CR LF
  • ВЕДУЩИЙ: ACK
  • INST: STX..............CR LF
  • ВЕДУЩИЙ: ACK
  • INST: EOT

Вот код в моем OnCustomStart, OnCustomEnd, а также OnPacket События:

procedure TdmInstrument.cdpPacketCustomStart(Sender: TObject; const Str: string;
  var Pos: Integer);
begin
  if not FInitInfo.IsASTM then // simple packet structure
    Pos := System.Pos(FInitInfo.StartChar, Str)
  else
  begin
    Sleep(500); // no idea why this is required
    Application.ProcessMessages;
    Pos := System.Pos(cENQ, Str);
    if Pos = 0 then
    begin
      Pos := System.Pos(cSTX, Str);
      if Pos = 0 then
        Pos := System.Pos(cEOT, Str);
    end
    else
      ASTMStr := '';
  end;
end;

procedure TdmInstrument.cdpPacketCustomStop(Sender: TObject; const Str: string;
  var Pos: Integer);
begin
  if not FInitInfo.IsASTM then
    Pos := System.Pos(FInitInfo.EndChar, Str)
  else
  begin
    Pos := System.Pos(cENQ, Str);
    if Pos = 0 then
    begin
      Pos := System.Pos(cCR + cLF, Str) + 1;
      if Pos = 0 then
        Pos := System.Pos(cEOT, Str);
    end;
  end;
end;

procedure TdmInstrument.cdpPacketPacket(Sender: TObject; const Str: string);
var
  i: Integer;
begin
  if not FInitInfo.IsASTM then
  begin
    RawRecord := '';
    for i := 1 to Length(Str) do
      if Str[i] <> #0 then
        RawRecord := RawRecord + Str[i]
      else
        RawRecord := RawRecord + ' ';
  end else begin
    ASTMStr := ASTMStr + Str;
    if Str <> cEOT then
      cpCom.WriteStr(cACK);
    if Pos(cENQ, ASTMStr) * Pos(cEOT, ASTMStr) = 0 then // ASTM packet is not yet complete - exit
      Exit;
    RawRecord := ASTMStr;
  end;

  // we have a packet, so parse it
  ParsePacket;
end;

Моя проблема, если я не позвоню Sleep() со значением более 500 в OnCustomStart, в OnPacket, Str установлен в STX только. Так как у меня была эта проблема на более чем нескольких разных компьютерах и разных инструментах, и даже на моей тестовой машине с виртуальным последовательным портом с обратной связью, я думаю, это как-то связано с внутренней структурой TComPort или же TComDataPacket, Кто-нибудь может указать мне правильное направление?

3 ответа

Решение

У вас есть опечатка в вашем коде.

procedure TdmInstrument.cdpPacketCustomStop ...

begin
  ....

      Pos := System.Pos(cCR + cLF, Str) + 1;
      if Pos = 0 then
        Pos := System.Pos(cEOT, Str);
  ....
end;

if Pos = 0 then, Pos никогда не может быть 0

Вы не должны использовать Pos как ваша переменная. И использовать его для конкуренции с System.Pos.

Некоторая оптимизация кода

procedure TdmInstrument.cdpPacketPacket(Sender: TObject; const Str: string);

begin
  if not FInitInfo.IsASTM then
  begin
    RawRecord := '';
    if Pos(#0, Str) > 0 then Str:=Stringreplace(Str,#0,' ',[]);
    RawRecord := RawRecord + Str;
  end else begin
    ASTMStr := ASTMStr + Str;
    if (Pos(cENQ, ASTMStr) + Pos(cEOT, ASTMStr) + Pos(cCR + cLF,Str) = 0)  then 
      Exit; // ASTM packet is not yet complete - exit
            // Do Not exit if there is a `cCR + cLF`
    if Pos(cEOT, Str) = 0 then cpCom.WriteStr(cACK);
            // write only when one of  `cENQ , cCR + cLF` is present
    RawRecord := ASTMStr;
  end;

 // we have a packet, so parse it
  ParsePacket;
  end;

Здесь есть несколько проблем.

Во-первых, ваш пользовательский обработчик пакетов данных возвращается. Это плохо. призвание Application.ProcessMessages Внутри обработчик пакетов выйдет из обработчика и начнет выполнять функцию с самого начала, если новый пакет данных будет получен в то же время, только для продолжения с того места, на котором он остановился после полной обработки последующего пакета (если только другой пакет приходит в течение этого времени, и в этом случае он будет перезапущен). Это не то поведение, которое вам нужно, поскольку оно приведет к неправильной обработке пакетов и, вероятно, связано с вашей потребностью в sleep,

Что я могу предложить, так это настроить отдельные пакеты данных для каждой команды, отправляемой с прибора, т.е.

  • Пакет данных 1: Начальная строка ENQКонец строки #13#10 {CRLF}
  • Пакет данных 2: Начальная строка STXКонец строки #13#10 {CRLF}
  • Пакет данных 3: Начальная строка EOTКонец строки #13#10 {CRLF}

Вам понадобятся переменные класса, запись, объект и т. Д., Чтобы отслеживать ход выполнения пакета. Примерно так, например:

TASTMPkt = record
  Started : boolean;
  Complete : boolean;
  Data : TStringList;
end;

в ENQ обработчик пакетов вы бы сделали что-то вроде:

if FASTMPkt.Data = nil then FASTMPkt.Data := TStringList.Create();
FASTMData.Clear();
FASTMPkt.Started := true;
FASTMPkt.Complete := false;
cpCom.WriteStr(cACK);

в STX обработчик:

if (not FASTMPkt.Started) or (FASTMPkt.Complete) then begin 
  // Raise exception, etc
end else begin
  FASTMPkt.Data.Add(Str);
  cpCom.WriteStr(cACK);
end;

в EOT обработчик:

if (not FASTMPkt.Started) or (FASTMPkt.Complete) then begin 
  // Raise exception, etc
end else begin
  cpCom.WriteStr(cACK);
  FASTMPacket.Complete := true;
  ProcessPacket;
end;

куда ProcessPacket может затем работать с данными списка строк и делать что угодно. Это позволяет избежать засорения потока пользовательского интерфейса в ожидании возможных входящих пакетов, позволяет использовать таймеры для проверки FASTMPkt завершение своевременно (вы можете запустить таймер таймаута в ENQ обработчик пакетов, сбросьте его в STX обработчик, и остановите его в EOT обработчик, например). Это также позволяет избежать снов и ProcessMessages, и, как правило, дает вам способы обработки ошибок и обеспечения надлежащего процесса.

Кроме того, я никогда не использовал TComPort или TDataPacket, но я настоятельно рекомендую AsyncPro (TApdComPort, TApdDataPacket и т. Д.) - это отличные компоненты, которые можно настроить как визуальные или невизуальные компоненты, и я считаю, что они очень компетентны и надежный. Мой ответ здесь предполагает, что эти два компонента работают в целом одинаково (что, я думаю, они делают).

http://sourceforge.net/projects/tpapro/

На мой взгляд, создание успешного обмена данными с использованием TComPort (или любой последовательной библиотеки) - самое сложное. TComPort дает вам хорошие подпрограммы, но нет хорошего примера простого "Отправь и жди ответа", и почти все последовательные соединения, которые я сделал с Delphi, нуждаются в некотором "ожидании завершения условия". Раньше я использовал AsyncPro, и хотя он все еще доступен, у него также нет четких примеров того, как настроить двусторонний последовательный порт с отправкой и ответом. Таким образом, у вас возникает соблазн создать что-то, что использует Application.ProcessMessages для "выборки" символов ответа, и, как указал J...., это вызывает другие проблемы.

Чтобы решить эту проблему, я сделал собственное дополнение к TComPort следующим образом. Это, вероятно, не оптимально, но работает с рядом последовательных приборов со многими различными протоколами.

Сначала настройте TComPort следующим образом - ключевой бит - это FStopEvent...

constructor TArtTComPort.Create( const APort : string);
begin
  inherited Create;

  FTimeoutMS := 3000;

  FComPort := TComPort.Create( nil );

  FComPort.Events := [];  // do not create monitoring thread

  FComPort.Port := APort;

  FComPortParametersStr := sDefaultSerialPortParameters;

  FFlowControl := sfcNone;

  // Prepare a stop event for killing a waiting communication wait.
  FStopEvent := TEvent.Create(
    nil, //ManualReset
    false, //InitialState
    false,
    'StopEvent' );

end;

Для отправки символов вы просто вызываете FComPort.WriteStr.

Пока вы ждете ответа, я использую следующее. Это позволяет мне указать, что такое завершающий символ, и игнорировать (или обрабатывать) завершающие символы после. В случае успеха он просто возвращает ответ. Он не вызывает Application.ProcessMessages, поэтому нет проблем с арендной платой и учитывает тайм-аут.

function TArtTComPort.SerialPort_AwaitChars(AMinLength: integer;
  ATerminator: char; AQtyAfterTerm: integer; ARaise: boolean): string;
var
  fDueBy : TDateTime;

  function IsEndOfReplyOrTimeout( var AStr : string ) : boolean;
  var
   I : integer;
  begin
    Result := False;
    If ATerminator <> #0 then
      begin
      I := Length( AStr ) - AQtyAfterTerm;
      If I > 0 then
        Result := AStr[I] = ATerminator;
      end;
    If not Result then
      Result := Length(AStr) >= AMinLength;


    // Un-comment this next line to disable the timeout.
    //Exit;

    If not Result then
      begin
      Result := Now > fDueBy;
      If Result then
        If ARaise then
          raise EArtTComPort.Create( 'Serial port reply timeout' )
        else
          AStr := '';
      end;
  end;

var
  Events : TComEvents;
  iCount : integer;
  S : string;
begin
  Assert( AMinLength > 0, 'Invalid minimum length' );

  If not FComPort.Connected then
    begin
    Result := '';
    Exit;
    end;

  fDueBy := Now + (FTimeoutMS * TDMSec );

  Result := '';

  Repeat

    // Setup events to wait for:
    Events := [evRxChar, evTxEmpty, evRxFlag, evRing, evBreak,
             evCTS, evDSR, evError, evRLSD, evRx80Full];

    // Wait until at least one event happens.
    FComPort.WaitForEvent(
      Events,
      FStopEvent.Handle,
      FTimeOutMS);

    If Events = [] then // timeout
      begin
      If ARaise then
        raise EArtTComPort.Create( 'Serial port reply timeout' )
      end
     else
      begin
      If evRxChar in Events then
        begin
        iCount := FComport.InputCount;
        FComPort.ReadStr( S, iCount );
        Result := Result + S;
        end;
      end;

  until IsEndOfReplyOrTimeout( Result );


end;

Обратите внимание, что этот код имеет некоторые другие небольшие зависимости, которые не показаны, но он должен дать вам хорошее начало. Если кто-то может показать мне, как этого можно достичь с помощью кода TComPort, я буду благодарен.

Другие вопросы по тегам