////////////////////////////////////////////////////////////////////////////////
// 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:                                                                   //
// Serial terminal like cat.                                                  //
// Trransmit data from stdio to serial port and from serial port to stdout.   //
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// History:                                                                   //
// 20230708 - Modified for FPC (A.K.)                                         //
////////////////////////////////////////////////////////////////////////////////

program sercat;
{$I _crw_sysdef.inc}

{$I _crw_sysmode.inc}

{$IFNDEF FPC}{$APPTYPE CONSOLE}{$ENDIF}

{$R *.res}

uses
 //////////////////////////////////////////////////////
 {$I _crw_uses_first.inc} // NB: MUST BE FIRST USES !!!
 //////////////////////////////////////////////////////
 {$IFDEF UNIX} unix, baseunix, termio,  {$ENDIF}
 sysutils, classes, math,
 _crw_alloc, _crw_str, _crw_crypt, _crw_bsencode, _crw_fio,
 _crw_rtc, _crw_serio;

var
 serPort    : TSerialHandle = INVALID_HANDLE_VALUE; // Serial port handle
 PollPeriod : Integer       = 4;                    // Polling period, ms
 Verbose    : Boolean       = false;                // Print verbosely ?
 RawInMode  : Boolean       = false;                // StdIn raw mode  ?
 HexInMode  : Boolean       = false;                // StdIn HEX mode  ?
 HexOutMode : Boolean       = false;                // StdOut HEX mode ?
 EscInMode  : Boolean       = false;                // StdIn ESC mode  ?
 EscOutMode : Boolean       = false;                // StdOut ESC mode ?
 NonBlockIn : Boolean       = true;                 // StdIn non-block ?
 BlkPipeMod : Boolean       = false;                // Pipe block mode ?
 Terminated : Boolean       = false;                // Loop terminated ?

type EWriteError=class(Exception);
type EBadArgument=class(Exception);

const stdin  = 0;
const stdout = 1;
const stderr = 2;

procedure Print(n:Integer; const S:LongString);
var h:THandle;
begin
 if Length(S)>0 then begin
  case n of
   stdout: h:=GetStdHandle(STD_OUTPUT_HANDLE);
   stderr: h:=GetStdHandle(STD_ERROR_HANDLE);
   else    Exit;
  end;
  if (FileWrite(h,PChar(S)^,Length(S))<0)
  then Raise EWriteError.Create('Fail write stream '+IntToStr(h));
 end;
end;

procedure Echo(s:LongString);
begin
 Print(stdout,s+EOL);
end;

procedure BugReport(E:Exception; Sender:TObject=nil; Note:LongString='');
var s:LongString;
begin
 if Assigned(E) then begin
  s:=Format('%s %s',[E.ClassName,E.Message]);
  Print(stderr,s+EOL);
 end;
 ExitCode:=1;
end;

procedure Fatal(Msg:LongString);
begin
 Print(stderr,Msg+EOL);
 serClose(serPort);
 Halt(1);
end;

function BaseName:LongString;
begin
 Result:=ChangeFileExt(ExtractFileName(ParamStr(0)),'');
end;

function VersionInfo:LongString;
const Info:LongString='';
begin
 if (Info='') then Info:=GetFileVersionInfoAsText(ProgName);
 Result:=Info;
end;

function ProductVersion:LongString;
begin
 Result:=CookieScan(VersionInfo,'ProductVersion');
end;

function LegalCopyright:LongString;
begin
 Result:=CookieScan(VersionInfo,'LegalCopyright');
end;

procedure PrintVersion;
begin
 Echo(BaseName+' '+ProductVersion);
end;

procedure PrintHelp;
var exe:LongString;
begin
 Exe:=BaseName;
 Echo(exe+' '+Trim(ProductVersion+EOL+LegalCopyright));
 Echo(exe+' is "serial cat" utility, i.e. simple serial port terminal.');
 Echo('Usage:');
 Echo(' '+exe+' [-options] [parameters]');
 Echo(' with specified options [-option] and/or next positional [parameters]:');
 Echo('  n - serial port name like COM1 or /dev/ttyS0');
 Echo('  b - baudrate (bits per second),      default = 9600');
 Echo('  d - databits (bytesize) = {5,6,7,8}, default = 8');
 Echo('  p - parity = {NONE,ODD,EVEN},        default = NONE');
 Echo('  s - stop bits = {1,2},               default = 1');
 Echo('  f - flags,                           default = []');
 Echo(' Options may have two equivalent forms: short and long');
 Echo('  -b B (short) and --baudrate B (long) with argument B');
 Echo(' Options are:');
 Echo('  --help           - print help');
 Echo('  --version        - print version');
 Echo('  -v --verb        - set verbose mode to view more details');
 Echo('  -l --list        - list table of all detected COM ports');
 Echo('  -r --rawin       - stdin raw data input mode');
 Echo('  -h --hexin       - stdin HEX data input mode');
 Echo('  -e --escin       - stdin ESC data input mode');
 Echo('  -H --hexout      - stdout HEX data output mode');
 Echo('  -E --escout      - stdout ESC data output mode');
 Echo('  -b --baudrate b  - set baudrate (b) = bits per second:');
 Echo('                    {50,75,110,134,150,200,300,600,1200,1800,2400,');
 Echo('                     4800,9600,19200,38400,57600,115200,230400,460800,');
 Echo('                     500000,576000,921600,1000000,1152000,1500000,');
 Echo('                     2000000,2500000,3000000,3500000,4000000}');
 Echo('  -d --databits d  - set data bits (d) = byte size = {5,6,7,8}');
 Echo('  -p --parity p    - set parity (p) = {NONE,ODD,EVEN}');
 Echo('  -s --stopbits s  - set stopbits (p) = {1,2}');
 Echo('  -f --flags f     - set flags (f), default = []');
 Echo('  --poll p         - poll period (p), ms, default = 4 ms');
 Echo('  -t --deadtime t  - close after dead time (t), default t=0');
 Echo('                     deadtime happens when no any input comes');
 Echo('                     during specified time (t>0), ms');
 Echo('Notes:');
 Echo(' 1. HEX in/out mode means data encode/decode as hexadecimal values.');
 Echo(' 2. ESC in/out mode means data encode/decode by backslash escaping.');
 Echo('Example:');
 Echo(' '+exe+' --help');
 Echo(' '+exe+' --list');
 Echo(' '+exe+' -v COM1 9600 8 NO 1');
 Echo(' cat /etc/hosts | '+exe+' COM1 115200 -t 1000');
 Echo(' '+exe+' ttyUSB0 -e -H -b 115200 -p ODD --poll 1');
 Echo(' '+exe+' ttyS0 -b 115200 -d 8 -p NONE -s 1 -f [CTSRTS]');
 Echo(' '+exe+' ttyS0 --baudrate 115200 --databits 8 --parity EVEN --stopbits 1');
end;

function MakeRaw(fd:LongInt):Boolean;
{$IFDEF UNIX} var tios:termios; {$ENDIF ~UNIX}
begin
 Result:=false;
 {$IFDEF UNIX}
 if (GetFileType(fd)=FILE_TYPE_PIPE) then Exit(true);
 SafeFillChar(tios,SizeOf(tios),0);
 if (TcGetAttr(fd,tios)<0) then Exit;
 cfMakeRaw(tios);
 Result:=(TcSetAttr(fd,TCSANOW,tios)=0);
 {$ENDIF ~UNIX}
end;

function IsPipe(fd:LongInt):Boolean;
begin
 Result:=(GetFileType(fd)=FILE_TYPE_PIPE);
end;

function MakeNonBlock(fd:LongInt):Boolean;
begin
 if SerHasNonBlockFlag(fd)
 then Result:=true
 else Result:=SerSetNonBlockFlag(fd,true);
end;

function BrokenPipe:Boolean;
begin
 Result:=eBrokenPipe(GetLastOsError);
end;

procedure Main;
var ParamNum,ParamMax,ArgNum,PortNum,Len:Integer;
 Param,ParamList,serPath,conIn,conOut,serIn,serOut:LongString;
 BaudRate:LongInt; ByteSize:Integer; Parity:TSerParityType;
 StopBits:TSerStopBits; Flags:TSerialFlags; ending:Char;
 WhenConIn,Tick,DeadTime:QWORD;
 procedure PrintPortList;
 var n:Integer;
 begin
  n:=SerPortMap.Count;
  if (n>0) then Print(stdout,SerPortMap.Table);
  if (n=0) or Verbose
  then Print(stderr,IntToStr(n)+' serial port(s) found.'+EOL);
 end;
 procedure SetPortNum(Param:LongString);
 begin
  PortNum:=SerPortMap.PortNum(Param);
  if (PortNum=0) then Fatal('Error: not found port '+Param);
 end;
 function HasOption(Param,OptList:LongString):Boolean;
 begin
  Result:=(WordIndex(Param,OptList,SerDelims,true)>0);
 end;
 function NextParam:LongString;
 begin
  Inc(ParamNum);
  Param:=ParamStr(ParamNum);
  Result:=Param;
 end;
 procedure AddParamList(Option,Param:LongString);
 begin
  if IsEmptyStr(Param) then Exit;
  if IsEmptyStr(Option) then Exit;
  ParamList:=ParamList+' '+Trim(Option)+' '+Trim(Param);
 end;
 procedure ApplyDefaultOption(opt:LongString; def:LongString);
 begin
  if IsOption(opt) and not HasOption(opt,ParamList) then AddParamList(opt,def);
 end;
begin
 if (ParamCount<1) then PrintHelp else begin
  PortNum:=0; DeadTime:=0;
  conIn:=''; conOut:='';
  serIn:=''; serOut:='';
  // Initialize serial defaults
  BaudRate:=SerDefaultBaudRate;
  ByteSize:=SerDefaultByteSize;
  Parity:=SerDefaultParityType;
  StopBits:=ONESTOPBIT;
  Flags:=[];
  // Parse command line options and argiments
  ParamNum:=1; ArgNum:=1; ParamMax:=ParamCount; ParamList:='';
  while (ParamNum<=ParamMax) do begin
   Param:=ParamStr(ParamNum);
   if IsOption(Param) then begin
    // Handle options started from "-"
    if HasOption(Param,'-help,--help')
    then begin PrintHelp; Exit; end
    else
    if HasOption(Param,'-version,--version')
    then begin PrintVersion; Exit; end
    else
    if HasOption(Param,'-l,-list,--list')
    then begin PrintPortList; Exit; end
    else
    if HasOption(Param,'-v,-verb,--verb,-verbose,--verbose')
    then Verbose:=True
    else
    if HasOption(Param,'-r,-rawin,--rawin,-rawinmode,--rawinmode')
    then RawInMode:=True
    else
    if HasOption(Param,'-h,-hexin,--hexin,-hexinmode,--hexinmode')
    then HexInMode:=True
    else
    if HasOption(Param,'-H,-hexout,--hexout,-hexoutmode,--hexoutmode')
    then HexOutMode:=True
    else
    if HasOption(Param,'-e,-escin,--escin,-escinmode,--escinmode')
    then EscInMode:=True
    else
    if HasOption(Param,'-E,-escout,--escout,-escoutmode,--escoutmode')
    then EscOutMode:=True
    else
    if HasOption(Param,'-b,-baud,--baud,-rate,--rate,-baudrate,--baudrate')
    then AddParamList('--baudrate',NextParam)
    else
    if HasOption(Param,'-d,-data,--data,-databits,--databits,-bytesize,--bytesize')
    then AddParamList('--bytesize',NextParam)
    else
    if HasOption(Param,'-p,-parity,--parity,-paritytype,--paritytype')
    then AddParamList('--parity',NextParam)
    else
    if HasOption(Param,'-s,-stop,--stop,-stopbits,--stopbits')
    then AddParamList('--stopbits',NextParam)
    else
    if HasOption(Param,'-f,-flags,--flags,-dcbflags,--dcbflags')
    then AddParamList('--flags',NextParam)
    else
    if HasOption(Param,'-poll,--poll')
    then PollPeriod:=EnsureRange(StrToIntDef(NextParam,PollPeriod),1,100)
    else
    if HasOption(Param,'-t,-deadtime,--deadtime')
    then DeadTime:=EnsureRange(StrToIntDef(NextParam,DeadTime),0,60000)
    else
    Raise EBadArgument.Create('Invalid option: '+Param);
   end else begin
    // Handle params
    case ArgNum of
     1:   if (Param<>'') then SetPortNum(Param);
     2:   if (Param<>'') then AddParamList('--baudrate',Param);
     3:   if (Param<>'') then AddParamList('--bytesize',Param);
     4:   if (Param<>'') then AddParamList('--parity',Param);
     5:   if (Param<>'') then AddParamList('--stopbits',Param);
     6:   if (Param<>'') then AddParamList('--flags',Param);
     else raise EBadArgument.Create('Bad parameter: '+Param);
    end;
    inc(ArgNum);
   end;
   inc(ParamNum);
  end;
  // Validate in/out mode
  if EscInMode then HexInMode:=false;
  if EscOutMode then HexOutMode:=false;
  // Apply default options if one was not set
  ApplyDefaultOption('--baudrate',SerBaudRateToString(BaudRate));
  ApplyDefaultOption('--bytesize',SerByteSizeToString(ByteSize));
  ApplyDefaultOption('--parity',SerParityTypeToString(Parity));
  ApplyDefaultOption('--stopbits',SerStopBitsToString(StopBits));
  ApplyDefaultOption('--flags',SerFlagsToString(Flags));
  // Serial port path name
  serPath:=SerPortMap.PathName[PortNum];
  // Verbose information
  if Verbose then Print(stderr,'sercat '+serPath+' '+ParamList+EOL);
  // Create & fill bitmap
  serPort:=SerOpen(serPath,O_SERNOBLK);
  if not SerValidHandle(serPort) then begin
   Print(stderr,'Could not open '+serPath+EOL);
   Fatal('Error: '+SysErrorMessage(GetLastOSError));
  end;
  if SerValidHandle(serPort) then
  try
   if SerSetParams(serPort,ParamList) then begin
    serIn:=SerGetParamsAsString(serPort);
    if Verbose then Print(stderr,'serget '+serPath+' '+serIn+EOL);
   end else begin
    raise ESerialFail.Create('Error: serSetParams '+serPath+' '+ParamList);
   end;
   WhenConIn:=GetTickCount64;
   if RawInMode and not MakeRaw(stdin)
   then Print(stderr,'Error: stdin raw mode fail'+EOL);
   if BlkPipeMod and IsPipe(stdin) then NonBlockIn:=false;
   if NonBlockIn and not MakeNonBlock(stdin)
   then Print(stderr,'Error: stdin nonblock fail'+EOL);
   while not Terminated do begin
    Tick:=GetTickCount64;
    SetLastOsError(0);
    conIn:=SerReadStr(stdin,SerBuffSize);
    if BrokenPipe then Terminated:=true;
    if (conIn='') and (DeadTime=0) and not SerHasNonBlockFlag(stdin)
    then Terminated:=true; // end of file detected?
    if (conIn='') and (DeadTime>0) and (Tick-WhenConIn>DeadTime)
    then Terminated:=true; // no any input timeout?
    if (conIn<>'') then WhenConIn:=Tick;
    if HexInMode then conIn:=hex_decode(conIn);
    if EscInMode then conIn:=backslash_decode(conIn);
    if (conIn<>'') then serOut:=serOut+conIn;
    if (serOut<>'') then begin
     Len:=SerWriteStr(serPort,serOut);
     if BrokenPipe then Terminated:=true;
     if (Len>0) then Delete(serOut,1,Len);
    end;
    SetLastOsError(0);
    serIn:=SerReadStr(serPort,SerBuffSize);
    if BrokenPipe then Terminated:=true;
    ending:=StrFetch(serIn,Length(serIn));
    if HexOutMode then serIn:=hex_encode(serIn);
    if EscOutMode then serIn:=backslash_encode(serIn);
    if (serIn<>'') then conOut:=conOut+serIn;
    if (conOut<>'') then begin
     Len:=SerWriteStr(stdout,conOut);
     if BrokenPipe then Terminated:=true;
     if (Len>0) then Delete(conOut,1,Len);
     if (ending in [ASCII_CR,ASCII_LF]) and (HexOutMode or EscOutMode)
     then Print(stdout,EOL);
    end;
    Sleep(PollPeriod);
   end;
   SerSync(serPort);
  finally
   SerClose(serPort);
  end;
 end;
end;

begin
 try
  Main;
 except
  on E:Exception do BugReport(E,nil,'sercat');
 end;
end.

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

