Newsgroup: it.comp.lang.delphi
Titolo: [D6] PLC e TurboPower AsyncPro - Esperienze - LUNGO

Ciao a tutti.
Più che un aiuto per un problema specifico, mi interessa sentire qualche
parere ed esperienza.
Mi è stato richiesto di rivedere completamente tutto l'apparato di gestione
delle comunicazioni seriali di un'applicazione che si interfaccia ad un PLC,
per renderle un pò più robuste e gestibili, soprattutto in caso di errori ed
eventi eccezionali.
Attualmente tale applicazione fa un uso massivo delle Api di comunicazione
ed a mio avviso il vero punto debole è che, trattandosi di comunicazioni
sincrone eseguite nel thread principale dell'applicazione, al minimo
problema non c'è modo di controllare ciò che avviene e quindi ripristinare
uno stato di normale funzionamento intervenendo da codice. C'è comunque da
dire che i casi di blocco dell'applicazione (che esegue almeno una lettura
al secondo per 24x7) sono veramente piuttosto rari in condizioni normali, un
pò di più se per qualche motivo ho problemi di interferenze (o roditori!!!).
Ho scartato immediatamente la soluzione di riscrivere tutto in modalità
asincrona ed ho cominciato a vedere qualche componente.
Ora, visto che molti mi sembrano soddisfatti dall'uso, da qualche giorno sto
provando gli AsyncPro di TurboPower.
Di primo impatto devo dire che sono rimasto veramente soddisfatto,
soprattutto per la semplicità con cui posso restituire il segnale di ritorno
dal PLC (tramite un TapdDataPacket.OnPacketString).
Senza preoccuparmi troppo della costruzione di un nuovo componente, ho preso
un TDataModule, un TapdComPort e un TapdDataPacket.
Il TDataModule simula un componente esponendo una serie di proprietà ma
soprattutto, tra gli altri, tre metodi:
- gestione di un segnale di test (invio una stringa al PLC che mi deve
restituire la stessa stringa)
- lettura aree di memoria del PLC (passo al PLC una stringa e resto in
attesa del risultato, da trattare e convertire in relazione all'esigenza)
- scrittura aree di memoria PLC (passo al PLC una stringa e resto in attesa
dell'ok dal PLC)
Il segnale di test viene eseguito ciclicamente in un Timer ogni secondo
Le letture e scritture devono essere eseguite seguendo quella che è la
logica del ciclo di esecuzione dell'applicazione
E qui vorrei sentire qualche parere.
1)
Trattandosi di operazioni asincrone, ho definito un flag semaforo
InOperations che mi dice se uno dei metodi è già in esecuzione; tutte le
routine sono strutturate in questo modo:
begin
  self.waitcomplete;
  try
    InOperations := True;
    ... operazioni specifiche della routine
  finally
    InOperations := False;
  end;
end;
dove WaitComplete:
begin
  Ticks := GetTickCount;
  while InOperations do begin
    Application.ProcessMessages;
    if GetTickCount > (Ticks + WAIT_TIMEOUT) then
      raise Exception.Create(...);
  end;
end;
in questo modo una chiamata a lettura e scrittura non viene mai a
sovrapporsi con il risultato dell'altra. Inoltre in Test non attendo la fine
del'evento precedente, ma semplicemente se è in corso un'operazione il test
non è necessario quindi esco per evitare di appesantire ulteriormente le
comunicazioni.
2)
Per ogni routine ho definito un proprio Flag WaitingForTest, WaitingForRead,
WaitingForWrite che ad esempio, uso in questo modo:
WaitingForRead := True;
comPort.output := StringaOut            // contiene l'area di memoria che
voglio leggere dal PLC
Ticks := GetTickCount;
while WaitingForRead do begin
  Application.ProcessMessages;
  if GetTickCount > (Ticks + WAIT_TIMEOUT) then
    raise Exception.Create(...);
end;
if FErrorCode <> PLC_ERRC_OK then
    raise Exception.Create(...);
... se arrivo qui, elaboro e restituisco una stringa ReadBuffer privata al
DataModule
3)
Il TDataPacket, nell'evento OnPacketString, elabora una stringa '@ .... *'
Data che, in relazione al WaitingFor... attivo in quel momento, mi aspetto
abbia il formato specifico:
... controlli di CheckSum comuni a tutte le stringhe in ingresso
if WaitingForRead then begin
    // la stringa dovrebbe essere stata generata da una operazione Read
    try
      ...elaboro la stringa ed imposto ReadBuffer
    finally
      WaitingForRead := False;
    end;
    Exit;
end;
if WaitingForWrite then begin
  ...
Per il momento ho fatto un'applicazione che ha un timer che esegue il test
(ogni 1000 ms), un'altro che esegue delle letture e scritture (ogni 700 ms)
ed un pulsante che forza l'esecuzione di una lettura. Ho notato che a pieno
ritmo (i due timer attivi piu io che clicco frequentemente sul pulsante
forzando la lettura) ogni tanto mi si blocca tutto e mi va in TimeOut per
poi riprendere dopo aver sollevato l'eccezione. Questo credo sia dovuto alla
logica dei timer e dei continui ProcessMessages che creano una coda
eccessiva di eventi in attesa, o probabilmente a qualche anomalia dovuta al
codice che è ovviamente è poco più di un test
Piu o meno questa è la logica con cui andrei poi ad implementare le
comunicazioni.
Se qualcuno ha già esperienza al riguardo, prima di andare a sviluppare
tutto quanto, mi piacerebbe conoscere qualche parere al riguardo, o se
possono essere esistere soluzioni migliori, sia in termini di
implementazione della soluzione, ma soprattutto di scelta dei componenti.
Ringraziando,
Gianluca.

 passo al PLC una stringa e resto in attesa dell'ok dal PLC
Ora: se questo e' veramente cosi' (cioe' *attendi* prima di trasmettere
un ulteriore comando) personalmente adotterei un approccio governato da
una semplice macchina a stati: il tuo datamodule, per quel che riguarda
la comunicazione col PLC puo' essere in uno stato fra i seguenti:
stUndefined, stPortClosed, stReady, stWaitTestAnswer, stWaitReadAnswer,
stWaitWriteAnswer, stTimeout
supponendo che, in caso di ricezione di una risposta il programma la
elabori in sincrono, cioe' nell'eventhandler in cui l'ha ricevuta, e poi
al termine dell'elaborazione passi in stReady.
Avrai uno o piu' timer a regolare la trasmissione della stringa di test,
della stringa di read e della stringa di write: questi ovviamente
trasmetteranno solo se lo stato e' stReady.
Ovviamente dovrai scegliere, a seconda delle specifiche di progetto, se
accodare la richiesta di trasmissione in apposita coda oppure se
soprassedere e tentare al tocco successivo:
procedure TdmCom.OnAnyTimer(Sender: TObject);
var Which: TWhichRequest;
begin
  Which := TWhichRequest(TTimer(Sender).Tag);
  case fStatus of
    stUndef: raise EComPLC.Create('Should not be here');
    stPortClosed: OpenPort;
    stReady: if IsAnyMessageInQueue() then
      SendFirstMessageInQueue
    else
      SendProperRequest(Which); // setta fStatus a stWait
    end;
    stTimeout: ResetSomething;
    else begin
      EnqueueMessage(Which);
    end;
  end;
end;
procedure TdmCom.SendProperRequest(Which: TWhichRequest);
const ProperStatus: array[TWhichRequest] of TStatus = (stWaitTest,
stWait...);
begin
  fStatus := ProperStatus[Which];
  fLastCommand := BuildCommandString(Which);
  // se necessario testa coda uscita se vuota o abbastanza spazio
  ComPlc.PutString(fLastCommand);
  ComPlc.SetTimerTrigger(hTout, 9, True);  // attende risposta max 0.5
sec
end;
procedure TdmCom.OnStringPacket(Sender: TObject; Data: String);
begin
  try
    ComPlc.SetTimerTrigger(hTout, 0, False);    // reset timeout
    // analizza pacchetto, fa quel che deve purche' abbastanza breve
    // altrimenti meglio postarsi un msg per gestirlo in asincrono
  finally
    fStatus := stReady;  // riabilita trasmissione nuovi pacchetti
  end;
end;
procedure TdmCom.TriggerTimer(CP : TObject; TriggerHandle : Word);
begin
  fStatus := stTimeout;
  // ci pensa poi il timer ad agire opportunamente
end;
Quando trasmetti effettivamente attivi il timeout (che purtroppo ha una
risoluzione un po' grossolana) sulla ComPort con SetTimerTrigger. Nel
tuo OnDataPacket disattivi il trigger. Se l'OnTriggerTimer scatta poni
lo stato in stTimeout, dopo di che fai un log dell'evento ed intraprendi
le azioni correttive previste dalle specifiche (apri-chiudi la seriale,
mandi un frame break, attivi una sirena, resetti il pc, mandi una mail).
E questo era un approccio.
Alternativamente, puoi andare in vero sincrono: mandi la tua stringa e
fai una blocking read (WaitChar / GetChar) fino a che hai raggiunto la
fine del pacchetto o hai terminato il tempo di attesa previsto. Per non
_bloccare_ l'applicazione ovviamente occorre mettere il tutto in un
thread separato, il che non e' questo gran problema. Sul sito di Paul
Breneman (
http://www.brenemanlabs.com/AsyncPro.htm
) ci sono esempi, in
particolare ExThread per un esempio semplificato ed ExThreadedClient.
Inoltre, puoi trovare supporto anche sul newsgroup 
news.turbocontrol.com
Saluti
Paolo Arosio
-- 
"La dimostrazione dell'esistenza di specie intelligenti nell'universo
 e' che non ci vengono a trovare" [Calvin & Hobbes]

"Paolo Arosio" ha scritto nel messaggio
[...]
Ora che sto approfondendo i componenti ed il loro funzionamento, sto anche
adattando il mio codice. In quella prima fase mi interessava più che altro
sapere se tali componenti avrebbero potuto fare al caso mio senza investire
tempo eccessivo. Purtroppo in generale non ho molto tempo per verificare
varie soluzioni e quindi devo prendere delle decisioni sulla base di poche
informazioni. Ora li sto usando e vedendo un pò più nel dettaglio.
Ti ringrazio per i suggerimenti, che sono stati molto utili.
Ciao,
Gianluca.