{"id":104,"date":"2026-09-16T17:09:21","date_gmt":"2026-09-16T20:09:21","guid":{"rendered":"http:\/\/www.pascalscada.com\/?page_id=104"},"modified":"2026-09-16T19:05:59","modified_gmt":"2026-09-16T22:05:59","slug":"writing-a-protocol-driver","status":"publish","type":"page","link":"https:\/\/www.pascalscada.com\/pb\/writing-a-protocol-driver\/","title":{"rendered":"Escrevendo um driver de protocolo"},"content":{"rendered":"<p><\/p>\n\n<h5>Conhecimentos necess\u00e1rios<\/h5>\n<ul>\n<li>Comunica\u00e7\u00e3o de dados;<\/li>\n<li>Conhecer o meio f\u00edsico que seu protocolo usar\u00e1 (serial, TCP\/IP, UDP);<\/li>\n<li>Cria\u00e7\u00e3o de componentes em Delphi\/Lazarus;<\/li>\n<li>Conhecimento do protocolo sendo implementado \u2014 com a especifica\u00e7\u00e3o em m\u00e3os, ou pelo menos capturas de tr\u00e1fego (Wireshark) de um equipamento real.<\/li>\n<\/ul>\n<h5>Defini\u00e7\u00f5es<\/h5>\n<ul>\n<li><strong>Scan de atualiza\u00e7\u00e3o<\/strong>: m\u00e9todo c\u00edclico que atualiza o valor do tag com o \u00faltimo valor lido pelo driver de protocolo. Roda em uma thread pr\u00f3pria do driver e entrega os valores aos tags na thread principal.<\/li>\n<li><strong>Scan de leitura<\/strong>: m\u00e9todo c\u00edclico que varre as \u00e1reas de mem\u00f3ria que mais precisam de atualiza\u00e7\u00e3o, codifica o pacote de dados a ser enviado ao seu equipamento, faz o envio atrav\u00e9s da porta de comunica\u00e7\u00e3o, espera a resposta da porta, recebe os dados, decodifica os dados recebidos e atualiza as \u00e1reas de dados internas com o valor e a data\/hora da \u00faltima leitura.<\/li>\n<li><strong>Scan de escrita<\/strong>: m\u00e9todo c\u00edclico que atende as ordens de escrita enfileiradas pelos tags (<code>ScanWrite<\/code>, ou a atribui\u00e7\u00e3o de <code>Value<\/code> com <code>AutoWrite = True<\/code>), executando-as de maneira ass\u00edncrona \u2014 ou seja, a aplica\u00e7\u00e3o continua enquanto a escrita vai para o equipamento, e o resultado chega depois pelos eventos do tag.<\/li>\n<li><strong>Leitura s\u00edncrona<\/strong>: leitura onde o scan de leitura \u00e9 paralisado para liberar a porta de comunica\u00e7\u00e3o e a aplica\u00e7\u00e3o faz a leitura do tag diretamente (<code>Tag.Read<\/code>), bloqueando at\u00e9 a resposta chegar.<\/li>\n<li><strong>Escrita s\u00edncrona<\/strong>: escrita onde o scan de leitura \u00e9 paralisado da mesma forma e a aplica\u00e7\u00e3o escreve diretamente (<code>Tag.Write<\/code>), bloqueando at\u00e9 o equipamento confirmar.<\/li>\n<\/ul>\n<h5>Introdu\u00e7\u00e3o<\/h5>\n<p>Antes de sair escrevendo c\u00f3digos como um louco, vou explicar o conceito de como um driver de protocolo funciona no PascalSCADA. Um driver de protocolo \u00e9 o objeto que gerencia os tags dentro de sua aplica\u00e7\u00e3o. Ele atualiza os tags de acordo com a sua taxa de atualiza\u00e7\u00e3o, organiza blocos e evita a duplica\u00e7\u00e3o de pedidos de leitura dessas mem\u00f3rias. Sua organiza\u00e7\u00e3o interna pode variar de acordo com o protocolo, que pode oferecer leituras em bloco ou individualizadas por mem\u00f3ria. Tudo isso feito em nome da performance.<\/p>\n<p>Os tags no PascalSCADA s\u00e3o <strong>c\u00f3pias dos valores de uma outra \u00e1rea de mem\u00f3ria gerenciada pelo driver<\/strong>. \u00c9 atrav\u00e9s desta \u00e1rea interna que o driver gerencia seu scan de leitura. A organiza\u00e7\u00e3o dessa \u00e1rea de mem\u00f3ria varia de acordo com a organiza\u00e7\u00e3o interna do equipamento. Os tags, por sua vez, podem representar um ou mais endere\u00e7os de mem\u00f3ria do seu equipamento.<\/p>\n<p>O que a classe base <code>TProtocolDriver<\/code> j\u00e1 faz por voc\u00ea:<\/p>\n<pre><code>                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 TProtocolDriver \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n tags \u2500\u2500AddTag\u2500\u2500&#x25b6; \u2502 lista de tags                                                   \u2502\n                  \u2502                                                                 \u2502\n                  \u2502 TScanThread  \u2500\u2500&#x25b6; DoScanRead \u2500\u2500&#x25b6; DoRead \u2500\u2500&#x25b6; porta \u2500\u2500&#x25b6; equipamento \u2502\n                  \u2502                       (seu c\u00f3digo: monta e decodifica frames)   \u2502\n                  \u2502                                                                 \u2502\n                  \u2502 TScanUpdate  \u2500\u2500&#x25b6; DoGetValue \u2500\u2500&#x25b6; Synchronize \u2500\u2500&#x25b6; Tag.Value       \u2502\n                  \u2502                       (seu c\u00f3digo: copia da \u00e1rea interna)       \u2502\n                  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/pre>\n<ul>\n<li>cria e mant\u00e9m as duas threads (<code>TScanThread<\/code>, que chama <code>DoScanRead<\/code> em loop, e <code>TScanUpdate<\/code>, que chama <code>DoGetValue<\/code> para cada tag e entrega os valores na thread principal);<\/li>\n<li>implementa <code>Read<\/code>\/<code>Write<\/code>\/<code>ScanRead<\/code>\/<code>ScanWrite<\/code> chamados pelos tags, pausando o scan e chamando o seu <code>DoRead<\/code>\/<code>DoWrite<\/code>;<\/li>\n<li>cuida da sincroniza\u00e7\u00e3o entre as threads (se\u00e7\u00f5es cr\u00edticas, pausa do scan) \u2014 voc\u00ea nunca precisa de um mutex nos m\u00e9todos <code>Do*<\/code>;<\/li>\n<li>trata a propriedade <code>CommunicationPort<\/code>, o <code>ReadOnly<\/code>, os contadores e o <code>LiteralTagAddress<\/code> do Object Inspector.<\/li>\n<\/ul>\n<p>O que fica para voc\u00ea: <strong>sete m\u00e9todos<\/strong> que descrevem o seu equipamento e o seu protocolo. \u00c9 o que o resto desta p\u00e1gina mostra.<\/p>\n<p>Para facilitar o entendimento, vou basear este texto num <strong>CLP fict\u00edcio<\/strong>, que tem uma \u00e1rea de registradores de 16 bits, entradas e sa\u00eddas digitais endere\u00e7adas em byte (8 bits), com todas as \u00e1reas suportando leitura\/escrita em bloco (atualizar v\u00e1rios tags em uma \u00fanica requisi\u00e7\u00e3o), com endere\u00e7amento simples do CLP de 1 a 255 (semelhante ao Modbus).<\/p>\n<h5>Passo 1: a unit e a estrutura do equipamento<\/h5>\n<p>O primeiro passo \u00e9 criar uma unit para abrigar seu novo protocolo e adicion\u00e1-la ao pacote do PascalSCADA (veja <em>Registrando o driver<\/em>, mais abaixo). Nesta unit, a representa\u00e7\u00e3o do nosso CLP fict\u00edcio seria semelhante \u00e0 estrutura abaixo:<\/p>\n<pre><code class=\"language-pascal\">uses ProtocolDriver, ProtocolTypes, PLCMemoryManager, Tag, PLCBlock, commtypes;\n\ntype\n  TDummyPLC = record\n    PLCAddress: Byte;\n    Inputs:    TPLCMemoryManager;  \/\/ representada por MemReadFunction = 1 nos tags\n    Outputs:   TPLCMemoryManager;  \/\/ representada por MemReadFunction = 2 nos tags\n    Registers: TPLCMemoryManager;  \/\/ representada por MemReadFunction = 3 nos tags\n  end;\n  TDummyPLCs = array of TDummyPLC;\n<\/code><\/pre>\n<p>Note que foi usada uma classe chamada <code>TPLCMemoryManager<\/code>. O que ela faz? Ela organiza mem\u00f3rias de uma mesma \u00e1rea em blocos cont\u00ednuos do maior tamanho poss\u00edvel, evitando a duplica\u00e7\u00e3o de mem\u00f3rias e otimizando sua comunica\u00e7\u00e3o. Ela tem duas propriedades que controlam todo o seu comportamento:<\/p>\n<ul>\n<li><strong><code>MaxHole<\/code><\/strong>: controla quantas mem\u00f3rias podem faltar para manter um \u00fanico bloco. Digamos que sejam adicionadas as mem\u00f3rias de endere\u00e7os [1, 2, 5, 6]. Com <code>MaxHole = 0<\/code>, ser\u00e3o formados dois blocos, o primeiro com as mem\u00f3rias [1, 2] e o segundo com as mem\u00f3rias [5, 6]. Mas com <code>MaxHole = 2<\/code>, ser\u00e1 formado um \u00fanico bloco com os endere\u00e7os [1, 2, 3, 4, 5, 6]. Note que os endere\u00e7os 3 e 4 s\u00e3o adicionados para manter a continuidade do bloco, fazendo com que este seja lido com somente um pedido pelo driver de protocolo, melhorando a performance.<\/li>\n<li><strong><code>MaxBlockItems<\/code><\/strong>: controla o tamanho m\u00e1ximo dos blocos. Se forem adicionados os endere\u00e7os [1, 2, 3, 4, 5, 6] com <code>MaxBlockItems = 3<\/code>, ser\u00e3o formados dois blocos, o primeiro contendo os endere\u00e7os [1, 2, 3] e o segundo os endere\u00e7os [4, 5, 6]. Esta propriedade \u00e9 \u00fatil em protocolos que limitam o tamanho m\u00e1ximo do pedido, como por exemplo o Modbus (125 registradores por leitura). <code>0<\/code> significa sem limite.<\/li>\n<\/ul>\n<p>Os m\u00e9todos que voc\u00ea vai usar nela:<\/p>\n<table>\n<thead>\n<tr>\n<th>M\u00e9todo<\/th>\n<th>Faz<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>AddAddress(Address, Size, RegSize, ScanTime)<\/code><\/td>\n<td>Registra <code>Size<\/code> mem\u00f3rias a partir de <code>Address<\/code>, com scan de <code>ScanTime<\/code> ms. <code>RegSize<\/code> \u00e9 o tamanho da vari\u00e1vel em rela\u00e7\u00e3o \u00e0 menor palavra da \u00e1rea: para adicionar MW0, MW2 e MW4 de um Siemens (menor palavra = byte) use <code>AddAddress(0, 3, 2, 1000)<\/code>. O bloco resultante tem o <strong>menor<\/strong> <code>ScanTime<\/code> dos tags que o comp\u00f5em.<\/td>\n<\/tr>\n<tr>\n<td><code>RemoveAddress(Address, Size, RegSize)<\/code><\/td>\n<td>O inverso.<\/td>\n<\/tr>\n<tr>\n<td><code>Blocks[i]<\/code><\/td>\n<td>Os blocos cont\u00ednuos formados, cada um com <code>AddressStart<\/code>, <code>Size<\/code>, <code>ScanTime<\/code>, <code>LastUpdate<\/code> e <code>NeedRefresh<\/code> (verdadeiro quando passou o tempo de scan).<\/td>\n<\/tr>\n<tr>\n<td><code>SetValues(Address, Len, RegSize, Values, LastResult)<\/code><\/td>\n<td>Guarda os valores lidos do equipamento e marca a hora.<\/td>\n<\/tr>\n<tr>\n<td><code>SetFault(Address, Len, RegSize, Fault)<\/code><\/td>\n<td>Marca que a leitura falhou \u2014 os tags recebem o <code>TProtocolIOResult<\/code> e disparam <code>OnReadFail<\/code>.<\/td>\n<\/tr>\n<tr>\n<td><code>GetValues(Address, Len, RegSize, Values, LastResult, Timestamp)<\/code><\/td>\n<td>Copia valores, resultado e hora para entregar a um tag.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Note tamb\u00e9m que adicionei um coment\u00e1rio a respeito de cada \u00e1rea de mem\u00f3ria. Eu escolhi a propriedade <code>MemReadFunction<\/code> do tag para escolher a \u00e1rea de dados (entradas, sa\u00eddas e registradores). O valor 1 identifica as entradas digitais, 2 as sa\u00eddas digitais e 3 os registradores. Onde este mapeamento do valor da propriedade \u00e9 ligado com a \u00e1rea de mem\u00f3ria \u00e9 um pouco mais \u00e0 frente.<\/p>\n<p>Feita a estrutura que representa o seu equipamento, o segundo passo \u00e9 criar uma classe herdeira de <code>TProtocolDriver<\/code>:<\/p>\n<pre><code class=\"language-pascal\">type\n  TProtocoloFicticio = class(TProtocolDriver)\n  private\n    FCLPs: TDummyPLCs;\n  protected\n    procedure DoAddTag(TagObj: TTag; TagValid: Boolean); override;\n    procedure DoDelTag(TagObj: TTag); override;\n    procedure DoScanRead(Sender: TObject; var NeedSleep: LongInt); override;\n    procedure DoGetValue(TagRec: TTagRec; var values: TScanReadRec); override;\n    function  DoRead(const tagrec: TTagRec; out Values: TArrayOfDouble; Sync: Boolean): TProtocolIOResult; override;\n    function  DoWrite(const tagrec: TTagRec; const Values: TArrayOfDouble; Sync: Boolean): TProtocolIOResult; override;\n  public\n    destructor Destroy; override;\n    function SizeOfTag(aTag: TTag; isWrite: Boolean; var ProtocolTagType: TProtocolTagType): BYTE; override;\n  published\n    property ReadSomethingAlways;\n  end;\n<\/code><\/pre>\n<h5>Passo 2: adicionando tags ao scan \u2014 DoAddTag<\/h5>\n<p>Neste novo protocolo, voc\u00ea deve dizer como os tags ser\u00e3o adicionados ao scan do driver para serem lidos. E como fa\u00e7o isso? Simples. Sobrescreva o m\u00e9todo:<\/p>\n<pre><code class=\"language-pascal\">procedure DoAddTag(TagObj: TTag; TagValid: Boolean);\n<\/code><\/pre>\n<p>Ele ir\u00e1 adicionar tags \u00e0 \u00e1rea de mem\u00f3ria gerenciada pelo driver. Fa\u00e7a as devidas verifica\u00e7\u00f5es do tag (exemplo: faixa de endere\u00e7os correta, tipo correto) nesse m\u00e9todo e, caso ele seja um tag v\u00e1lido, chame o m\u00e9todo herdado com<\/p>\n<pre><code class=\"language-pascal\">inherited DoAddTag(TagObj, TheTagIsValid);\n<\/code><\/pre>\n<p>para adicionar o tag na classe base para que este seja atualizado de acordo com a sua taxa de scan. O segundo par\u00e2metro \u00e9 importante: a classe base marca o tag como v\u00e1lido ou inv\u00e1lido, e um tag inv\u00e1lido fica na lista mas nunca \u00e9 lido \u2014 \u00e9 o que acontece quando o usu\u00e1rio preenche um <code>MemReadFunction<\/code> que o driver n\u00e3o conhece. Chame o <code>inherited<\/code> <strong>sempre<\/strong>, mesmo com <code>False<\/code>; se n\u00e3o chamar, o tag n\u00e3o \u00e9 registrado e <code>DoDelTag<\/code> n\u00e3o vai encontr\u00e1-lo depois.<\/p>\n<p>Neste exemplo, vou tratar somente tags bloco. Com o nosso CLP fict\u00edcio, o m\u00e9todo ficaria parecido com o c\u00f3digo abaixo:<\/p>\n<pre><code class=\"language-pascal\">procedure TProtocoloFicticio.DoAddTag(TagObj: TTag; TagValid: Boolean);\nvar\n  Valido, clpencontrado: Boolean;\n  clp: Integer;\nbegin\n  Valido := False;\n  clpencontrado := False;\n\n  \/\/ como eu disse, ser\u00e1 tratado somente tags bloco neste exemplo.\n  if TagObj is TPLCBlock then\n    with TagObj as TPLCBlock do begin\n      \/\/ verifica se o endere\u00e7o do CLP est\u00e1 na faixa aceita (entre 1 e 255)\n      \/\/ e, se est\u00e1, procura para ver se ele j\u00e1 n\u00e3o est\u00e1 cadastrado na\n      \/\/ \u00e1rea de mem\u00f3ria gerenciada pelo driver.\n      if PLCStation in [1..255] then\n        for clp := 0 to High(FCLPs) do\n          if FCLPs[clp].PLCAddress = PLCStation then begin\n            clpencontrado := True;\n            Break;\n          end;\n\n      \/\/ se n\u00e3o encontrou o CLP, adiciona ele.\n      if (not clpencontrado) and (PLCStation in [1..255]) then begin\n        clp := Length(FCLPs);\n        SetLength(FCLPs, clp + 1);\n        FCLPs[clp].PLCAddress := PLCStation;\n        FCLPs[clp].Inputs    := TPLCMemoryManager.Create;\n        FCLPs[clp].Outputs   := TPLCMemoryManager.Create;\n        FCLPs[clp].Registers := TPLCMemoryManager.Create;\n        FCLPs[clp].Registers.MaxBlockItems := 125;   \/\/ limite do nosso protocolo\n        clpencontrado := True;\n      end;\n\n      \/\/ verifica se o tag est\u00e1 v\u00e1lido (associado com alguma \u00e1rea)\n      if clpencontrado and (MemReadFunction in [1..3]) then\n        Valido := True;\n\n      \/\/ adiciona o tag \u00e0 \u00e1rea de mem\u00f3ria gerenciada pelo driver de protocolo\n      if Valido then\n        case MemReadFunction of\n          1: FCLPs[clp].Inputs.AddAddress(MemAddress, TagSizeOnProtocol, 1, RefreshTime);\n          2: FCLPs[clp].Outputs.AddAddress(MemAddress, TagSizeOnProtocol, 1, RefreshTime);\n          3: FCLPs[clp].Registers.AddAddress(MemAddress, TagSizeOnProtocol, 1, RefreshTime);\n        end;\n    end;\n\n  \/\/ adiciona o tag \u00e0 classe base\n  inherited DoAddTag(TagObj, Valido);\nend;\n<\/code><\/pre>\n<p><code>TagSizeOnProtocol<\/code> \u00e9 o tamanho do tag <strong>em palavras do protocolo<\/strong> \u2014 um <code>TPLCBlock<\/code> de <code>Size = 10<\/code> com <code>TagType = pttFloat<\/code> sobre registradores de 16 bits ocupa 20 palavras. Ele j\u00e1 vem calculado pelo tag a partir do seu <code>SizeOfTag<\/code> (passo 7), por isso \u00e9 ele, e n\u00e3o <code>Size<\/code>, que vai para o gerenciador.<\/p>\n<p>Pronto, seu tag est\u00e1 validado e adicionado ao scan do seu driver de protocolo. S\u00f3 que este m\u00e9todo ainda n\u00e3o faz a leitura dos valores dos tags sozinho. N\u00e3o, mas poderia \u2014 por\u00e9m seria ineficiente quando se procura performance, pois como a base n\u00e3o conhece a estrutura do driver, a maneira de se fazer isso seria passar tag a tag solicitando-os para o dispositivo e, caso existisse um tag duplicado (mesma \u00e1rea de mem\u00f3ria de um mesmo CLP), ele seria solicitado duas vezes. Para fazer a rotina de scan melhorada \u00e9 necess\u00e1rio que sejam sobrescritos dois m\u00e9todos.<\/p>\n<h5>Passo 3: entregando valores aos tags \u2014 DoGetValue<\/h5>\n<pre><code class=\"language-pascal\">procedure DoGetValue(TagRec: TTagRec; var values: TScanReadRec);\n<\/code><\/pre>\n<p>Lembram que os tags s\u00e3o c\u00f3pias de uma \u00e1rea de mem\u00f3ria gerenciada pelo driver? Pois \u00e9, este \u00e9 o m\u00e9todo que realiza essa c\u00f3pia. Ele \u00e9 chamado pela thread de atualiza\u00e7\u00e3o para cada tag, na taxa do tag. A maneira de implement\u00e1-lo \u00e9 simples: basta procurar a sua mem\u00f3ria na organiza\u00e7\u00e3o interna do protocolo. No nosso driver fict\u00edcio ficaria mais ou menos assim:<\/p>\n<pre><code class=\"language-pascal\">procedure TProtocoloFicticio.DoGetValue(TagRec: TTagRec; var values: TScanReadRec);\nvar\n  clp: Integer;\nbegin\n  \/\/ varre os CLPs e, caso encontre o CLP procurado, solicita ao\n  \/\/ gerenciador de blocos da \u00e1rea desejada os valores do tag.\n  for clp := 0 to High(FCLPs) do\n    if FCLPs[clp].PLCAddress = TagRec.Station then\n      case TagRec.ReadFunction of\n        1: FCLPs[clp].Inputs.GetValues(TagRec.Address, TagRec.Size, 1, values.Values,\n                                       values.LastQueryResult, values.ClkMonotonicTStamp);\n        2: FCLPs[clp].Outputs.GetValues(TagRec.Address, TagRec.Size, 1, values.Values,\n                                        values.LastQueryResult, values.ClkMonotonicTStamp);\n        3: FCLPs[clp].Registers.GetValues(TagRec.Address, TagRec.Size, 1, values.Values,\n                                          values.LastQueryResult, values.ClkMonotonicTStamp);\n      end;\nend;\n<\/code><\/pre>\n<p><code>TTagRec<\/code> \u00e9 a fotografia das propriedades do tag no momento do pedido: <code>Station<\/code>, <code>Address<\/code>, <code>Size<\/code>, <code>ReadFunction<\/code>, <code>WriteFunction<\/code>, <code>File_DB<\/code>, <code>SubElement<\/code>, <code>Rack<\/code>, <code>Slot<\/code>, <code>Path<\/code> (o <code>LongAddress<\/code>), <code>UpdateTime<\/code>, <code>Retries<\/code> e o <code>CallBack<\/code> que devolve o resultado ao tag. \u00c9 com ela \u2014 e n\u00e3o com o objeto do tag \u2014 que <code>DoGetValue<\/code>, <code>DoRead<\/code> e <code>DoWrite<\/code> trabalham, porque esses m\u00e9todos rodam nas threads do driver.<\/p>\n<h5>Passo 4: o scan de leitura \u2014 DoScanRead<\/h5>\n<p>Perfeito, mas quem l\u00ea os dados do meu equipamento e atualiza os valores dessas \u00e1reas de mem\u00f3ria? Quem faz isso \u00e9 o m\u00e9todo<\/p>\n<pre><code class=\"language-pascal\">procedure DoScanRead(Sender: TObject; var NeedSleep: LongInt);\n<\/code><\/pre>\n<p>Este procedimento \u00e9 chamado, em loop, pela thread de scan para verificar se h\u00e1 algum tag necessitando ser lido. Ele faz isso varrendo a \u00e1rea de mem\u00f3ria gerenciada pelo driver. Ao final, basta informar \u00e0 thread de scan o que foi feito, usando a vari\u00e1vel <code>NeedSleep<\/code>:<\/p>\n<ul>\n<li>Caso <code>NeedSleep = 0<\/code>, a thread segue sua execu\u00e7\u00e3o normal. Retorne 0 sempre que o driver executar alguma a\u00e7\u00e3o de E\/S, pois estas atuam como um &#8220;atraso natural&#8221;, evitando o consumo alto de CPU.<\/li>\n<li>Caso <code>NeedSleep &lt; 0<\/code>, a thread de scan ir\u00e1 for\u00e7ar uma troca de contexto de threads (<code>ThreadSwitch<\/code>), retornando assim que poss\u00edvel. Voc\u00ea pode optar por retornar esse valor sempre que o seu driver n\u00e3o fizer nada.<\/li>\n<li>Caso <code>NeedSleep &gt; 0<\/code>, a thread ir\u00e1 dormir <code>NeedSleep<\/code> milissegundos. Voc\u00ea pode optar por retornar esse valor sempre que o seu driver n\u00e3o fizer nada.<\/li>\n<\/ul>\n<blockquote>\n<p>O erro mais comum de um driver novo \u00e9 retornar <code>0<\/code> sem ter feito E\/S: a thread de scan vira um loop apertado e a CPU vai a 100 %. Se n\u00e3o havia nada para ler, devolva <code>1<\/code> (ou o tempo at\u00e9 o pr\u00f3ximo bloco vencer).<\/p>\n<\/blockquote>\n<p>A rotina de scan do nosso driver fict\u00edcio ficaria parecida com esta (note que ela usa o <code>DoRead<\/code>, apresentado no passo seguinte):<\/p>\n<pre><code class=\"language-pascal\">procedure TProtocoloFicticio.DoScanRead(Sender: TObject; var NeedSleep: LongInt);\nvar\n  clp, bloco: Integer;\n  tagrec: TTagRec;\n  ReadResult: TProtocolIOResult;\n  Values: TArrayOfDouble;\n  fezAlgo: Boolean;\nbegin\n  fezAlgo := False;\n  NeedSleep := 0;\n\n  \/\/ sem porta ativa n\u00e3o h\u00e1 o que fazer: durma um pouco em vez de girar\n  if (PCommPort = nil) or (not PCommPort.ReallyActive) then begin\n    NeedSleep := 1;\n    Exit;\n  end;\n\n  \/\/ varre os CLPs\n  for clp := 0 to High(FCLPs) do begin\n    \/\/ varre os blocos formados das entradas digitais\n    for bloco := 0 to High(FCLPs[clp].Inputs.Blocks) do\n      \/\/ caso o bloco necessite ser atualizado...\n      if FCLPs[clp].Inputs.Blocks[bloco].NeedRefresh then begin\n        \/\/ preenche os campos da estrutura TTagRec que importam\n        \/\/ para o protocolo (ou para a fun\u00e7\u00e3o DoRead)\n        tagrec.Station      := FCLPs[clp].PLCAddress;\n        tagrec.ReadFunction := 1;\n        tagrec.Address      := FCLPs[clp].Inputs.Blocks[bloco].AddressStart;\n        tagrec.Size         := FCLPs[clp].Inputs.Blocks[bloco].Size;\n\n        \/\/ realiza a leitura usando a fun\u00e7\u00e3o DoRead\n        ReadResult := DoRead(tagrec, Values, False);\n        if ReadResult = ioOk then\n          FCLPs[clp].Inputs.SetValues(tagrec.Address, tagrec.Size, 1, Values, ReadResult)\n        else\n          FCLPs[clp].Inputs.SetFault(tagrec.Address, tagrec.Size, 1, ReadResult);\n        fezAlgo := True;\n      end;\n\n    \/\/ repete o procedimento acima para as sa\u00eddas digitais e para os registradores...\n  end;\n\n  if not fezAlgo then\n    NeedSleep := 1;\nend;\n<\/code><\/pre>\n<p>Dois refinamentos que os drivers do PascalSCADA fazem e que valem a pena quando o n\u00famero de blocos cresce:<\/p>\n<ul>\n<li><strong>Ler um bloco por chamada, o mais atrasado primeiro.<\/strong> Em vez de ler todos os blocos vencidos de uma vez (o que atrasa os tags r\u00e1pidos enquanto os lentos s\u00e3o lidos), o driver Modbus monta a lista de todos os blocos, ordena por atraso e l\u00ea s\u00f3 o primeiro; a thread chama <code>DoScanRead<\/code> de novo em seguida. Assim um tag de 100 ms n\u00e3o espera vinte blocos de 5 s.<\/li>\n<li><strong><code>ReadSomethingAlways<\/code><\/strong>: quando nenhum bloco venceu, ler mesmo assim o mais antigo mant\u00e9m a conex\u00e3o viva e detecta a queda do equipamento cedo. Publique a propriedade herdada e respeite-a no seu scan.<\/li>\n<\/ul>\n<h5>Passo 5: lendo do equipamento \u2014 DoRead<\/h5>\n<p>Para n\u00e3o trabalhar dobrado montando pacotes de dados para enviar ao dispositivo, sobrescreva o m\u00e9todo<\/p>\n<pre><code class=\"language-pascal\">function DoRead(const tagrec: TTagRec; out Values: TArrayOfDouble; Sync: Boolean): TProtocolIOResult;\n<\/code><\/pre>\n<p>pois \u00e9 ele quem executa as leituras s\u00edncronas (solicitadas pelo m\u00e9todo <code>Read<\/code> do tag) e ass\u00edncronas (pedidos feitos pelo scan do driver de protocolo). A montagem do pacote do pedido da mem\u00f3ria e a decodifica\u00e7\u00e3o do pacote retornado contendo os valores das mem\u00f3rias deve ser feita aqui. O par\u00e2metro <code>tagrec<\/code> cont\u00e9m as informa\u00e7\u00f5es do tag (endere\u00e7o do equipamento, endere\u00e7o da mem\u00f3ria), enquanto a vari\u00e1vel <code>Values<\/code> ir\u00e1 receber os valores decodificados do pacote recebido. O par\u00e2metro <code>Sync<\/code> n\u00e3o \u00e9 mais usado.<\/p>\n<p>O retorno \u00e9 um <code>TProtocolIOResult<\/code>: <code>ioOk<\/code>, <code>ioTimeOut<\/code>, <code>ioCommError<\/code> (resposta corrompida), <code>ioIllegalFunction<\/code>\/<code>ioIllegalRegAddress<\/code>\/<code>ioIllegalValue<\/code> (o equipamento recusou), <code>ioPLCError<\/code>, <code>ioDriverError<\/code>, <code>ioNullDriver<\/code> (sem porta ativa)\u2026 Escolha o mais espec\u00edfico: ele chega ao tag em <code>LastSyncReadStatus<\/code> e \u00e9 o que o usu\u00e1rio v\u00ea para diagnosticar.<\/p>\n<p>\u00c9 aqui que o driver conversa com a <strong>porta de comunica\u00e7\u00e3o<\/strong>. A porta (<code>TCommPortDriver<\/code>, seja <code>TSerialPortDriver<\/code> ou <code>TTCP_UDPPort<\/code>) n\u00e3o sabe nada do protocolo \u2014 ela s\u00f3 transporta bytes \u2014 e oferece uma opera\u00e7\u00e3o s\u00edncrona:<\/p>\n<pre><code class=\"language-pascal\">function IOCommandSync(Cmd: TIOCommand;             \/\/ iocWrite, iocRead ou iocWriteRead\n                       BytesToWrite: Cardinal; ToWrite: BYTES;\n                       BytesToRead: Cardinal;      \/\/ quantos bytes esperar de resposta\n                       DriverID, DelayBetweenCmds: Cardinal;\n                       pkt: PIOPacket;             \/\/ resultado\n                       OnBegin: TNotifyEvent = nil; OnEnd: TNotifyEvent = nil): Cardinal;\n<\/code><\/pre>\n<p>O <code>TIOPacket<\/code> devolvido traz <code>WriteIOResult<\/code>\/<code>ReadIOResult<\/code> (<code>iorOK<\/code>, <code>iorTimeOut<\/code>, <code>iorPortError<\/code>\u2026), <code>Received<\/code> e <code>BufferToRead<\/code>. Como uma porta pode ser compartilhada por v\u00e1rios drivers, cerque a transa\u00e7\u00e3o com <code>PCommPort.Lock(DriverID)<\/code> \/ <code>Unlock(DriverID)<\/code> \u2014 <code>DriverID<\/code> \u00e9 um n\u00famero \u00fanico que a classe base j\u00e1 deu ao seu driver. O padr\u00e3o que todos os drivers do PascalSCADA seguem:<\/p>\n<pre><code class=\"language-pascal\">function TProtocoloFicticio.DoRead(const tagrec: TTagRec; out Values: TArrayOfDouble;\n  Sync: Boolean): TProtocolIOResult;\nvar\n  pedido: BYTES;\n  pkt: TIOPacket;\n  faltam: LongInt;\nbegin\n  Result := ioNullDriver;\n  if (PCommPort = nil) or (not PCommPort.ReallyActive) then Exit;\n\n  pedido := MontaPedidoDeLeitura(tagrec);          \/\/ seu protocolo: cabe\u00e7alho, fun\u00e7\u00e3o, endere\u00e7o, CRC...\n\n  PCommPort.Lock(DriverID);\n  try\n    \/\/ 1) envia o pedido e l\u00ea o cabe\u00e7alho da resposta (tamanho fixo)\n    if PCommPort.IOCommandSync(iocWriteRead, Length(pedido), pedido, TAMANHO_CABECALHO,\n                               DriverID, 0, @pkt) = 0 then begin\n      Result := ioDriverError;\n      Exit;\n    end;\n    if pkt.ReadIOResult = iorTimeOut then begin Result := ioTimeOut;  Exit; end;\n    if pkt.ReadIOResult &lt;&gt; iorOK      then begin Result := ioCommError; Exit; end;\n\n    \/\/ 2) o cabe\u00e7alho diz quantos bytes ainda v\u00eam: l\u00ea o resto\n    faltam := BytesRestantes(pkt.BufferToRead);\n    if faltam &gt; 0 then begin\n      if PCommPort.IOCommandSync(iocRead, 0, nil, faltam, DriverID, 0, @pkt2) = 0 then ...\n      pkt.BufferToRead := ConcatenateBYTES(pkt.BufferToRead, pkt2.BufferToRead);\n    end;\n\n    \/\/ 3) valida (CRC, eco da fun\u00e7\u00e3o, c\u00f3digo de exce\u00e7\u00e3o) e converte para Double\n    Result := DecodificaResposta(pkt.BufferToRead, tagrec, Values);\n  finally\n    PCommPort.Unlock(DriverID);\n    SetLength(pedido, 0);\n    SetLength(pkt.BufferToRead, 0);\n  end;\nend;\n<\/code><\/pre>\n<p>A leitura em duas etapas \u2014 cabe\u00e7alho de tamanho fixo e depois o resto \u2014 \u00e9 o que permite ao driver saber quantos bytes esperar sem adivinhar; num protocolo de tamanho fixo basta uma chamada. Se a resposta vier de outra fun\u00e7\u00e3o ou de outra esta\u00e7\u00e3o (lixo na linha serial), esvazie a porta com <code>iocRead<\/code> de 1 byte at\u00e9 dar timeout, como o Modbus faz, sen\u00e3o o pedido seguinte l\u00ea a sobra.<\/p>\n<p>Os valores em <code>Values<\/code> s\u00e3o sempre <code>Double<\/code>, <strong>uma palavra do protocolo por posi\u00e7\u00e3o<\/strong>: para um bloco de 4 registradores de 16 bits, <code>Values<\/code> tem 4 elementos com 0..65535. \u00c9 o tag quem monta <code>pttFloat<\/code>, <code>pttLongInt<\/code> etc. a partir dessas palavras, usando o tamanho que voc\u00ea informou em <code>SizeOfTag<\/code> \u2014 o driver n\u00e3o converte tipos.<\/p>\n<p><strong>Separe a montagem do frame do fluxo de E\/S.<\/strong> Os drivers Modbus e Melsec p\u00f5em o fluxo acima em uma classe base da fam\u00edlia e deixam para as subclasses s\u00f3 dois m\u00e9todos virtuais, <code>EncodePkg<\/code> (TTagRec \u2192 bytes) e <code>DecodePkg<\/code> (bytes \u2192 valores + <code>SetValues<\/code>\/<code>SetFault<\/code>). \u00c9 assim que <code>TModBusRTUDriver<\/code> e <code>TModBusTCPDriver<\/code> compartilham 90 % do c\u00f3digo diferindo s\u00f3 no cabe\u00e7alho e no CRC. Fa\u00e7a o mesmo desde o in\u00edcio se o seu protocolo tem variantes serial\/TCP.<\/p>\n<h5>Passo 6: escrevendo no equipamento \u2014 DoWrite<\/h5>\n<p>Bom, j\u00e1 consigo ler meus tags; e como fa\u00e7o para escrever valores no dispositivo? O procedimento <code>DoWrite<\/code> existe para isso, bastando sobrescrev\u00ea-lo, montando o pacote e enviando este ao seu dispositivo:<\/p>\n<pre><code class=\"language-pascal\">function DoWrite(const tagrec: TTagRec; const Values: TArrayOfDouble; Sync: Boolean): TProtocolIOResult;\n<\/code><\/pre>\n<p>\u00c9 este m\u00e9todo quem executa as escritas de valores s\u00edncronas (usando o tag com <code>AutoWrite = False<\/code> e posteriormente chamando o procedimento <code>Write<\/code>) e ass\u00edncronas (quando \u00e9 atribu\u00eddo algum valor \u00e0 propriedade <code>Value<\/code> com <code>AutoWrite = True<\/code>) no equipamento. O par\u00e2metro <code>tagrec<\/code> cont\u00e9m as informa\u00e7\u00f5es do tag (endere\u00e7o do equipamento, endere\u00e7o da mem\u00f3ria \u2014 use <code>WriteFunction<\/code>, n\u00e3o <code>ReadFunction<\/code>), enquanto <code>Values<\/code> cont\u00e9m os valores a serem escritos. O par\u00e2metro <code>Sync<\/code> n\u00e3o \u00e9 mais usado.<\/p>\n<p>O fluxo \u00e9 o mesmo do <code>DoRead<\/code>: montar, <code>Lock<\/code>, <code>IOCommandSync<\/code>, validar a confirma\u00e7\u00e3o, <code>Unlock<\/code>. Depois de uma escrita bem-sucedida, atualize tamb\u00e9m a \u00e1rea interna com <code>SetValues<\/code> \u2014 assim os outros tags que apontam para a mesma mem\u00f3ria mostram o valor novo antes do pr\u00f3ximo scan. Se o driver tiver <code>ReadOnly = True<\/code>, a classe base j\u00e1 recusa a escrita com <code>ioReadOnlyProtocol<\/code> antes de chegar aqui.<\/p>\n<h5>Passo 7: o tamanho das palavras \u2014 SizeOfTag<\/h5>\n<p>Bom, mas meu equipamento tem os registradores sendo words e as entradas e sa\u00eddas digitais s\u00e3o endere\u00e7adas como bytes. Como especifico isso? Simples. Os tags fazem uma s\u00e9rie de convers\u00f5es de tipo, mas o protocolo precisa informar que tipo de dado ele est\u00e1 fornecendo para o tag, para que ele possa fazer as devidas convers\u00f5es. Para isso sobrescreva<\/p>\n<pre><code class=\"language-pascal\">function SizeOfTag(aTag: TTag; isWrite: Boolean; var ProtocolTagType: TProtocolTagType): BYTE;\n<\/code><\/pre>\n<p>pois \u00e9 esta a fun\u00e7\u00e3o respons\u00e1vel por informar o tamanho da palavra em bits que o tag est\u00e1 referenciando e o tipo dela (<code>ptBit<\/code>, <code>ptByte<\/code>, <code>ptShortInt<\/code>, <code>ptWord<\/code>, <code>ptSmallInt<\/code>, <code>ptDWord<\/code>, <code>ptLongInt<\/code>, <code>ptFloat<\/code>, <code>ptInt64<\/code>, <code>ptQWord<\/code>, <code>ptDouble<\/code>). Para retornar o tamanho da palavra use o <code>Result<\/code> da fun\u00e7\u00e3o e para retornar o tipo utilize a vari\u00e1vel <code>ProtocolTagType<\/code>. <code>isWrite<\/code> permite tamanhos diferentes para leitura e escrita (no Modbus, ler com a fun\u00e7\u00e3o 3 e escrever com a 16 d\u00e1 o mesmo tamanho, mas ler com a 1 e escrever com a 5 tamb\u00e9m). Em nosso driver fict\u00edcio ficaria assim (mais uma vez vou tratar somente os blocos, cabe a voc\u00ea tratar os demais tags), assumindo que entradas e sa\u00eddas digitais s\u00e3o bytes e registradores s\u00e3o words:<\/p>\n<pre><code class=\"language-pascal\">function TProtocoloFicticio.SizeOfTag(aTag: TTag; isWrite: Boolean;\n  var ProtocolTagType: TProtocolTagType): BYTE;\nbegin\n  Result := 0;\n  ProtocolTagType := ptUnknown;\n  if aTag is TPLCBlock then\n    case TPLCBlock(aTag).MemReadFunction of\n      1, 2: begin\n        Result := 8;\n        ProtocolTagType := ptByte;\n      end;\n      3: begin\n        Result := 16;\n        ProtocolTagType := ptWord;\n      end;\n    end;\nend;\n<\/code><\/pre>\n<p>Este valor \u00e9 consultado pelo tag ao calcular <code>TagSizeOnProtocol<\/code> e ao montar os <code>TagType<\/code> maiores que a palavra do protocolo (dois registradores de 16 bits viram um <code>pttFloat<\/code>). Se voc\u00ea informar 16 bits mas entregar bytes em <code>Values<\/code>, os valores saem errados.<\/p>\n<h5>Passo 8: removendo tags \u2014 DoDelTag<\/h5>\n<p>Bom, j\u00e1 li demais. Quero apagar meus tags, fechar essa p\u00e1gina e ir embora\u2026 Calma, para o seu tag ser apagado e removido do scan do driver sobrescreva um \u00faltimo m\u00e9todo:<\/p>\n<pre><code class=\"language-pascal\">procedure DoDelTag(TagObj: TTag);\n<\/code><\/pre>\n<p>Sobrescreva este procedimento para remover tags do scan do driver. N\u00e3o esque\u00e7a de chamar o m\u00e9todo herdado,<\/p>\n<pre><code class=\"language-pascal\">inherited DoDelTag(TagObj);\n<\/code><\/pre>\n<p>para remover o tag da classe base. O c\u00f3digo ficaria muito semelhante ao de <code>DoAddTag<\/code>, exceto pelo fato de que o tag ir\u00e1 ser removido da \u00e1rea gerenciada pelo driver (<code>RemoveAddress<\/code> com os mesmos par\u00e2metros do <code>AddAddress<\/code>). Ele \u00e9 chamado quando o tag \u00e9 destru\u00eddo, quando troca de <code>ProtocolDriver<\/code> e \u2014 importante \u2014 <strong>quando qualquer propriedade de endere\u00e7o muda<\/strong>: o tag se remove com o endere\u00e7o antigo e se adiciona de novo com o novo. Por isso <code>DoAddTag<\/code>\/<code>DoDelTag<\/code> precisam ser sim\u00e9tricos.<\/p>\n<p>No <code>destructor<\/code> do driver, libere os <code>TPLCMemoryManager<\/code> que voc\u00ea criou.<\/p>\n<p><strong>Vale lembrar que o seu driver precisa ter uma porta de comunica\u00e7\u00e3o setada e ativa para os tags estarem sendo atualizados. Esta \u00e9 uma condi\u00e7\u00e3o para o driver de protocolo atualizar os tags.<\/strong><\/p>\n<h5>Registrando o driver<\/h5>\n<p>Com os sete m\u00e9todos escritos, falta o driver aparecer na paleta:<\/p>\n<ol>\n<li><strong>Unit no pacote<\/strong>: adicione a unit ao <code>pascalscada.lpk<\/code> (ou ao seu pr\u00f3prio pacote, que dependa do <code>pascalscada<\/code>).<\/li>\n<li><strong>Registro na paleta<\/strong>: em <code>src\/scada_dsng\/scadareg.pas<\/code>, inclua a classe na chamada <code>RegisterComponents(strProtocolsPallete, [...])<\/code>. Num pacote pr\u00f3prio, crie um <code>procedure Register<\/code> com a mesma chamada.<\/li>\n<li><strong>\u00cdcone<\/strong>: um PNG de 24\u00d724 chamado <code>TProtocoloFicticio.png<\/code> em <code>artwork\/24x24png\/<\/code> (e o SVG em <code>artwork\/scalable\/<\/code>); rode <code>artwork\/generateres.sh<\/code> para regerar o arquivo de recursos.<\/li>\n<li><strong>Tag Builder (opcional)<\/strong>: o assistente que cria tags pelo bot\u00e3o direito no driver. Como ele tem forms (LCL), ele fica no pacote de design (<code>scada_dsng<\/code>), n\u00e3o na unit do driver: o driver exp\u00f5e <code>HasTabBuilderEditor<\/code>\/<code>OpenTagEditor<\/code> e uma vari\u00e1vel global <code>SetTagBuilderToolFor\u2026ProtocolFamily<\/code> que o pacote de design preenche na inicializa\u00e7\u00e3o \u2014 veja <code>modbustagassistant.pas<\/code> e <code>siemenstagassistant.pas<\/code> como modelo. Sem ele o driver funciona normalmente, s\u00f3 n\u00e3o tem o item de menu.<\/li>\n<li><strong><code>LiteralTagAddress<\/code><\/strong> (opcional): sobrescreva para o Object Inspector mostrar o endere\u00e7o no formato do seu protocolo (<code>DB5.DBW20<\/code>, <code>40001<\/code>) na dica do tag.<\/li>\n<\/ol>\n<h5>Testando sem o equipamento<\/h5>\n<p>Voc\u00ea n\u00e3o precisa de um CLP para desenvolver o driver: a su\u00edte de testes tem uma <strong>porta falsa<\/strong> (<code>tests\/testsupport.fakeport.pas<\/code>, <code>TFakeCommPort<\/code>) que grava o que o driver escreveu e devolve respostas programadas. Um teste de leitura fica assim (copiado, com adapta\u00e7\u00f5es, de <code>tests\/ut.modbustcp.pas<\/code>):<\/p>\n<pre><code class=\"language-pascal\">\/\/ DoRead \u00e9 protected: uma &quot;sonda&quot; o exp\u00f5e ao teste e desliga a thread de\n\/\/ scan, para que s\u00f3 o teste use a porta falsa.\ntype\n  TProtocoloFicticioProbe = class(TProtocoloFicticio)\n  protected\n    procedure DoScanRead(Sender: TObject; var NeedSleep: LongInt); override;  \/\/ NeedSleep := 1\n  public\n    function ReadSync(const aTag: TTagRec; out aValues: TArrayOfDouble): TProtocolIOResult;\n  end;\n\nfunction TProtocoloFicticioProbe.ReadSync(const aTag: TTagRec; out aValues: TArrayOfDouble): TProtocolIOResult;\nbegin\n  Result := DoRead(aTag, aValues, True);\nend;\n\nprocedure TTestProtocoloFicticio.SetUp;\nbegin\n  FPort := TFakeCommPort.Create(nil);\n  FPort.Active := True;\n  FDrv := TProtocoloFicticioProbe.Create(nil);\n  FDrv.CommunicationPort := FPort;\nend;\n\nprocedure TTestProtocoloFicticio.UmaLeituraVaiEVoltaPelaPorta;\nvar\n  res: TProtocolIOResult;\n  vals: TArrayOfDouble;\nbegin\n  \/\/ o que o &quot;equipamento&quot; responderia\n  FPort.QueueResponse(BytesOf('01 03 04 00 0A 00 14'));\n\n  res := FDrv.ReadSync(TagRecFor(1, 3, 16, 0, 2), vals);   \/\/ esta\u00e7\u00e3o, fun\u00e7\u00e3o de leitura, de escrita, endere\u00e7o, tamanho\n\n  AssertEquals('resultado', Ord(ioOk), Ord(res));\n  AssertBytesEqual('o pedido que saiu', BytesOf('01 03 00 00 00 02'), FPort.LastWrittenFrame);\n  AssertEquals('dois valores', 2, Length(vals));\n  AssertEquals('primeiro registro', 10, vals[0], 0);\n  AssertEquals('resposta consumida inteira', 0, FPort.PendingResponses);\nend;\n<\/code><\/pre>\n<p><code>TagRecFor<\/code>, <code>BytesOf<\/code> e <code>AssertBytesEqual<\/code> v\u00eam de <code>tests\/testsupport.protocol.pas<\/code> e <code>tests\/testsupport.bytes.pas<\/code>. <code>QueueTimeout<\/code> simula o equipamento mudo, para testar o caminho de <code>ioTimeOut<\/code>; <code>WrittenFrame(i)<\/code> inspeciona cada pedido quando o driver faz v\u00e1rias trocas por leitura. Os testes rodam com <code>tests\/pascalscada_tests.lpi<\/code>. Escreva um teste por fun\u00e7\u00e3o do protocolo e um por c\u00f3digo de erro <strong>antes<\/strong> de ligar no equipamento real \u2014 quase todos os defeitos de driver (byte trocado, tamanho errado, CRC) aparecem aqui, em segundos, com o frame esperado ao lado do frame que saiu.<\/p>\n<h5>Erros comuns<\/h5>\n<table>\n<thead>\n<tr>\n<th>Sintoma<\/th>\n<th>Causa<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Tags nunca atualizam e nem marcam falha<\/td>\n<td><code>DoAddTag<\/code> n\u00e3o chamou <code>inherited<\/code> com <code>True<\/code>, ou <code>DoScanRead<\/code> nunca chama <code>SetValues<\/code>\/<code>SetFault<\/code>.<\/td>\n<\/tr>\n<tr>\n<td>CPU em 100 %<\/td>\n<td><code>DoScanRead<\/code> devolve <code>NeedSleep = 0<\/code> sem ter feito E\/S.<\/td>\n<\/tr>\n<tr>\n<td>Segundo driver na mesma porta trava<\/td>\n<td><code>Lock<\/code> sem <code>Unlock<\/code> num caminho de erro \u2014 use <code>try\u2026finally<\/code>.<\/td>\n<\/tr>\n<tr>\n<td>Valores errados s\u00f3 nos tipos de 32 bits<\/td>\n<td><code>SizeOfTag<\/code> informa um tamanho diferente do que <code>Values<\/code> entrega.<\/td>\n<\/tr>\n<tr>\n<td>Tag some do scan ao mudar o endere\u00e7o<\/td>\n<td><code>DoDelTag<\/code> n\u00e3o remove com os mesmos par\u00e2metros que <code>DoAddTag<\/code> adicionou.<\/td>\n<\/tr>\n<tr>\n<td>Leitura seguinte devolve lixo depois de um erro<\/td>\n<td>A porta n\u00e3o foi esvaziada ap\u00f3s uma resposta inesperada.<\/td>\n<\/tr>\n<tr>\n<td>Falhas aparecem como <code>ioDriverError<\/code> gen\u00e9rico<\/td>\n<td><code>DecodePkg<\/code> n\u00e3o distingue timeout, CRC e exce\u00e7\u00e3o do equipamento.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Espero que eu tenha conseguido repassar como os drivers funcionam no PascalSCADA. Cr\u00edticas, sugest\u00f5es, melhorias e principalmente corre\u00e7\u00f5es, usem os coment\u00e1rios.<\/p>\n<h5>Drivers de refer\u00eancia no reposit\u00f3rio<\/h5>\n<p>Todos em <code>src\/scada\/<\/code>, do mais simples ao mais completo:<\/p>\n<ul>\n<li><code>iboxdriver.pas<\/code> \u2014 <code>TIBoxDriver<\/code>: serial, poucos registradores fixos por esta\u00e7\u00e3o, sem gerenciador de mem\u00f3ria; o menor exemplo completo dos sete m\u00e9todos.<\/li>\n<li><code>westasciidriver.pas<\/code> \u2014 <code>TWestASCIIDriver<\/code>: protocolo ASCII serial com checksum.<\/li>\n<li><code>modbusdriver.pas<\/code> + <code>modbusserial.pas<\/code> + <code>modbustcp.pas<\/code> \u2014 a fam\u00edlia Modbus: base com o fluxo de E\/S e <code>TPLCMemoryManager<\/code> por \u00e1rea, subclasses com <code>EncodePkg<\/code>\/<code>DecodePkg<\/code> para RTU e TCP. \u00c9 o modelo recomendado.<\/li>\n<li><code>MelsecDriver.pas<\/code> + <code>MelsecTCP.pas<\/code> \u2014 mesma estrutura de fam\u00edlia, com dez \u00e1reas de mem\u00f3ria.<\/li>\n<li><code>s7family.pas<\/code> + <code>isotcpdriver.pas<\/code> \u2014 protocolo com conex\u00e3o e negocia\u00e7\u00e3o (PDU), v\u00e1rias \u00e1reas e blocos por DB.<\/li>\n<li><code>lgxdriver.pas<\/code> \u2014 EtherNet\/IP: endere\u00e7amento simb\u00f3lico por <code>LongAddress<\/code>, sem gerenciador de mem\u00f3ria por endere\u00e7o.<\/li>\n<\/ul>\n<p>Os testes correspondentes em <code>tests\/ut.*.pas<\/code> mostram, frame a frame, o que cada um envia e espera.<\/p>\n<p><\/p>","protected":false},"excerpt":{"rendered":"<p>Conhecimentos necess\u00e1rios Comunica\u00e7\u00e3o de dados; Conhecer o meio f\u00edsico que seu protocolo usar\u00e1 (serial, TCP\/IP, UDP); Cria\u00e7\u00e3o de componentes em Delphi\/Lazarus; Conhecimento do protocolo sendo implementado \u2014 com a especifica\u00e7\u00e3o &#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"open","ping_status":"open","template":"template-fullwidth.php","meta":{"footnotes":""},"class_list":["post-104","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/pages\/104","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/comments?post=104"}],"version-history":[{"count":26,"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/pages\/104\/revisions"}],"predecessor-version":[{"id":1189,"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/pages\/104\/revisions\/1189"}],"wp:attachment":[{"href":"https:\/\/www.pascalscada.com\/pb\/wp-json\/wp\/v2\/media?parent=104"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}