////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2001-2023 Alexey Kuryakin daqgroup@mail.ru under MIT license //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// This file is part of the CRW-DAQ project by DaqGroup - component CRWKIT.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Purpose:                                                                   //
// SMI User Interface server for CRW-DAQ Finite State Machines.               //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 2020xxxx - Created (A.K.)                                                  //
// 20230907 - Modified for FPC (A.K.)                                         //
////////////////////////////////////////////////////////////////////////////////

program smiuisrv; // SMI User Interface server

{$I _crw_sysdef.inc}

{$I _crw_sysmode.inc}

{-$WARN 5024 off : Parameter "$1" not used}
{-$WARN 5028 off : Local $1 "$2" is not used}

{$R *.res}

uses
 //////////////////////////////////////////////////////
 {$I _crw_uses_first.inc} // NB: MUST BE FIRST USES !!!
 //////////////////////////////////////////////////////
 {$IFDEF WINDOWS} shellapi, {$ENDIF}
 sysutils, classes, math, forms, interfaces,
 _crw_alloc, _crw_cmdargs, _crw_str, _crw_base64, _crw_fio, _crw_fifo,
 _crw_rtc, _crw_polling, _crw_ascio, _crw_az, _crw_dim, _crw_smiuirtl,
 _crw_bsencode, _crw_task, _crw_fsm, _crw_proc;

 //
 // OS abstraction layer routines
 //
{$IFDEF SKIP_DRAFT}
// TODO: for UNIX
{$ELSE ~SKIP_DRAFT}
const
 THREAD_PRIORITY_IDLE          = -15;
 THREAD_PRIORITY_LOWEST        = -2;
 THREAD_PRIORITY_BELOW_NORMAL  = -1;
 THREAD_PRIORITY_NORMAL        = 0;
 THREAD_PRIORITY_ABOVE_NORMAL  = 1;
 THREAD_PRIORITY_HIGHEST       = 2;
 THREAD_PRIORITY_TIME_CRITICAL = 15;
function GetCurrentProcess:THandle;
begin
 Result:=0;
 {$IF DEFINED (WINDOWS)}
 Result:=windows.GetCurrentProcess;
 {$ELSEIF DEFINED (UNIX)}
 Result:=GetCurrentProcessId;
 {$ENDIF }
end;
function GetPriorityClass(hProcess:THandle):DWORD;
begin
 Result:=0;
 {$IF DEFINED (WINDOWS)}
 Result:=windows.GetPriorityClass(hProcess);
 {$ELSEIF DEFINED (UNIX)}
 Result:=0;
 {$ENDIF }
end;
function SetPriorityClass(hProcess:THandle; aClass:DWORD):Boolean;
begin
 Result:=false;
 {$IF DEFINED (WINDOWS)}
 Result:=windows.SetPriorityClass(hProcess,aClass);
 {$ELSEIF DEFINED (UNIX)}
 Result:=false;
 {$ENDIF}
end;
function GetCurrentThread:THandle;
begin
 Result:=0;
 {$IF DEFINED (WINDOWS)}
 Result:=windows.GetCurrentThread;
 {$ELSEIF DEFINED (UNIX)}
 Result:=GetCurrentThreadId;
 {$ENDIF}
end;
function SetThreadPriority(hThread:THandle; aPriority:LongInt):Boolean;
begin
 Result:=false;
 {$IF DEFINED (WINDOWS)}
 Result:=windows.SetThreadPriority(hThread,aPriority);
 {$ELSEIF DEFINED (UNIX)}
 Result:=false;
 {$ENDIF}
end;
{$ENDIF ~SKIP_DRAFT}
 //
 // General variables and constants
 //
const ///////////////////////////////////// General variables
 Terminated       : Boolean     = false; // Program should be terminated
 opt_verbose      : Boolean     = false; // Option -verbose
 smiui_err_count  : Integer     = 0;     // Error counter
 smiui_dom_count  : Integer     = 0;     // Domain handler counter
 smiui_sta_count  : Integer     = 0;     // State  handler counter
 smiui_msg_count  : Integer     = 0;     // SMI  message handler counter
 smiui_usr_count  : Integer     = 0;     // User message handler counter
 smiui_set_count  : Integer     = 0;     // Objectset    handler counter
 fsm              : Integer     = 0;     // FsmManager reference
 dns_version      : Integer     = 0;     // To receive DNS version
 dns_no_link      : Integer     = -1;    // To detect  DNS server die
 dns_task_id      : LongString  = '';    // DIS_DNS@hostname
 dns_proc_id      : Integer     = 0;     // DNS process PID
 dns_conn_id      : Integer     = 0;     // DNS connection ID
 dns_curnode      : LongString  = ' ';   // DNS current node

const ///////////////////////////////////// FSM error codes
 fsm_errno_get_param            = 1;     // FSM error number on get parameter
 fsm_errno_set_param            = 2;     // FSM error number on set parameter

const /////////////////////////////////////
 MAX_OPTS                       = 512;   // Buffer size for options
 EmptyStrReplacer               = '-';   // Uses to replace empty strings

const ///////////////////////////////////// String identifiers for parameters
 par_book_statechange_id        = 'book_statechange_id';
 par_book_smi_message_id        = 'book_smi_message_id';
 par_book_user_message_id       = 'book_user_message_id';
 par_book_objectsetchange_id    = 'book_objectsetchange_id';
 par_number_of_objects          = 'number_of_objects';
 par_opt_auto_booking           = 'opt_auto_booking';
 par_dim_dns_node               = 'dim_dns_node';

const ///////////////////////////////////// SMI++ special chars to be escaped in string parameters
 smixx_spec_chars = [QuoteMark,Apostrophe,' ',',','=',';','/','|','&','<','>']+JustSpaces;
 //
 // Types uses for buffers of names and options
 //
type
 TNameBuffer    = packed array[0..MAX_NAME] of char;
 TOptionsBuffer = packed array[0..MAX_OPTS] of char;
 //
 // Callback on receive DNS version
 //
 procedure on_got_dns_version(var tag:TDimLong; buff:Pointer; var size:Integer); cdecl;
 var pid:Integer; node:TNameBuffer;
 begin
  try
   if (size=SizeOf(dns_no_link)) and (Integer(buff^) = dns_no_link) then begin
    StdOut.Put:='@smiui_report WARNING: DNS server is dead.';
    dns_version:=Integer(buff^);
    dns_task_id:='';
    dns_proc_id:=0;
    Exit;
   end;
   if (size=SizeOf(dns_version)) then begin
    dns_version:=Integer(buff^);
    node:=''; pid:=0; // Get server info
    if dic_get_server(node)=0 then node:='';
    if dic_get_server_pid(pid)=0 then pid:=0;
    dns_task_id:=Trim(node); dns_proc_id:=pid;
    Exit;
   end;
   dns_version:=0;
   dns_task_id:='';
   dns_proc_id:=0;
  except
   on E:Exception do BugReport(E,nil,'on_got_dns_version');
  end;
 end;
 //
 // Disconnect DNS version.
 //
 procedure dns_disconnect;
 begin
  if (dns_conn_id<>0) then begin
   dic_release_service(dns_conn_id);
   dns_conn_id:=0;
  end;
  dns_version:=0;
  dns_task_id:='';
  dns_proc_id:=0;
 end;
 //
 // Connect DNS version.
 //
 procedure dns_connect(reconnect:Boolean=false);
 begin
  if (dns_conn_id<>0) and not reconnect then Exit;
  if (dns_conn_id<>0) then dns_disconnect;
  dns_conn_id:=dic_info_service('DIS_DNS/VERSION_NUMBER',MONITORED,5,
               nil,0,on_got_dns_version,0,@dns_no_link,SizeOf(dns_no_link));
 end;
 //
 // Init/Free FSM Manager
 //
 procedure InitFsm;
 begin
  if (fsm<>0) then Exit;
  fsm:=fsm_new;
 end;
 procedure FreeFsm;
 begin
  if (fsm<>0) then begin
   fsm_free(fsm);
   fsm:=0;
  end;
 end;
 //
 // Helper routines.
 //
 procedure UnbookDomain(domn:LongString);
 var dom,par:Integer;
 begin
  dom:=fsm_find(fsm,fsm_type_domain,domn);
  par:=fsm_find(dom,fsm_type_int,par_number_of_objects);
  fsm_set_iparam(par,0);
 end;
 //
 // Print usage info on --help option.
 //
 procedure PrintUsageInfo;
 begin
  StdOut.Put:='Usage: ';
  StdOut.Put:=' '+CmdArgs.ExeBaseName+' [-opt [par]]';
  StdOut.Put:='Where:';
  StdOut.Put:=' -opt      - option start from -';
  StdOut.Put:=' par       - parameter or option';
  StdOut.Put:='Options:';
  StdOut.Put:=' -dns n    - Set DIM_DNS_NODE n';
  StdOut.Put:=' -dom d    - SMI domain name d';
  StdOut.Put:=' -verbose  - Print verbose';
  StdOut.Put:=' -verb     - Print verbose';
  StdOut.Put:=' -auto     - auto book all objects';
  StdOut.Put:=' -sleep n  - sleep for n milliseconds';
  StdOut.Put:='Notes:';
  StdOut.Put:=' 1) Option --opt means same as -opt';
  StdOut.Put:=' 2) -dns . is same as -dns localhost';
  StdOut.Put:='Example:';
  StdOut.Put:=' '+CmdArgs.ExeBaseName+' -verb -auto -dns localhost -dom DEMO -sleep 1000';
 end;
 //
 // Is string (s) is empty?
 //
 function IsEmptyStr(const s:LongString):Boolean;
 begin
  Result:=(SysUtils.Trim(s)='');
 end;
 //
 // Return string - original (arg) or default (def).
 //
 function StrDef(const arg:LongString; const def:LongString=EmptyStrReplacer):LongString;
 begin
  if (arg<>'') then Result:=arg else Result:=def;
 end;
 //
 // Copy string to MAX_NAME size buffer.
 //
 function StrNameCopy(Buff:PChar; const S:LongString):PChar;
 begin
  Result:=StrLCopy(Buff,PChar(S),MAX_NAME);
 end;
 //
 // Append string Item to string List with Delimiter.
 //
 procedure AppendStringListItem(var List:LongString; const Delimiter:LongString; const Item:LongString);
 begin
  if (List='') then List:=Item else List:=List+Delimiter+Item;
 end;
 //
 // Report on error/info.
 //
 procedure ReportError(const msg:LongString);
 begin
  StdOut.Put:='@smiui_report ERROR: '+msg;
  LockedInc(smiui_err_count);
 end;
 procedure ReportInfo(const msg:LongString);
 begin
  StdOut.Put:='@smiui_report INFO: '+msg;
 end;
 //
 // FSM error handler.
 //
 procedure fsm_trouble(errno:Integer; msg:LongString);
 begin
  if (errno<>0) then ReportError(msg);
 end;
 //
 // SMI++ Encode/Decode data string uses in string parameters.
 //
 function smixx_encode(data:LongString; quoted:Boolean=false):LongString;
 begin
  Result:=backslash_encode(data,[],smixx_spec_chars);
  if quoted and (Result<>'') then Result:=AnsiQuotedIfNeed(Result);
 end;
 function smixx_decode(data:LongString):LongString;
 begin
  Result:=backslash_decode(data);
 end;
 //
 // FSM add parameter int/float/string and set data.
 //
 procedure fsm_add_iparam(ref:Integer; name:LongString; data:Integer);
 begin
  if not fsm_set_iparam(fsm_add(ref,fsm_type_int,name),data)
  then fsm_trouble(fsm_errno_set_param,'fsm_add_iparam '+name);
 end;
 procedure fsm_add_fparam(ref:Integer; name:LongString; data:Double);
 begin
  if not fsm_set_fparam(fsm_add(ref,fsm_type_float,name),data)
  then fsm_trouble(fsm_errno_set_param,'fsm_add_fparam: '+name);
 end;
 procedure fsm_add_sparam(ref:Integer; name:LongString; data:LongString);
 begin
  if not fsm_set_sparam(fsm_add(ref,fsm_type_string,name),data)
  then fsm_trouble(fsm_errno_set_param,'fsm_add_sparam: '+name);
 end;
 //
 // FSM put (set by name) parameter int/float/string with given data.
 //
 procedure fsm_put_iparam(ref:Integer; name:LongString; data:Integer);
 begin
  if not fsm_set_iparam(fsm_find(ref,fsm_type_int,name),data)
  then fsm_trouble(fsm_errno_set_param,'fsm_put_iparam '+name);
 end;
 procedure fsm_put_fparam(ref:Integer; name:LongString; data:Double);
 begin
  if not fsm_set_fparam(fsm_find(ref,fsm_type_float,name),data)
  then fsm_trouble(fsm_errno_set_param,'fsm_put_fparam: '+name);
 end;
 procedure fsm_put_sparam(ref:Integer; name:LongString; data:LongString);
 begin
  if not fsm_set_sparam(fsm_find(ref,fsm_type_string,name),data)
  then fsm_trouble(fsm_errno_set_param,'fsm_put_sparam: '+name);
 end;
 //
 // FSM ask (get by name) int/float/string parameter value.
 //
 function fsm_ask_iparam(ref:Integer; name:LongString):Integer;
 var par:Integer; v:Integer;
 begin
  v:=0;
  par:=fsm_find(ref,fsm_type_int,name);
  if (par<>0) then v:=fsm_get_iparam(par);
  fsm_ask_iparam:=v;
 end;
 function fsm_ask_fparam(ref:Integer; name:LongString):Double;
 var par:Integer; v:Double;
 begin
  v:=0;
  par:=fsm_find(ref,fsm_type_float,name);
  if (par<>0) then v:=fsm_get_fparam(par);
  fsm_ask_fparam:=v;
 end;
 function fsm_ask_sparam(ref:Integer; name:LongString):LongString;
 var par:Integer; v:LongString;
 begin
  v:='';
  par:=fsm_find(ref,fsm_type_string,name);
  if (par<>0) then v:=fsm_get_sparam(par);
  fsm_ask_sparam:=v;
 end;
 //
 // SMI++ domain connect handler.
 // Example:
 // > @smiui_book_connect_domain=DEMO
 // < @smiui_connect_domain_handler=1,DEMO,6,DEMO::&DOMAIN|DEMO::AUTO_PILOT|DEMO::RUN_TYPE|DEMO::RUN|DEMO::LOGGER/ASSOCIATED|DEMO::EVT_BUILDER/ASSOCIATED
 //   Word[1] = Call Count
 //   Word[2] = Domain Name
 //   Word[3] = Domain Objects Counter
 //   Word[4] = List of objects/attributes separated with | delimiter or '-' of no objects
 //
 procedure smiui_connect_domain_handler(var par:dim_long_t; var nobjs:Integer); cdecl;
 var dom:dim_long_t; nobj,auto:Integer; domn,objn:TNameBuffer; attr:TOptionsBuffer; objs,outs:LongString;
 begin
  try
   inc(smiui_dom_count);
   dom:=par; nobj:=nobjs; objs:=''; domn:='';
   if (dom<>0) then StrNameCopy(domn,fsm_name(dom));
   auto:=fsm_ask_iparam(dom,par_opt_auto_booking);
   if (nobj>0) then
   while (smiui_get_next_object(domn,objn)>0) do begin
    if (objs<>'') then objs:=objs+'|'+objn else objs:=objs+objn;
    while (smiui_get_next_attribute(objn,attr)>0) do objs:=objs+'/'+attr;
    if (auto<>0) and (Pos('&',objn)=0) then begin
     StdIn.Put:='@smiui_book_statechange='+objn;
    end;
   end;
   if (auto<>0) and (nobj>0) then begin
    fsm_put_iparam(dom,par_opt_auto_booking,0);
    StdIn.Put:='@smiui_book_smi_message='+domn;
    StdIn.Put:='@smiui_book_user_message='+domn;
    StdIn.Put:='@smiui_list_domain_objectsets='+domn+',auto';
   end;
   outs:=Format('@smiui_connect_domain_handler=%d,%s,%d,%s',[smiui_dom_count,StrDef(domn),nobj,StrDef(objs)]);
   StdOut.Put:=outs;
  except
   on E:Exception do BugReport(E,nil,'smiui_connect_domain_handler');
  end;
 end;
 //
 // SMI++ object state change handler.
 // Example:
 // > @smiui_book_statechange=DEMO::LOGGER
 // > @smiui_book_statechange=DEMO::EVT_BUILDER
 // < @smiui_statechange_handler=1,DEMO::LOGGER,LOGGING,-,NOLOG|X_OPEN_FILE,-
 // < @smiui_statechange_handler=2,DEMO::EVT_BUILDER,ERROR,-,RECOVER,NUMBER_T(I)=145/NUMBER_P(I)=28
 //   Word[1] = Call Count
 //   Word[2] = Object Name
 //   Word[3] = Object State
 //   Word[4] = Action in progress (if busy) or '-' (if not busy)
 //   Word[5] = List of Actions available for State (with | delimiter) or '-' if no actions
 //   Word[6] = List of Parameters (with / delimiter) or '-' if no parameters available
 //             Parameters: string NAME(S)=VALUE or int NAME(I)=VALUE or float NAME(F)=VALUE
 //             String parameters passed as backslash_encode() data string
 //
 procedure smiui_statechange_handler(var id:Integer; var par:dim_long_t); cdecl;
 var obj:dim_long_t; busy_flg,n_actions:Integer;
     objn,state,busy_act:TNameBuffer; acts,pars,outs:LongString;
  function GetActionWithParams(id:Integer; action:LongString; n_pars:Integer):LongString;
  var param:TNameBuffer; typ,size,iv:Integer; sv,par,buff:LongString; fv:Double;
  begin
   typ:=0; size:=0; sv:=''; par:=''; buff:='';
   Result:=UpperCase(action);
   if (n_pars>0) then
   while (smiui_get_next_param(id,param,typ,size)>0) do begin
    par:=UpperCase(param); sv:='';
    case typ of
     SMI_STRING: begin
      par:=par+'(S)';
      if (size>0) then begin
       SetLength(buff,size);
       if (smiui_get_param_default_value(id,PChar(buff))>0) then sv:=PChar(buff);
       if (sv<>'') then sv:=smixx_encode(sv,true);
       if (sv<>'') then par:=par+'='+sv;
      end;
     end;
     SMI_INTEGER: begin
      par:=par+'(I)';
      if (size=sizeof(iv)) then begin
       if (smiui_get_param_default_value(id,@iv)>0) then sv:=IntToStr(iv);
       if (sv<>'') then par:=par+'='+sv;
      end;
     end;
     SMI_FLOAT: begin
      par:=par+'(F)';
      if (size=sizeof(fv)) then begin
       if (smiui_get_param_default_value(id,@fv)>0) then sv:=Format('%g',[fv]);
       if (sv<>'') then par:=par+'='+sv;
      end;
     end;
     else begin
      par:='';
     end;
    end;
    if (par<>'') then AppendStringListItem(Result,'/',par);
   end;
  end;
  function GetActionsWithParams(id:Integer; n_actions:Integer):LongString;
  var action:TNameBuffer; n_pars:Integer;
  begin
   Result:=''; action:=''; n_pars:=0;
   if (n_actions>0) then
   if (smiui_get_first_action(id,action,n_pars)>0) then begin
    AppendStringListItem(Result,'|',GetActionWithParams(id,action,n_pars));
    while (smiui_get_next_action(id,action,n_pars)>0) do begin
     AppendStringListItem(Result,'|',GetActionWithParams(id,action,n_pars));
    end;
   end;
  end;
  function GetObjectParameters(id:Integer; objn:PChar):LongString;
  var typ,size,iv:Integer; param:TNameBuffer; sv,par,buff:LongString; fv:Double;
  begin
   Result:=''; param:=''; par:=''; buff:=''; typ:=0; size:=0;
   while (smiui_get_next_obj_param(id,param,typ,size)>0) do begin
    par:=UpperCase(param); sv:='';
    case typ of
     SMI_STRING: begin
      par:=par+'(S)';
      if (size>0) then begin
       SetLength(buff,size);
       if (smiui_get_obj_param_value(id,PChar(buff))>0) then sv:=PChar(buff);
       if (sv<>'') then sv:=smixx_encode(sv,true);
       if (sv<>'') then par:=par+'='+sv;
      end;
     end;
     SMI_INTEGER: begin
      par:=par+'(I)';
      if (size=sizeof(iv)) then begin
       if (smiui_get_obj_param_value(id,@iv)>0) then sv:=IntToStr(iv);
       if (sv<>'') then par:=par+'='+sv;
      end;
     end;
     SMI_FLOAT: begin
      par:=par+'(F)';
      if (size=sizeof(fv)) then begin
       if (smiui_get_obj_param_value(id,@fv)>0) then sv:=Format('%g',[fv]);
       if (sv<>'') then par:=par+'='+sv;
      end;
     end;
     else begin
      par:='';
     end;
    end;
    if (par<>'') then AppendStringListItem(Result,'/',par);
   end;
  end;
 begin
  try
   inc(smiui_sta_count);
   obj:=par; busy_flg:=0; n_actions:=0; objn:=''; state:=''; busy_act:=''; acts:=''; pars:='';
   if (obj=0) or (id=0) then begin ReportError('Bad smiui_statechange_handler params.'); Exit; end;
   smiui_get_name(id,objn); smiui_get_state(id,busy_flg,state,n_actions);
   if (busy_flg<>0) then smiui_get_action_in_progress(id,busy_act);
   if (n_actions>0) then acts:=GetActionsWithParams(id,n_actions);
   pars:=GetObjectParameters(id,objn);
   outs:=Format('@smiui_statechange_handler=%d,%s,%s,%s,%s,%s',[smiui_sta_count,StrDef(objn),StrDef(state),StrDef(busy_act),StrDef(acts),StrDef(pars)]);
   StdOut.Put:=outs;
  except
   on E:Exception do BugReport(E,nil,'smiui_statechange_handler');
  end;
 end;
 //
 // SMI++ SMI message handler.
 // Example:
 // < @smiui_book_smi_message=DEMO
 // > @smiui_smi_message_handler=1,DEMO,SMI INFO DEMO, State Manager started
 //   Word[1]  - Call Count
 //   Word[2]  - Domain Name
 //   Word[3+] - Message comes from SMI core
 //              Message -> SMI (INFO|WARNING|FATAL) <DOMAIN>, <Text Of Message>
 //
 procedure smiui_smi_message_handler(var id:Integer; var par:dim_long_t); cdecl;
 var dom:dim_long_t; domn:TNameBuffer; msg:TOptionsBuffer; outs:LongString;
 begin
  try
   inc(smiui_msg_count);
   dom:=par; StrNameCopy(domn,fsm_name(dom)); msg:='';
   if (smiui_get_smi_message(id,domn,msg)=0) then msg:='';
   outs:=Format('@smiui_smi_message_handler=%d,%s,%s',[smiui_msg_count,StrDef(domn),StrDef(msg)]);
   StdOut.Put:=outs;
  except
   on E:Exception do BugReport(E,nil,'smiui_smi_message_handler');
  end;
 end;
 //
 // SMI User  message handler.
 // Example:
 // > @smiui_book_user_message=DEMO
 // < @smiui_user_message_handler=1,DEMO,-
 // ... REPORT(INFO,"doing ACTIVATE") ...
 // < @smiui_user_message_handler=2,DEMO,SMI INFO DEMO::AUTO_PILOT, doing ACTIVATE
 //   Word[1]  - Call Count
 //   Word[2]  - Domain Name
 //   Word[3+] - Message comes from REPORT(..)
 //              Message -> SMI (INFO|WARNING|FATAL) <OBJECTNAME>, <Text Of Message>
 //
 procedure smiui_user_message_handler(var id:Integer; var par:dim_long_t); cdecl;
 var dom:dim_long_t; domn:TNameBuffer; msg:TOptionsBuffer; outs:LongString;
 begin
  try
   inc(smiui_usr_count);
   dom:=par; StrNameCopy(domn,fsm_name(dom)); msg:='';
   if (smiui_get_user_message(id,domn,msg)=0) then msg:='';
   outs:=Format('@smiui_user_message_handler=%d,%s,%s',[smiui_usr_count,StrDef(domn),StrDef(msg)]);
   StdOut.Put:=outs;
  except
   on E:Exception do BugReport(E,nil,'smiui_user_message_handler');
  end;
 end;
 //
 // Object set change handler
 // Example:
 // > @smiui_book_objectsetchange=DEMO::LOW_SET
 // < @smiui_book_objectsetchange=20,DEMO::LOW_SET
 // < @smiui_objectsetchange_handler=1,DEMO::LOW_SET,DEMO::LOGGER|DEMO::EVT_BUILDER
 //   Word[1]  - Call Count
 //   Word[2]  - Objectset Name
 //   Word[3]  - List of objects in objectset, separated by | char
 //
 procedure smiui_objectsetchange_handler(var id:Integer; var par:dim_long_t); cdecl;
 var setn:TNameBuffer; objsetlist,outs:LongString;
  function GetObjectSetList(id:Integer):LongString;
  var objn:TNameBuffer;
  begin
   Result:='';
   if (smiui_get_first_object_in_set(id,objn)>0) then begin
    AppendStringListItem(Result,'|',objn);
    while (smiui_get_next_object_in_set(id,objn)>0) do begin
     AppendStringListItem(Result,'|',objn);
    end;
   end;
  end;
 begin
  try
   inc(smiui_set_count);
   setn:=''; smiui_get_setname(id,setn); objsetlist:=GetObjectSetList(id);
   outs:=Format('@smiui_objectsetchange_handler=%d,%s,%s',[smiui_set_count,StrDef(setn),StrDef(objsetlist)]);
   StdOut.Put:=outs;
  except
   on E:Exception do BugReport(E,nil,'smiui_objectsetchange_handler');
  end;
 end;
 //
 // Request: @help
 // Reply:   help info
 // Comment: Show help.
 //
 procedure DoHelp(const cmnd,args:LongString);
 begin
  StdOut.Put:=Format('%s',[cmnd]);
  StdOut.Put:='>> Command list:';
  StdOut.Put:='>> @help                              This help.';
  StdOut.Put:='>> @exit=n                            Exit program with code n.';
  StdOut.Put:='>> @errors                            Return error counter.';
  StdOut.Put:='>> @sleep=n                           Sleep n milliseconds.';
  StdOut.Put:='>> @memory                            Return memory status.';
  StdOut.Put:='>> @processpriority=n                 Set process priority:Idle/Low/Normal/High/RealTime.';
  StdOut.Put:='>> @threadpriority=n                  Set thread priority:tpIdle/tpLow/tpNormal/tpHigh/tpTimeCritical.';
  StdOut.Put:='>> @dim_dns_node=n                    Set/get DIM_DNS_NODE environment variable as n.';
  StdOut.Put:='>> @smiui_dns_version                 Read DNS version, DNS node, DIS_DNS@hostname and DNS PID.';
  StdOut.Put:='>> @smiui_current_state=obj           Get current state of SMI obj=DOMAIN::OBJECT.';
  StdOut.Put:='>> @smiui_send_command=obj,c          Send command (c) to SMI obj=DOMAIN::OBJECT (nonblocking).';
  StdOut.Put:='>> @smiui_ep_send_command=obj,c       Send command (c) to SMI obj=DOMAIN::OBJECT (nonblocking,proxy).';
  StdOut.Put:='>> @smiui_send_command_wait=obj,c     Send command (c) to SMI obj=DOMAIN::OBJECT (blocking).';
  StdOut.Put:='>> @smiui_ep_send_command_wait=obj,c  Send command (c) to SMI obj=DOMAIN::OBJECT (blocking,proxy).';
  StdOut.Put:='>> @smiui_change_option=dom,o,v       Change option (o) value (v) of SMI dom=DOMAIN (nonblocking).';
  StdOut.Put:='>> @smiui_change_option_wait=dom,o,v  Change option (o) value (v) of SMI obj=DOMAIN (blocking).';
  StdOut.Put:='>> @smiui_get_options=dom             Get options of SMI dom=DOMAIN.';
  StdOut.Put:='>> @smiui_number_of_objects=dom       Connect domain and get number of objects.';
  StdOut.Put:='>> @smiui_connect_domain=dom          Connect domain and get number of objects.';
  StdOut.Put:='>> @smiui_book_connect_domain=dom     Connect domain and book to view changes.';
  StdOut.Put:='>> @smiui_cancel_connect_domain=dom   Disconnect and unbook domain (dom).';
  StdOut.Put:='>> @smiui_shutdown_domain=dom         Shutdown domain server process.';
  StdOut.Put:='>> @smiui_check_proxy=proxy           Check proxy object.';
  StdOut.Put:='>> @smiui_kill=server                 Shutdown domain or proxy.';
  StdOut.Put:='>> @smiui_errors                      Get/set SMI errors counter.';
  StdOut.Put:='>> @smiui_book_statechange=obj        Connect object and book to view changes.';
  StdOut.Put:='>> @smiui_cancel_statechange=obj      Disconnect object, ignore state changes.';
  StdOut.Put:='>> @smiui_book_smi_message=dom        Connect and book to SMI Alarm message queue.';
  StdOut.Put:='>> @smiui_cancel_smi_message=dom      Disconnect and unbook SMI Alarm message queue.';
  StdOut.Put:='>> @smiui_book_user_message=dom       Connect and book to SMI User message queue (Report instruction).';
  StdOut.Put:='>> @smiui_cancel_user_message=dom     Disconnect and unbook SMI User message queue (Report instruction).';
  StdOut.Put:='>> @smiui_list_domain_objects=dom     List all objects in domain (dom).';
  StdOut.Put:='>> @smiui_list_domain_objectsets=dom  List all objectsets in domain (dom).';
  StdOut.Put:='>> @smiui_book_objectsetchange=s      Book objectset (s) changes.';
  StdOut.Put:='>> @smiui_cancel_objectsetchange=s    Unbook objectset (s) changes.';
  StdOut.Put:='<< Outcome message list:';
  StdOut.Put:='<< @smiui_connect_domain_handler=...  SMI doman connection handler';
  StdOut.Put:='<< @smiui_statechange_handler=...     SMI object state handler';
  StdOut.Put:='<< @smiui_smi_message_handler=...     SMI alarm message handler';
  StdOut.Put:='<< @smiui_user_message_handler=...    SMI user message handler';
  StdOut.Put:='<< @smiui_objectsetchange_handler=... SMI object state handler';
  StdOut.Put:='<< @smiui_report=m                    Report on error with message m.';
 end;
 //
 // Request: @exit
 // Request: @exit=n
 // Reply:   @exit=n
 // Comment: Terminate program with exit code n.
 //
 procedure DoExit(const cmnd,args:LongString);
 begin
  Terminated:=true;
  System.ExitCode:=StrToIntDef(args,0);
  StdOut.Put:=Format('%s=%d',[cmnd,System.ExitCode]);
 end;
 //
 // Request: @errors
 // Request: @errors=n
 // Reply:   @errors=n
 // Comment: Return value n of error counter.
 //
 procedure DoErrors(const cmnd,args:LongString);
 var n:LongInt;
 begin
  if Str2Long(args,n)
  then n:=LockedExchange(StdIoErrorCount,n)
  else n:=LockedGet(StdIoErrorCount);
  StdOut.Put:=Format('%s=%d',[cmnd,n]);
 end;
 //
 // Request: @sleep=n
 // Reply:   @sleep=n
 // Comment: Sleep for n millisec.
 //
 procedure DoSleep(const cmnd,args:LongString);
 var n:LongInt; ms:Cardinal;
 begin
  n:=0; ms:=0;
  if Str2Long(args,n) and (n>0) then begin
   ms:=GetTickCount; Sleep(n);
   ms:=GetTickCount-ms;
  end;
  StdOut.Put:=Format('%s=%d,%u',[cmnd,n,ms]);
 end;
 //
 // Request: @memory
 // Comment: Return AllocMemSize.
 //
 procedure DoMemory(const cmnd,args:LongString);
 begin
  StdOut.Put:=Format('%s=%d',[cmnd,GetAllocMemSize]);
 end;
 //
 // Request: @ProcessPriority
 // Request: @ProcessPriority=n
 // Reply:   @ProcessPriority=n
 // Comment: Set process priority class n=(Idle,Normal,High,RealTime).
 //
 procedure DoProcessPriority(const cmnd,args:LongString);
 var p:DWORD;
 begin
  if not IsEmptyStr(args) then begin
   p:=GetPriorityClassByName(args);
   if p>0 then SetPriorityClass(GetCurrentProcess,p);
  end;
  StdOut.Put:=Format('%s=%s',[cmnd,GetPriorityClassName(GetPriorityClass(GetCurrentProcess))]);
 end;
 //
 // Request: @ThreadPriority
 // Request: @ThreadPriority=n
 // Reply:   @ThreadPriority=n
 // Comment: Set thread priority class n=(tpIdle,tpLowest,tpLower,tpNormal,tpHigher,tpHighest,tpTimeCritical).
 //
 procedure DoThreadPriority(const cmnd,args:LongString);
 var p:TThreadPriority;
 begin
  if not IsEmptyStr(args) then begin
   p:=GetPriorityByName(args);
   StdIn.Priority:=p;
   StdOut.Priority:=p;
   case p of
    tpIdle         : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_IDLE);
    tpLowest       : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_LOWEST);
    tpLower        : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_BELOW_NORMAL);
    tpNormal       : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_NORMAL);
    tpHigher       : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_ABOVE_NORMAL);
    tpHighest      : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_HIGHEST);
    tpTimeCritical : SetThreadPriority(GetCurrentThread,THREAD_PRIORITY_TIME_CRITICAL);
   end;
  end;
  StdOut.Put:=Format('%s=%s',[cmnd,GetPriorityName(StdIn.Priority)]);
 end;
 //
 // Request: @dim_dns_node
 // Request: @dim_dns_node=n
 // Reply:   @dim_dns_node=n
 // Comment: Set/get DIM_DNS_NODE environment variable as n.
 // Example: > @dim_dns_node=localhost
 //          < @dim_dns_node=localhost
 //
 procedure DoDimDnsNode(const cmnd,args:LongString);
 var node:LongString; buff:TNameBuffer; conn:Boolean;
 begin
  node:=ExtractWord(1,args,ScanSpaces);
  conn:=not IsEmptyStr(node);
  if SameText(node,'.') then node:='localhost';
  if (node<>'') then SetEnv('DIM_DNS_NODE',node);
  if (node<>'') then dim_set_dns_node(PChar(node));
  if (dim_get_dns_node(buff)>0) then node:=buff else node:=GetEnv('DIM_DNS_NODE');
  dns_curnode:=node; if conn then dns_connect(true);
  StdOut.Put:=Format('%s=%s',[cmnd,node]);
 end;
 //
 // Request: @smiui_dns_version
 // Reply:   @smiui_dns_version=version,dnsnode,pidhost
 // Comment: Get DIM DNS verion nimber, DNS node, DIS_DNS@hostname, DNS process PID.
 // Example: > @smiui_dns_version
 //          < @smiui_dns_version=2032,localhost,4023,DIS_DNS@ak-wxp-vm
 //
 procedure DoSmiuiDnsVersion(const cmnd,args:LongString);
 begin
  StdOut.Put:=Format('%s=%d,%s,%s,%d',[cmnd,dns_version,StrDef(dns_curnode),StrDef(dns_task_id),dns_proc_id]);
 end;
 //
 // Request: @smiui_current_state=obj
 // Reply:   @smiui_current_state=status,obj,state,busy
 // Comment: Read current state of SMI object obj=DOMAIN::OBJECT.
 // Example: > @smiui_current_state=DEMO::LOGGER
 //          < @smiui_current_state=1,DEMO::LOGGER,NOT_LOGGING,-
 //
 procedure DoSmiuiCurrentState(const cmnd,args:LongString);
 var obj,state,busy:TNameBuffer; n:Integer;
 begin
  n:=0; obj:=''; state:=''; busy:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(obj,ExtractWord(1,args,ScanSpaces));
   n:=smiui_current_state(obj,state,busy);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s,%s',[cmnd,n,StrDef(obj),StrDef(state),StrDef(busy)]);
 end;
 //
 // Request: @smiui_send_command=obj,command
 // Reply:   @smiui_send_command=status,obj,command
 // Comment: Sent command to SMI object obj=DOMAIN::OBJECT (nonblocking).
 // Note:    command is a new state of object
 // Example: > @smiui_send_command=DEMO::LOGGER,LOG
 //          < @smiui_send_command=1,DEMO::LOGGER,LOG
 //
 procedure DoSmiuiSendCommand(const cmnd,args:LongString);
 var obj,command:TNameBuffer; n:Integer;
 begin
  n:=0; obj:=''; command:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(obj,ExtractWord(1,args,ScanSpaces));
   StrNameCopy(command,Trim(SkipWords(1,args,ScanSpaces)));
   n:=smiui_send_command(obj,command);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,n,StrDef(obj),StrDef(command)]);
 end;
 //
 // Request: @smiui_ep_send_command=obj,command
 // Reply:   @smiui_ep_send_command=status
 // Comment: Sent command to SMI proxy object obj=DOMAIN::OBJECT (nonblocking,proxy).
 //
 procedure DoSmiuiEpSendCommand(const cmnd,args:LongString);
 var obj,command:TNameBuffer; n:Integer;
 begin
  n:=0; obj:=''; command:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(obj,ExtractWord(1,args,ScanSpaces));
   StrNameCopy(command,Trim(SkipWords(1,args,ScanSpaces)));
   n:=smiui_ep_send_command(obj,command);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,n,StrDef(obj),StrDef(command)]);
 end;
 //
 // Request: @smiui_send_command_wait=obj,command
 // Reply:   @smiui_send_command_wait=status,obj,command
 // Comment: Sent command to SMI object obj=DOMAIN::OBJECT (blocking).
 //
 procedure DoSmiuiSendCommandWait(const cmnd,args:LongString);
 var obj,command:TNameBuffer; n:Integer;
 begin
  n:=0; obj:=''; command:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(obj,ExtractWord(1,args,ScanSpaces));
   StrNameCopy(command,Trim(SkipWords(1,args,ScanSpaces)));
   n:=smiui_send_command_wait(obj,command);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,n,StrDef(obj),StrDef(command)]);
 end;
 //
 // Request: @smiui_ep_send_command_wait=obj,command
 // Reply:   @smiui_ep_send_command_wait=status,obj,command
 // Comment: Sent command to SMI proxy object obj=DOMAIN::OBJECT (blocking,proxy).
 //
 procedure DoSmiuiEpSendCommandWait(const cmnd,args:LongString);
 var obj,command:TNameBuffer; n:Integer;
 begin
  n:=0; obj:=''; command:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(obj,ExtractWord(1,args,ScanSpaces));
   StrNameCopy(command,Trim(SkipWords(1,args,ScanSpaces)));
   n:=smiui_ep_send_command_wait(obj,command);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,n,StrDef(obj),StrDef(command)]);
 end;
 //
 // Request: @smiui_change_option=dom,option,value
 // Reply:   @smiui_change_option=status,dom,option,value
 // Comment: Change option value of SMI dom=DOMAIN (nonblocking).
 // Example: > @smiui_change_option=demo,d,8
 //          < Sending d/8 to SMI/DEMO/OPTIONS/CMD
 //          < @smiui_change_option=1,demo,d,8
 //
 procedure DoSmiuiChangeOption(const cmnd,args:LongString);
 var dom,option,value:TNameBuffer; n:Integer;
 begin
  n:=0; dom:=''; option:=''; value:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   StrNameCopy(option,ExtractWord(2,args,ScanSpaces));
   StrNameCopy(value,Trim(SkipWords(2,args,ScanSpaces)));
   n:=smiui_change_option(dom,option,value);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s,%s',[cmnd,n,StrDef(dom),StrDef(option),StrDef(value)]);
 end;
 //
 // Request: @smiui_change_option_wait=dom,option,value
 // Reply:   @smiui_change_option_wait=status,dom,option,value
 // Comment: Change option value of SMI dom=DOMAIN (blocking).
 //
 procedure DoSmiuiChangeOptionWait(const cmnd,args:LongString);
 var dom,option,value:TNameBuffer; n:Integer;
 begin
  n:=0; dom:=''; option:=''; value:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   StrNameCopy(option,ExtractWord(2,args,ScanSpaces));
   StrNameCopy(value,Trim(SkipWords(2,args,ScanSpaces)));
   n:=smiui_change_option_wait(dom,option,value);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s,%s',[cmnd,n,StrDef(dom),StrDef(option),StrDef(value)]);
 end;
 //
 // Request: @smiui_get_options=dom
 // Reply:   @smiui_get_options=status,dom,options
 // Comment: Get options of SMI dom=DOMAIN.
 // Example: > @smiui_get_options=DEMO
 //          < @smiui_get_options=1,DEMO,d/INT/9/Diagnostic Level|u/BOOL/0/Unlocked IFs|t/BOOL/1/New time format|...
 //
 procedure DoSmiuiGetOptions(const cmnd,args:LongString);
 var dom:TNameBuffer; options:TOptionsBuffer; n:Integer;
 begin
  n:=0; dom:=''; options:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   n:=smiui_get_options(dom,options);
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,n,StrDef(dom),StrDef(options)]);
 end;
 //
 // Request: @smiui_number_of_objects=dom
 // Reply:   @smiui_number_of_objects=nobjs,dom
 // Comment: Connect domain and get number of objects in SMI dom=DOMAIN.
 // Example: > @smiui_number_of_objects=DEMO
 //          < @smiui_number_of_objects=6,DEMO
 //
 procedure DoSmiuiNumberOfObjects(const cmnd,args:LongString);
 var dom:TNameBuffer; n:Integer;
 begin
  n:=0; dom:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   n:=smiui_number_of_objects(dom);
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(dom)]);
 end;
 //
 // Request: @smiui_connect_domain=dom
 // Reply:   @smiui_connect_domain=nobjs,dom
 // Comment: Connect domain and get number of objects in SMI dom=DOMAIN.
 //
 procedure DoSmiuiConnectDomain(const cmnd,args:LongString);
 var dom:TNameBuffer; n:Integer;
 begin
  n:=0; dom:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   n:=smiui_connect_domain(dom);
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(dom)]);
 end;
 //
 // Request: @smiui_book_connect_domain=dom
 //      or: @smiui_book_connect_domain=dom,auto
 // Reply:   @smiui_book_connect_domain=status,dom
 // Comment: Connect domain and book to view changes.
 //          If uses 'auto' flag, auto book all objects in domain.
 // Example: > @smiui_book_connect_domain=DEMO
 //          < @smiui_book_connect_domain=6,DEMO
 //          < @smiui_domain_handler=1,6,DEMO,DEMO::&DOMAIN,DEMO::AUTO_PILOT,DEMO::RUN_TYPE,DEMO::RUN,DEMO::LOGGER/ASSOCIATED,DEMO::EVT_BUILDER/ASSOCIATED
 //
 procedure DoSmiuiBookConnectDomain(const cmnd,args:LongString);
 var dom,nobj,auto:Integer; domn:TNameBuffer;
 begin
  nobj:=0; dom:=0; domn:='';
  if not IsEmptyStr(StrNameCopy(domn,ExtractWord(1,args,ScanSpaces))) then begin
   dom:=fsm_find(fsm,fsm_type_domain,domn);
   if (dom=0) then dom:=fsm_add(fsm,fsm_type_domain,domn);
   if (dom=0) then ReportError('Could not init domain '+domn) else begin
    smiui_cancel_connect_domain(domn);
    auto:=Ord(SameText(ExtractWord(2,args,ScanSpaces),'auto'));
    fsm_add_iparam(dom,par_opt_auto_booking,auto);
    fsm_add_iparam(dom,par_number_of_objects,0);
    fsm_add_sparam(dom,par_dim_dns_node,GetEnv('DIM_DNS_NODE'));
    nobj:=smiui_book_connect_domain(domn,smiui_connect_domain_handler,dom);
    fsm_put_iparam(dom,par_number_of_objects,nobj);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,nobj,StrDef(fsm_name(dom))]);
 end;
 //
 // Request: @smiui_cancel_connect_domain=dom
 // Reply:   @smiui_cancel_connect_domain=status,dom
 // Comment: Disconnect and unbook domain.
 //
 procedure DoSmiuiCancelConnectDomain(const cmnd,args:LongString);
 var dom:TNameBuffer; n:Integer;
 begin
  n:=0; dom:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   n:=smiui_cancel_connect_domain(dom);
   UnbookDomain(dom);
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(dom)]);
 end;
 //
 // Request: @smiui_shutdown_domain=dom
 // Reply:   @smiui_shutdown_domain=status,dom
 // Comment: Shutdown SMI domain process.
 // Example: > @smiui_shutdown_domain=DEMO
 //          < @smiui_shutdown_domain=1,DEMO
 //
 procedure DoSmiuiShutdownDomain(const cmnd,args:LongString);
 var dom:TNameBuffer; n:Integer;
 begin
  n:=0; dom:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   n:=smiui_shutdown_domain(dom);
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(dom)]);
 end;
 //
 // Request: @smiui_check_proxy=proxy
 // Reply:   @smiui_check_proxy=status,proxy
 // Comment: Check status of proxy.
 // Example: > @smiui_check_proxy=DEMO::LOGGER
 //          < @smiui_check_proxy=1,DEMO::LOGGER
 //
 procedure DoSmiuiCheckProxy(const cmnd,args:LongString);
 var proxy:TNameBuffer; n:Integer;
 begin
  n:=0; proxy:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(proxy,ExtractWord(1,args,ScanSpaces));
   n:=smiui_check_proxy(proxy);
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(proxy)]);
 end;
 //
 // Request: @smiui_kill=server
 // Reply:   @smiui_kill=status,server
 // Comment: Shutdown domain or proxy.
 // Example: > @smiui_kill=DEMO
 //          < @smiui_kill=1,DEMO
 //
 procedure DoSmiuiKill(const cmnd,args:LongString);
 var server:TNameBuffer; n:Integer;
 begin
  n:=0; server:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(server,ExtractWord(1,args,ScanSpaces));
   n:=smiui_kill(server);
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(server)]);
 end;
 //
 // Request: @smiui_book_statechange=obj
 // Reply:   @smiui_book_statechange=id,obj
 // Comment: Connect object and book to view state changes.
 // Example: > @smiui_book_statechange=DEMO::LOGGER
 //          > @smiui_book_statechange=DEMO::EVT_BUILDER
 //          < @smiui_book_statechange=18,DEMO::LOGGER
 //          < @smiui_book_statechange=22,DEMO::EVT_BUILDER
 //          < @smiui_statechange_handler=1,DEMO::LOGGER,LOGGING,-,NOLOG|X_OPEN_FILE,-
 //          < @smiui_statechange_handler=2,DEMO::EVT_BUILDER,ERROR,-,RECOVER,NUMBER_T(I)=145/NUMBER_P(I)=28
 //
 procedure DoSmiuiBookStateChange(const cmnd,args:LongString);
 var objn:TNameBuffer; obj,id:Integer;
 begin
  id:=0; objn:='';
  if not IsEmptyStr(StrNameCopy(objn,ExtractWord(1,args,ScanSpaces))) then begin
   obj:=fsm_find(fsm,fsm_type_object,objn);
   if (obj=0) then obj:=fsm_add(fsm,fsm_type_object,objn);
   if (obj=0) then ReportError('Could not init object '+objn) else begin
    id:=fsm_ask_iparam(obj,par_book_statechange_id);
    if (id<>0) then smiui_cancel_statechange(id);
    fsm_add_iparam(obj,par_book_statechange_id,0);
    id:=smiui_book_statechange(objn,smiui_statechange_handler,obj);
    fsm_put_iparam(obj,par_book_statechange_id,id);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,id,StrDef(objn)]);
 end;
 //
 // Request: @smiui_cancel_statechange=obj
 // Reply:   @smiui_cancel_statechange=status,obj
 // Comment: Disconnect and unbook object.
 //
 procedure DoSmiuiCancelStateChange(const cmnd,args:LongString);
 var objn:TNameBuffer; n,obj,id:Integer;
 begin
  n:=0; objn:='';
  if not IsEmptyStr(StrNameCopy(objn,ExtractWord(1,args,ScanSpaces))) then begin
   obj:=fsm_find(fsm,fsm_type_object,objn);
   id:=fsm_ask_iparam(obj,par_book_statechange_id);
   if (id<>0)
   then n:=smiui_cancel_statechange(id)
   else ReportInfo(Format('Object %s is not booked.',[objn]));
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(objn)]);
 end;
 //
 // Request: @smiui_book_smi_message=dom
 // Reply:   @smiui_book_smi_message=id,dom
 // Comment: Connect domain (dom) SMI message queue.
 // Example: > @smiui_book_smi_message=DEMO
 //          < @smiui_book_smi_message=1,DEMO
 //          < @smiui_smi_message_handler=1,DEMO,SMI Message
 //
 procedure DoSmiuiBookSmiMessage(const cmnd,args:LongString);
 var dom,id:Integer; domn:TNameBuffer;
 begin
  id:=0; domn:='';
  if not IsEmptyStr(StrNameCopy(domn,ExtractWord(1,args,ScanSpaces))) then begin
   dom:=fsm_find(fsm,fsm_type_domain,domn);
   if (dom=0) then dom:=fsm_add(fsm,fsm_type_domain,domn);
   if (dom=0) then ReportError('Could not init domain '+domn) else begin
    id:=fsm_ask_iparam(dom,par_book_smi_message_id);
    if (id<>0) then smiui_cancel_smi_message(id);
    fsm_add_iparam(dom,par_book_smi_message_id,0);
    id:=smiui_book_smi_message(domn,smiui_smi_message_handler,dom);
    fsm_put_iparam(dom,par_book_smi_message_id,id);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,id,StrDef(domn)]);
 end;
 //
 // Request: @smiui_cancel_smi_message=dom
 // Reply:   @smiui_cancel_smi_message=status,dom
 // Comment: Disconnect and unbook smi message.
 //
 procedure DoSmiuiCancelSmiMessage(const cmnd,args:LongString);
 var domn:TNameBuffer; dom,id,n:Integer;
 begin
  n:=0; domn:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(domn,ExtractWord(1,args,ScanSpaces));
   dom:=fsm_find(fsm,fsm_type_domain,domn);
   if (dom=0) then dom:=fsm_add(fsm,fsm_type_domain,domn);
   if (dom=0) then ReportError('Could not init domain '+domn) else begin
    id:=fsm_ask_iparam(dom,par_book_smi_message_id);
    if (id<>0) then n:=smiui_cancel_smi_message(id);
    fsm_add_iparam(dom,par_book_smi_message_id,0);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(domn)]);
 end;
 //
 // Request: @smiui_book_user_message=dom
 // Reply:   @smiui_book_user_message=id,dom
 // Comment: Connect domain (dom) User message queue.
 // Example: > @smiui_book_user_message=DEMO
 //          < @smiui_book_user_message=1,DEMO
 //          < @smiui_user_message_handler=1,DEMO,User Message
 //
 procedure DoSmiuiBookUserMessage(const cmnd,args:LongString);
 var dom,id:Integer; domn:TNameBuffer;
 begin
  id:=0; domn:='';
  if not IsEmptyStr(StrNameCopy(domn,ExtractWord(1,args,ScanSpaces))) then begin
   dom:=fsm_find(fsm,fsm_type_domain,domn);
   if (dom=0) then dom:=fsm_add(fsm,fsm_type_domain,domn);
   if (dom=0) then ReportError('Could not init domain '+domn) else begin
    id:=fsm_ask_iparam(dom,par_book_user_message_id);
    if (id<>0) then smiui_cancel_user_message(id);
    fsm_add_iparam(dom,par_book_user_message_id,0);
    id:=smiui_book_user_message(domn,smiui_user_message_handler,dom);
    fsm_put_iparam(dom,par_book_user_message_id,id);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,id,StrDef(domn)]);
 end;
 //
 // Request: @smiui_cancel_user_message=dom
 // Reply:   @smiui_cancel_user_message=status,dom
 // Comment: Disconnect and unbook user message.
 //
 procedure DoSmiuiCancelUserMessage(const cmnd,args:LongString);
 var domn:TNameBuffer; dom,id,n:Integer;
 begin
  n:=0; domn:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(domn,ExtractWord(1,args,ScanSpaces));
   dom:=fsm_find(fsm,fsm_type_domain,domn);
   if (dom=0) then dom:=fsm_add(fsm,fsm_type_domain,domn);
   if (dom=0) then ReportError('Could not init domain '+domn) else begin
    id:=fsm_ask_iparam(dom,par_book_user_message_id);
    if (id<>0) then n:=smiui_cancel_user_message(id);
    fsm_add_iparam(dom,par_book_user_message_id,0);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(domn)]);
 end;
 //
 // Request: @smiui_list_domain_objects=dom
 // Reply:   @smiui_list_domain_objects=n,dom,obj1|obj2|..objn
 // Comment: Connect domain (dom) and return number of objects (n) in domain (dom)
 //          and | separated list of objects in domain with optional /attributes
 // Note:    @smiui_list_domain_objects conflicts with @smiui_book_connect_domain, use only one of them.
 // Example: > @smiui_list_domain_objects=DEMO
 //          < @smiui_list_domain_objects=6,DEMO,DEMO::&DOMAIN|DEMO::AUTO_PILOT|DEMO::RUN_TYPE|DEMO::RUN|DEMO::LOGGER/ASSOCIATED|DEMO::EVT_BUILDER/ASSOCIATED
 //
 procedure DoSmiuiListDomainObjects(const cmnd,args:LongString);
 var dom,obj,att:TNameBuffer; nobj:Integer; objs:LongString;
 begin
  nobj:=0; dom:=''; objs:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(dom,ExtractWord(1,args,ScanSpaces));
   nobj:=smiui_connect_domain(dom);
   if (nobj>0) then
   while (smiui_get_next_object(dom,obj)>0) do begin
    if (objs<>'') then objs:=objs+'|'+obj else objs:=objs+obj;
    while (smiui_get_next_attribute(obj,att)>0) do objs:=objs+'/'+att;
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,nobj,dom,objs]);
 end;
 //
 // Request: @smiui_list_domain_objectsets=dom
 //      or: @smiui_list_domain_objectsets=dom,auto
 // Reply:   @smiui_list_domain_objectsets=num,dom,objset1|objset2|...
 // Comment: Get list of domain objectsets in domain (dom).
 // Example: > @smiui_list_domain_objectsets=DEMO
 //          < @smiui_list_domain_objectsets=2,DEMO,DEMO::LOW_SET|DEMO::TOP_SET
 //
 procedure DoSmiuiListDomainObjectSets(const cmnd,args:LongString);
 var domn,objsetn:TNameBuffer; num,auto:Integer; objsetlist:LongString;
 begin
  num:=0; domn:=''; objsetlist:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(domn,ExtractWord(1,args,ScanSpaces));
   auto:=Ord(SameText(ExtractWord(2,args,ScanSpaces),'auto'));
   num:=smiui_number_of_objectsets(domn);
   if (num>0) then
   while (smiui_get_next_objectset(domn,objsetn)>0) do begin
    if (auto<>0) then StdIn.Put:='@smiui_book_objectsetchange='+objsetn;
    AppendStringListItem(objsetlist,'|',objsetn);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s,%s',[cmnd,num,StrDef(domn),StrDef(objsetlist)]);
 end;
 //
 // Request: @smiui_book_objectsetchange=objset
 // Reply:   @smiui_book_objectsetchange=id,objset
 // Comment: Connect and book objectset (objset) changes.
 // Example: > @smiui_book_objectsetchange=DEMO::LOW_SET
 //          < @smiui_book_objectsetchange=1,DEMO::LOW_SET
 //          < @smiui_objectset_handler=1,DEMO::LOW_SET,DEMO::LOGGER|DEMO::EVT_BUILDER
 //
 procedure DoSmiuiBookObjectsetChange(const cmnd,args:LongString);
 var objset,id:Integer; objsetn:TNameBuffer;
 begin
  id:=0; objsetn:='';
  if not IsEmptyStr(StrNameCopy(objsetn,ExtractWord(1,args,ScanSpaces))) then begin
   objset:=fsm_find(fsm,fsm_type_objectset,objsetn);
   if (objset=0) then objset:=fsm_add(fsm,fsm_type_objectset,objsetn);
   if (objset=0) then ReportError('Could not init objectset '+objsetn) else begin
    id:=fsm_ask_iparam(objset,par_book_objectsetchange_id);
    if (id<>0) then smiui_cancel_objectsetchange(id);
    fsm_add_iparam(objset,par_book_objectsetchange_id,0);
    id:=smiui_book_objectsetchange(objsetn,smiui_objectsetchange_handler,objset);
    fsm_put_iparam(objset,par_book_objectsetchange_id,id);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,id,StrDef(objsetn)]);
 end;
 //
 // Request: @smiui_cancel_objectsetchange=objset
 // Reply:   @smiui_cancel_objectsetchange=status,objset
 // Comment: Disconnect and unbook objectset change.
 //
 procedure DoSmiuiCancelObjectsetChange(const cmnd,args:LongString);
 var objset,id,n:Integer; objsetn:TNameBuffer;
 begin
  n:=0; objsetn:='';
  if not IsEmptyStr(args) then begin
   StrNameCopy(objsetn,ExtractWord(1,args,ScanSpaces));
   objset:=fsm_find(fsm,fsm_type_objectset,objsetn);
   if (objset=0) then objset:=fsm_add(fsm,fsm_type_objectset,objsetn);
   if (objset=0) then ReportError('Could not init objectset '+objsetn) else begin
    id:=fsm_ask_iparam(objset,par_book_objectsetchange_id);
    if (id<>0) then n:=smiui_cancel_objectsetchange(id);
    fsm_add_iparam(objset,par_book_objectsetchange_id,0);
   end;
  end;
  StdOut.Put:=Format('%s=%d,%s',[cmnd,n,StrDef(objsetn)]);
 end;
 //
 // Request: @smiui_errors
 // Request: @smiui_errors=n
 // Reply:   @smiui_errors=n
 // Comment: Return value n of error counter.
 //
 procedure DoSmiuiErrors(const cmnd,args:LongString);
 var n:LongInt;
 begin
  if Str2Long(args,n)
  then n:=LockedExchange(smiui_err_count,n)
  else n:=smiui_err_count;
  StdOut.Put:=Format('%s=%d',[cmnd,n]);
 end;
 //
 // Parse Command Line arguments and options.
 //
 procedure ParseCmdLine;
 var i,parnum:Integer; arg,opt:LongString; isopt,opt_auto:Boolean;
  procedure HandleCmdLineParam(parnum:Integer; arg:LongString);
  begin
  end;
  function GetOptAuto:LongString;
  begin
   if opt_auto then Result:=',auto' else Result:='';
  end;
 begin
  arg:=''; opt:='';
  parnum:=0; isopt:=true; opt_auto:=false;
  for i:=1 to ParamCount do begin
   arg:=ParamStr(i);
   if IsOption(arg) and isopt and (opt='') then begin
    if IsOption(arg,'--') then begin // assume next arguments are parameters
     isopt:=false;
     continue;
    end;
    if IsOption(arg,'-?') or IsOption(arg,'-h','--help') then begin
     Terminated:=true;
     PrintUsageInfo;
     Break;
    end;
    if IsOption(arg,'-verb','--verb') or IsOption(arg,'-verbose','--verbose') then begin
     opt_verbose:=true;
     continue;
    end;
    if IsOption(arg,'-auto','--auto') then begin
     opt_auto:=true;
     continue;
    end;
    if IsOption(arg,'-dns','--dns') then begin
     opt:=arg;
     continue;
    end;
    if IsOption(arg,'-dom','--dom') then begin
     opt:=arg;
     continue;
    end;
    if IsOption(arg,'-sleep','--sleep') then begin
     opt:=arg;
     continue;
    end;
   end else begin
    if (opt='') then begin
     inc(parnum); // arg is parameter
     HandleCmdLineParam(parnum,arg);
     continue;
    end;
    if IsOption(opt,'-dns','--dns') then begin
     if (arg<>'') then StdIn.Put:='@dim_dns_node='+arg;
    end;
    if IsOption(opt,'-dom','--dom') then begin
     if (arg<>'') then StdIn.Put:='@smiui_book_connect_domain='+arg+GetOptAuto;
    end;
    if IsOption(opt,'-sleep','--sleep') then begin
     if (arg<>'') then StdIn.Put:='@sleep='+arg;
    end;
    opt:='';
   end;
  end;
 end;
 //
 // This callback handles unrecognized commands.
 //
 procedure DoSpecificCommands(const args:LongString);
 begin
  if Length(args)>0 then
  ReportError(Format('Could not recognize "%s"',[args]));
 end;
 //
 // Get EXE file version info.
 //
 function GetVersionInfo(const Name:LongString):LongString;
 begin
  Result:=CookieScan(GetFileVersionInfoAsText(ProgName),Name);
 end;
 function DotEnding(const S:LongString):LongString;
 const dot='.';
 begin
  Result:=S;
  if (Result<>'') then if (StrFetch(Result,Length(Result))<>dot) then Result:=Result+dot;
 end;
 procedure PrintVersionInfo(const Fallback:LongString);
 begin
  if not IsEmptyStr(GetVersionInfo('ProductName')) then begin
   StdOut.Put:=DotEnding(GetVersionInfo('ProductName')+' version '+GetVersionInfo('ProductVersion'));
   StdOut.Put:=DotEnding(GetVersionInfo('FileDescription'));
   StdOut.Put:=DotEnding(GetVersionInfo('LegalCopyright'));
  end else begin
   StdOut.Put:=Fallback;
  end;
 end;
 //
 // Application specific initialization.
 //
 procedure SpecificInitialization;
 begin
  //
  // Version. Copyright. Greetings.
  //
  UseRunCommandEx(True);
  SystemEchoProcedure:=StdOutEcho;
  PrintVersionInfo('SMI UI server for CRW-DAQ.');
  StdOut.Put:='Call smiUiSrv --help to get help on cmdline params.';
  StdOut.Put:=Format('PID %d started.',[GetCurrentProcessId]);
  //
  // Check SmiuiRtlLibrary
  //
  if not LoadSmiuiRtlLibrary then begin
   StdOut.Put:='Could not load '+SmiuiRtlLibraryName+' library.';
   StdOut.Put:=SmiuiRtlLibrary.Report; Sleep(1000);
   raise ESmiuiRtlLibrary.Create('Could not load '+SmiuiRtlLibraryName);
  end;
  //
  // Register user commands coming from StdIn.
  //
  SystemEchoProcedure:=StdOutEcho;
  StdIn.SpecHandler:=DoSpecificCommands;
  StdIn.AddCommand('@Help',                         DoHelp);
  StdIn.AddCommand('@Exit',                         DoExit);
  StdIn.AddCommand('@Errors',                       DoErrors);
  StdIn.AddCommand('@Sleep',                        DoSleep);
  StdIn.AddCommand('@Memory',                       DoMemory);
  StdIn.AddCommand('@ProcessPriority',              DoProcessPriority);
  StdIn.AddCommand('@ThreadPriority',               DoThreadPriority);
  StdIn.AddCommand('@dim_dns_node',                 DoDimDnsNode);
  StdIn.AddCommand('@smiui_dns_version',            DoSmiuiDnsVersion);
  StdIn.AddCommand('@smiui_current_state',          DoSmiuiCurrentState);
  StdIn.AddCommand('@smiui_send_command',           DoSmiuiSendCommand);
  StdIn.AddCommand('@smiui_ep_send_command',        DoSmiuiEpSendCommand);
  StdIn.AddCommand('@smiui_send_command_wait',      DoSmiuiSendCommandWait);
  StdIn.AddCommand('@smiui_ep_send_command_wait',   DoSmiuiEpSendCommandWait);
  StdIn.AddCommand('@smiui_change_option',          DoSmiuiChangeOption);
  StdIn.AddCommand('@smiui_change_option_wait',     DoSmiuiChangeOptionWait);
  StdIn.AddCommand('@smiui_get_options',            DoSmiuiGetOptions);
  StdIn.AddCommand('@smiui_number_of_objects',      DoSmiuiNumberOfObjects);
  StdIn.AddCommand('@smiui_connect_domain',         DoSmiuiConnectDomain);
  StdIn.AddCommand('@smiui_book_connect_domain',    DoSmiuiBookConnectDomain);
  StdIn.AddCommand('@smiui_cancel_connect_domain',  DoSmiuiCancelConnectDomain);
  StdIn.AddCommand('@smiui_shutdown_domain',        DoSmiuiShutdownDomain);
  StdIn.AddCommand('@smiui_check_proxy',            DoSmiuiCheckProxy);
  StdIn.AddCommand('@smiui_kill',                   DoSmiuiKill);
  StdIn.AddCommand('@smiui_book_statechange',       DoSmiuiBookStateChange);
  StdIn.AddCommand('@smiui_cancel_statechange',     DoSmiuiCancelStateChange);
  StdIn.AddCommand('@smiui_book_smi_message',       DoSmiuiBookSmiMessage);
  StdIn.AddCommand('@smiui_cancel_smi_message',     DoSmiuiCancelSmiMessage);
  StdIn.AddCommand('@smiui_book_user_message',      DoSmiuiBookUserMessage);
  StdIn.AddCommand('@smiui_cancel_user_message',    DoSmiuiCancelUserMessage);
  StdIn.AddCommand('@smiui_book_objectsetchange',   DoSmiuiBookObjectsetChange);
  StdIn.AddCommand('@smiui_cancel_objectsetchange', DoSmiuiCancelObjectsetChange);
  StdIn.AddCommand('@smiui_errors',                 DoSmiuiErrors);
  StdIn.AddCommand('@smiui_list_domain_objects',    DoSmiuiListDomainObjects);
  StdIn.AddCommand('@smiui_list_domain_objectsets', DoSmiuiListDomainObjectSets);
  //
  // Initialize FSM Manager.
  //
  InitFsm;
  //
  // Parse command line
  //
  ParseCmdLine;
 end;
 //
 // Application specific finalization.
 //
 procedure SpecificFinalization;
 begin
  FreeFsm;
 end;
 //
 // Application specific polling.
 //
 procedure SpecificPolling;
 const LastTicks:QWord=0;
 var CurrTicks:QWord;
 begin
  CurrTicks:=GetTickCount64;
  if (CurrTicks>LastTicks+1000) then begin
   TTask.PollDetachedPids;
   LastTicks:=CurrTicks;
  end;
  if BecameZombie(FILE_TYPE_PIPE,1000) then StdIn.Put:='@Exit';
 end;
 //
 // Main program
 //
begin
 try
  try
   SpecificInitialization;
   while not Terminated do begin
    while (StdIn.Count>0) do StdIn.Process(StdIn.Get);
    SpecificPolling;
    Sleep(TPolling.DefPollPeriod);
   end;
  finally
   SpecificFinalization;
  end;
 except
  on E:Exception do StdOut.Put:=E.Message;
 end;
 Sleep(100);
 if BecameZombie(FILE_TYPE_PIPE) then ExitCode:=1;
end.

//////////////
// END OF FILE
//////////////

