Потеря данных 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 и т. Д.) - это отличные компоненты, которые можно настроить как визуальные или невизуальные компоненты, и я считаю, что они очень компетентны и надежный. Мой ответ здесь предполагает, что эти два компонента работают в целом одинаково (что, я думаю, они делают).
На мой взгляд, создание успешного обмена данными с использованием 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, я буду благодарен.