Newsgroup: it.comp.lang.delphi
Titolo: Delphi & Terminali Datalogic Formula 734

Salve a tutti,
 sperando di non essere troppo off-topic ma c'è qualcuno che usa i
Terminali Datalogic 734 con Delphi 7 o 2006 ?
In particolare c'è qualcuno che usa l' ActiveX ( OCX ) Formula per
scaricare i dati da Delphi ?
Questo riesco a farlo.
O meglio c'è qualcuno che riesce a caricare una singola tabella su
questi terminalini ?
Su questo problema mi sono ancorato.
Qualcuno ha qualche suggerimento o esperienza in merito.
Zeno

 Su questo problema mi sono ancorato.
Io mi ci ero "dilettato" qualche anno fa, devo avere ancora dei sorgenti.
Da quello che mi ricordo io perň scaricavo direttamente da seriale (mi 
ero studiato tutto il protocollo di comunicazione...)
Quello che non ricordo (e che non posso testare visto che quei terminali 
non li abbiamo piů), č se funzionava o meno ;)
-- 
Jax




Zeno
=============================================
Noi li usiamo, sono ottimi prodotti, reperibili ed abbastanza economici, 
specie il Formula 732
Nel nostro caso abbiamo seguito la via della semplice lettura di file di 
testo
In pratica, quando appoggi il terminalino nella culla, una procedura (sempre 
attiva) permette di leggere i dati dal terminalino, scriverli in un file di 
testo, archiviarli nella locazione di backup con data e ora, e resettare la 
memoria del terminalino
Quasta procedura verra' attivata ad ogni 'appoggio' del terminalino sulla 
culla
Poi quando l'operatore desidera, puo' avviare dal gestionale la procedura di 
acquisizione dati da terminalino
Verranno cosi' letti ed elaborati i file di testo acquisiti e non ancora 
trattati dal gestionale.
Ciao
RobertoA

Almeno sapere che qualcun'altro ha avuto problemi mi solleva in parte.
Non ho capito: hai scritto un prg che fa lo scarico con il protocollo
seriale dei Formula ?
Ma per lo scarico non bastava, usare l'OCX con i metodi di Open -
Close e agganciando l'evento relativo alla lettura del dato.
Quali componenti hai usato per leggere/scrivere sulla seriale ?
Posso scriverti in privato ?
Zeno

Zeno Michielon ha scritto:

Io ho avuto i tui stessi problemi, il codice qui sotto è il frutto di
due mesi di impazzimento, l'ocx non funziona bene ha parecchi bug's.
Io lo uso per i formula 725.
Alla fine ho realizzato una classe per rendere la vita più facile
spero ti possa essere utile.
Se trovi errori oppure inserisci dei miglioramenti fammelo sapere
Grazie
unit uTerm;
interface
uses
  Controls, DateUtils, Forms, Sysutils, Classes, StrUtils, OleCtrls,
Formula_TLB;
type
    {
    In ogni routine viene controllato se va aperta la porta del
terminalino.
    Dopo aver richiamato qualsiasi operazione sul terminalino,
quest'ultimo
    va chiuso, altrimenti potrebbero esserci comportamenti strani
    mTerm.OnTerminalReceiveData       := TerminalReceiveData;
    mTerm.OnTerminalCompleteOperation := TerminalCompleteOperation;
    mTerm.OnTerminalError             := TerminalError;
    mTerm.OnTerminalStatus            := TerminalStatus;
    }
  TTerminal = class(TObject)
  strict private
  type
    TNotifyReceiveData = procedure (ASender: TObject; AValue: string)
of object;
    TNotifyError = procedure (ASender: TObject; AErrorCode: integer;
AErrorDesc: string) of object;
    TNotifyStatus = procedure (ASender: TObject; ACode: integer;
ADesc: string) of object;
    TNotifyDownloadFile = procedure (ASender: TObject; ATrimTableName:
string; var AStringRead: string) of object;
  var
    FTerm: variant;
    FFormula: TFormula_;
    FComPort: CommPortConstant;
    FFieldSeparator: char;
    FTermReceiveData: TNotifyReceiveData;
    FTermCompleteOperation: TNotifyEvent;
    FTermError: TNotifyError;
    FTerminalStatus: TNotifyStatus;
    procedure SetCompleteOperation(AValue : TNotifyEvent);
    function GetCompleteOperation : TNotifyEvent;
    function GetTerm : variant;
    function GetFormula : TFormula_;
    procedure SetFieldSeparator(AValue : char);
    function GetFieldSeparator : char;
    procedure SetComPort(AValue : CommPortConstant);
    function GetComPort : CommPortConstant;
    procedure SetReceiveData(const Value: TNotifyReceiveData);
    function GetReceiveData: TNotifyReceiveData;
    procedure SetTermError(const Value: TNotifyError);
    function GetTermError: TNotifyError;
    procedure SetTerminalStatus(const Value: TNotifyStatus);
    function GetTerminalStatus: TNotifyStatus;
    //funzioni e procedure private
    function DirectWriteFile(AFileName: string): boolean;
    function UploadDatFile(AFileName: string): boolean;
    //Eventi
    procedure Formula_DataEvent(ASender: TObject; OutputID: Smallint);
    procedure Formula_OutputCompleteEvent(ASender: TObject; OutputID:
Smallint);
    procedure Formula_ErrorEvent(ASender: TObject; ResultCode:
Integer;
      ResultCodeExtended: Integer; ErrorLocus: Integer; var
ErrorResponse: Integer);
    procedure Formula_StatusUpdateEvent(ASender: TObject; OutputID:
Smallint);
  private
    FOnDownloadFile: TNotifyDownloadFile;
    procedure SetOnDownloadFile(const Value: TNotifyDownloadFile);
  public
    constructor Create(AComPort: CommPortConstant = FormulaCommPort1);
    destructor Destroy; override;
    //Propietà
    property ComPort: CommPortConstant read GetComPort write
SetComPort;
    property FieldSeparator: char read GetFieldSeparator write
SetFieldSeparator;
    //Funzioni
    function OpenTerminal: boolean;
    procedure CloseTerminal;
    function DownloadTable(ATableName, AFileName: string;
      ADeleteAfterDownload: boolean = True): boolean;
    function UploadTable(ATableName, AFileName: string): boolean;
    function UploadProgram(AExpFileName: string): boolean;
    function UploadHex(AHexFileName: string): boolean;
    function DeleteTable(ATableName: string): boolean;
    procedure SetTermDateTime(AValue: TDateTime);
    function GetTermDateTime: TDateTime;
    //Oggetti
    property Formula: TFormula_ read GetFormula;
    property Term : Variant read GetTerm;
    //Eventi
    property OnDownloadFile: TNotifyDownloadFile read FOnDownloadFile
write SetOnDownloadFile;
    //-------
    property OnTerminalReceiveData: TNotifyReceiveData read
GetReceiveData write SetReceiveData;
    property OnTerminalCompleteOperation : TNotifyEvent read
GetCompleteOperation write SetCompleteOperation;
    property OnTerminalError: TNotifyError read GetTermError write
SetTermError;
    property OnTerminalStatus: TNotifyStatus read GetTerminalStatus
write SetTerminalStatus;
  end;
  //Thread per upload programma al terminalino
  TUploadHex = class(TThread)
  private
    FTerminal: TTerminal;
    FHexFileName: string;
    procedure SetTerminal(const Value: TTerminal);
    procedure SetHexFileName(const Value: string);
    { Private declarations }
  protected
    property Terminal: TTerminal read FTerminal write SetTerminal;
    property HexFileName: string read FHexFileName write
SetHexFileName;
    procedure Execute; override;
  end;
  function DateAndTimeToDateTime(ADate, ATime: string): TDateTime;
implementation
Const
  //Per scaricare le tabelle
  DOWNLOAD_FILE = 0;
  //Per caricare le tabelle
  UPLOAD_FILE   = 0;
  //Carattere di escape
  ESC           = Chr(27);
  //Per iniziare il download del file
  CODE_DOWNLOAD = '0';
  //Per cancellare il file
  CODE_DELETE   = '1';
  //Per inviare il programma
  CODE_PROGRAM  = 'B';
function DateAndTimeToDateTime(ADate, ATime: string): TDateTime;
var
  lSet: TFormatSettings;
  lYear: Word;
  lMonth: Word;
  lDay: Word;
  lHour: Word;
  lMinute: Word;
  lSecond: Word;
  lMilliSecond: Word;
begin
  //Restituisce il timestamp da due campi DATA e ORA
  lSet.DateSeparator := '-';
  lSet.TimeSeparator := ':';
  lSet.ShortDateFormat := 'dd-mm-yy';
  lSet.TwoDigitYearCenturyWindow := 20;
  DecodeDate(StrToDate(ADate, lSet), lYear, lMonth, lDay);
  DecodeTime(StrToTime(ATime, lSet), lHour, lMinute, lSecond,
lMillisecond);
  result := EncodeDateTime(lYear, lMonth, lDay, lHour, lMinute,
lSecond, lMilliSecond);
end;
{------------------------------------------------------------------------------}
function TTerminal.GetComPort: CommPortConstant;
begin
  result := FComPort;
end;
procedure TTerminal.SetComPort(AValue : CommPortConstant);
begin
  FComPort            := AValue;
  FFormula.CommPort   := FComPort;
end;
procedure TTerminal.SetReceiveData(const Value: TNotifyReceiveData);
begin
  FTermReceiveData := Value;
end;
procedure TTerminal.SetTermDateTime(AValue: TDateTime);
Var
  lRes: boolean;
  str: string;
begin
  lRes := True;
  //Imposta la data del terminalino
  if ((Formula.State = FormulaStateIdle) or
     (Formula.State = FormulaStateClosed))then
  begin
    if (Formula.State = FormulaStateClosed) then
      lRes := OpenTerminal;
    if lRes then
    begin
      Term.Setup := UPLOAD_FILE;
      str := FormatDateTime('yymmddhhmmss', AValue);
      Term.DirectIO(ESC + 'Z' + str + ESC);
    end;
    CloseTerminal;
    Application.ProcessMessages;
  end;
end;
procedure TTerminal.SetTermError(const Value: TNotifyError);
begin
  FTermError := Value;
end;
procedure TTerminal.SetTerminalStatus(const Value: TNotifyStatus);
begin
  FTerminalStatus := Value;
end;
function TTerminal.UploadDatFile(AFileName: string): boolean;
begin
  //Invio file .dat
  result := FileExists(AFileName);
  if result then
    Term.FileWrite(AFileName);
end;
function TTerminal.UploadHex(AHexFileName: string): boolean;
Var
  myThread: TUploadHex;
begin
  //L'upload del firmware, oltre che essere un processo lungo, è anche
  //bloccante, per questo l'ho messo in un thread
  result := True;
  myThread := TUploadHex.Create(True);
  myThread.FreeOnTerminate := True;
  myThread.Terminal        := Self;
  myThread.HexFileName     := AHexFileName;
  myThread.Resume;
  Application.ProcessMessages;
end;
function TTerminal.UploadProgram(AExpFileName: string): boolean;
Var
  Str: string;
  lFileName: TextFile;
  lName, lExt: string;
  lPath: string;
  lRes: boolean;
begin
  {
    Invio programma al terminalino
    Va prima esportato da EasyGen in formato DOS DL
    Il file con estensione EXP contiene i files da trasmettere al
terminalino
    RISPETTANDO la sequenza scritta.
    1. <file>.cfg
    2. <file>.cmd
    3. <table>.inp
      Se esiste <table>.dat
  }
  result := False;
  lRes   := True;
  if FileExists(AExpFileName) and
    ((Formula.State = FormulaStateIdle) or
     (Formula.State = FormulaStateClosed))then
  begin
    if (Formula.State = FormulaStateClosed) then
      lRes := OpenTerminal;
    if lRes then
    begin
      Term.Setup       := UPLOAD_FILE;
      lPath := ExtractFilePath(AExpFileName);
      //Apertura file di configurazione
      AssignFile(lFileName, AExpFileName);
      Reset(lFileName);
      while not EOF(lFileName) do
      begin
        ReadLn(lFileName, Str);
        lExt  := RightStr(Str, 3);
        lName := LeftStr(Str, Pos('.', Str) - 1);
        if (lExt = 'cfg') or (lExt = 'cmd') then
          DirectWriteFile(lPath + Str);
        if (lExt = 'inp') then
          UploadTable(lName, lPath + lName);
        Application.ProcessMessages;
      end;
      CloseFile(lFileName);
      Term.DirectIO (ESC + CODE_PROGRAM + ESC);
      Application.ProcessMessages;
      CloseTerminal;
      Application.ProcessMessages;
      SetTermDateTime(Now);
      result := True;
    end;
  end;
end;
function TTerminal.UploadTable(ATableName, AFileName: string):
boolean;
Var
  lFile: string;
  lRes: boolean;
begin
  {
    Invio tabella al terminalino, se esistono anche i dati
  }
  result := False;
  lRes   := True;
  if (Formula.State = FormulaStateIdle) or
    (Formula.State = FormulaStateClosed) then
  begin
    Term.Setup       := UPLOAD_FILE;
    if (Formula.State = FormulaStateClosed) then
      lRes := OpenTerminal;
    if lRes then
    begin
      //Invio struttura
      lFile  := AFileName + '.inp';
      result := DirectWriteFile(lFile);
      //Invio file DAT
      lFile  := AFileName + '.dat';
      if result then
        UploadDatFile(lFile);
      CloseTerminal;
      Application.ProcessMessages;
    end;
  end;
end;
procedure TTerminal.Formula_DataEvent(ASender: TObject; OutputID:
Smallint);
begin
  Application.ProcessMessages;
  if Assigned(FTermReceiveData) then
    FTermReceiveData(ASender, FFormula.DeviceData);
end;
procedure TTerminal.Formula_ErrorEvent(ASender: TObject; ResultCode,
  ResultCodeExtended, ErrorLocus: Integer; var ErrorResponse:
Integer);
Var
  e: integer;
  eDesc: string;
begin
  Application.ProcessMessages;
  if ResultCode < 114 then
    e := ResultCode
  else
    e := ResultCodeExtended;
  case e of
  0   : eDesc := 'Operation completed successfully';
  101 : eDesc := 'Device is closed';
  102 : eDesc := 'Device is blocked';
  103 : eDesc := 'Device does not have exclusive access';
  104 : eDesc := 'Requested service does not exist';
  105 : eDesc := 'Device disabled';
  106 : eDesc := 'Invalid operation';
  107 : eDesc := 'Device requested does not exist or cannot be used';
  108 : eDesc := 'Device off-line';
  109 : eDesc := 'Device does not exist';
  110 : eDesc := 'Device already exists';
  111 : eDesc := 'Operation not successfully carried out';
  112 : eDesc := 'Maximum operation time exceeded';
  113 : eDesc := 'Device busy';
  //114 : eDesc := 'Extended error in checking the property
ResultCodeExtended';
  201 : eDesc := 'Invalid address';
  202 : eDesc := 'Device''s file invalid or in use';
  203 : eDesc := 'Device''s file was not opened';
  204 : eDesc := 'Device''s memory full';
  205 : eDesc := 'RAM error in device';
  206 : eDesc := 'EEPROM error in device';
  207 : eDesc := 'RAM and EEPROM error in device';
  208 : eDesc := 'Device''s polling inactive';
  end;
  if Assigned(FTermError) then
    FTermError(ASender, e, eDesc);
end;
procedure TTerminal.Formula_OutputCompleteEvent(ASender: TObject;
  OutputID: Smallint);
begin
  Application.ProcessMessages;
  //Al completamento chiudo
  if (Formula.State = FormulaStateIdle) then
    Formula.Close;
  if Assigned(FTermCompleteOperation) then
    FTermCompleteOperation(ASender);
end;
function TTerminal.GetFieldSeparator: char;
begin
  result := FFieldSeparator;
end;
procedure TTerminal.SetFieldSeparator(AValue : char);
begin
  FFieldSeparator      := AValue;
  FTerm.FieldSeparator := Ord(FFieldSeparator);
end;
procedure TTerminal.SetOnDownloadFile(const Value:
TNotifyDownloadFile);
begin
  FOnDownloadFile := Value;
end;
function TTerminal.GetFormula: TFormula_;
begin
  result := FFormula;
end;
function TTerminal.GetReceiveData: TNotifyReceiveData;
begin
  result := FTermReceiveData;
end;
procedure TTerminal.CloseTerminal;
begin
  if not (Formula.State = FormulaStateClosed) then
    Formula.Close;
  Application.ProcessMessages;
end;
constructor TTerminal.Create(AComPort: CommPortConstant);
begin
	inherited Create;
  FFormula := TFormula_.Create(nil);
  SetComPort(AComPort);
  Formula.BaudRate              := FormulaBaud9600;
  Formula.ByteSize              := FormulaByteSize8;
  Formula.Parity                := FormulaParityMark;
  Formula.Sleep                 := 2;
  Formula.ACKTimeout            := 200;
  FTerm                         := Formula.Terminals[0].Add;
  Term.ID                       := 1;
  Term.Setup                    := 0;//FormulaSetupPolling +
FormulaSetupDownloadRecord + FormulaSetupFileAppend;
  Formula.OnDataEvent           := Formula_DataEvent;
  Formula.OnOutputCompleteEvent := Formula_OutputCompleteEvent;
  Formula.OnErrorEvent          := Formula_ErrorEvent;
  Formula.OnStatusUpdateEvent   := Formula_StatusUpdateEvent;
  Formula.DataEventEnabled      := True;
end;
function TTerminal.DeleteTable(ATableName: string): boolean;
begin
  {
    Cancellazione dati tabella su terminalino
    Il nome della tabella deve essere di 8 caratteri FISSI
  }
  result := True;
  if (Formula.State = FormulaStateClosed) or
   (Formula.State = FormulaStateIdle) then
  begin
    if (Formula.State = FormulaStateClosed) then
      result := OpenTerminal;
    if result then
    begin
      ATableName := Copy(ATableName + '        ', 1, 8);
      Term.DirectIO(ESC + CODE_DELETE + ATableName + ESC);
      CloseTerminal;
      Application.ProcessMessages;
    end;
  end;
end;
destructor TTerminal.Destroy;
begin
	inherited Destroy;
  CloseTerminal;
  FFormula.Free;
end;
function TTerminal.DirectWriteFile(AFileName: string): boolean;
Var
  lFileName: TextFile;
  lRes: boolean;
  Str: string;
begin
  result := False;
  lRes   := True;
  //Apertura file fisico ed invio riga per riga
  if (FileExists(AFileName)) and
    ((Formula.State = FormulaStateIdle) or
    (Formula.State = FormulaStateClosed)) then
  begin
    if (Formula.State = FormulaStateClosed) then
      lRes := OpenTerminal;
    if lRes then
    begin
      AssignFile(lFileName, AFileName);
      Reset(lFileName);
      ReadLn(lFileName, Str);
      while not EOF(lFileName) do
      begin
        Term.DirectIO(Str);
        ReadLn(lFileName, Str);
        Application.ProcessMessages;
      end;
      Term.DirectIO(Str);
      CloseFile(lFileName);
      Term.ClearOutput;
      result := True;
    end;
  end;
end;
function TTerminal.DownloadTable(ATableName, AFileName: string;
  ADeleteAfterDownload: boolean): boolean;
Var
  Str: string;
  lRes: boolean;
  lFileName: TextFile;
begin
  {
    Viene scaricata nel percorso indicato la tabella relativa,
    è possibile indicare se cancellare i dati dopo il loro invio
    Se non leggo niente la tabella non esiste, controllare bene
    il nome della tabella, è case-sensitive ed il nome deve essere
    lungo SEMPRE 8 caratteri, nel caso sia più breve riempire con
    spazi.
    **** Attenzione! E' case-sensitive !!! ****
  }
  result := False;
  lRes   := True;
  if (Formula.State = FormulaStateClosed) or
   (Formula.State = FormulaStateIdle) then
  begin
    Term.Setup  := DOWNLOAD_FILE;
    if (Formula.State = FormulaStateClosed) then
      lRes := OpenTerminal;
    if lRes then
    begin
      //Il nome della tabella deve essere di 8 caratteri FISSI
      ATableName := Copy(ATableName + '        ', 1, 8);
      Str := ESC + CODE_DOWNLOAD + ATableName + ESC;
      Term.DirectIO(Str);
      Str := Term.DirectIORead('');
      if not (Str = '') then
      begin
        AssignFile(lFileName, AFileName);
        Rewrite(lFileName);
        //Inizio a leggere i dati dal terminalino finchè non finisce
la tabella
        while not (Pos('EOF', Str) > 0) and not (Str = '') do
        begin
          if Assigned(FOnDownloadFile) then
            FOnDownloadFile(Self, Trim(ATableName), Str);
          WriteLn(lFileName, Str);
          Str := Term.DirectIORead('');
          Application.ProcessMessages;
        end;
        CloseFile(lFileName);
        //Cancellazione dati da terminalino, se richiesto
        if ADeleteAfterDownload then
          DeleteTable(ATableName);
        Term.ClearOutput;
        CloseTerminal;
        Application.ProcessMessages;
        result := True;
      end;
    end;
  end;
end;
procedure TTerminal.Formula_StatusUpdateEvent(ASender: TObject;
  OutputID: Smallint);
var
  code: integer;
  desc: string;
begin
  Application.ProcessMessages;
  case FTerm.State of
  FormulaStateClosed :
    begin
      code := FormulaStateClosed;
      desc := 'Closed!';
    end;
  FormulaStateIdle   :
    begin
      code := FormulaStateIdle;
      desc := 'Idle!';
    end;
  FormulaStateError  :
    begin
      code := FormulaStateBusy;
      desc := 'Error!';
    end;
  FormulaStateBusy   :
    begin
      code := FormulaStateError;
      desc := 'Busy!';
    end;
  else
    code := 999;
    desc := 'Unknow status';
  end;
  //Al completamento chiudo
  if (Formula.State = FormulaStateIdle) then
    Formula.Close;
  if Assigned(FTerminalStatus) then
    FTerminalStatus(ASender, code, desc);
end;
function TTerminal.GetTerm: variant;
begin
  result := FTerm;
end;
function TTerminal.GetTermDateTime: TDateTime;
Var
  str: string;
  lRes: boolean;
begin
  lRes := True;
  result := 0;
  //Restituisce la data del terminalino
  if ((Formula.State = FormulaStateIdle) or
     (Formula.State = FormulaStateClosed))then
  begin
    if (Formula.State = FormulaStateClosed) then
      lRes := OpenTerminal;
    if lRes then
    begin
      Term.Setup := DOWNLOAD_FILE;
      Term.DirectIO(ESC + 'Z?' + ESC);
      str := Term.DirectIOText;
      if not (str = '') then
        result := EncodeDateTime(StrToInt('20' + Copy(str, 1, 2)),
StrToInt(Copy(str, 3, 2)), StrToInt(Copy(str, 5, 2)),
          StrToInt(Copy(str, 7, 2)), StrToInt(Copy(str, 9, 2)),
StrToInt(Copy(str, 11, 2)), 0);
    end;
    CloseTerminal;
  end;
end;
function TTerminal.GetTermError: TNotifyError;
begin
  result := FTermError;
end;
function TTerminal.GetTerminalStatus: TNotifyStatus;
begin
  result := FTerminalStatus;
end;
function TTerminal.OpenTerminal: boolean;
begin
  Formula.Close;
  result := Formula.Open = FormulaSuccess;
  Application.ProcessMessages;
end;
function TTerminal.GetCompleteOperation: TNotifyEvent;
begin
  result := FTermCompleteOperation;
end;
procedure TTerminal.SetCompleteOperation(AValue : TNotifyEvent);
begin
  FTermCompleteOperation := AValue;
end;
{------------------------------------------------------------------------------}
{ TUploadHex }
{ Important: Methods and properties of objects in visual components
can only be
  used in a method called using Synchronize, for example,
      Synchronize(UpdateCaption);
  and UpdateCaption could look like,
    procedure TUploadHex.UpdateCaption;
    begin
      Form1.Caption := 'Updated in a thread';
    end; }
{ TUploadHex }
procedure TUploadHex.Execute;
begin
  {
    Invio firmware terminalino, il terminalino deve essere chiuso
    essendo un processo bloccante, l'ho messo in un thread
  }
  with Terminal do
  begin
    if FileExists(FHexFileName) then
    begin
      if not (Formula.State = FormulaStateClosed) then
        CloseTerminal;
      try
        Formula.HexWrite (FHexFileName);
      except
        //Alla fine del processo ritorna sempre una Exception, pur
avendo
        //fatto bene l'invio
      end;
    end;
  end;
end;
procedure TUploadHex.SetHexFileName(const Value: string);
begin
  FHexFileName := Value;
end;
procedure TUploadHex.SetTerminal(const Value: TTerminal);
begin
  FTerminal := Value;
end;
end.

Semplicemente GRAZIE !!
Provo subito il tuo codice .... da una rapida occhiata ho già visto
dove forse sbagliavo !!
Spero di poter ricambiare.
Zeno