unit uScaleSerial;

interface

uses
  System.SysUtils, System.Classes, System.SyncObjs, Winapi.Windows, uScaleConfig;

type
  TScaleWeightEvent = procedure(Sender: TObject; AWeightGrams: Double; AIsStable: Boolean; const ARawData: string) of object;
  TScaleStatusEvent = procedure(Sender: TObject; AConnected: Boolean; const AMessage: string) of object;

  TScaleReaderThread = class(TThread)
  private
    FOwner: TObject;
    FConfig: TScaleConfig;
    FCommHandle: THandle;
    FStopEvent: TEvent;
    FLastWeights: array of Double;
    FLastWeightIndex: Integer;
    FTareGrams: Double;
    FSimulatedTargetWeight: Double;
    FSimulatedCurrentWeight: Double;

    function OpenPort: Boolean;
    procedure ClosePort;
    function ReadPortData(var ABuffer: AnsiString): Boolean;
    function ParseWeightLine(const ALine: string; var AWeightGrams: Double): Boolean;
    function EvaluateStability(AWeight: Double): Boolean;
    procedure NotifyWeight(AWeight: Double; AStable: Boolean; const ARaw: string);
    procedure NotifyStatus(AConnected: Boolean; const AMsg: string);
  protected
    procedure Execute; override;
  public
    constructor Create(AOwner: TObject; const AConfig: TScaleConfig);
    destructor Destroy; override;
    procedure Stop;
    procedure SetTare(const AValue: Double);
    procedure SetSimulatedTarget(const ATarget: Double);
  end;

  TScaleSerialReader = class
  private
    FConfig: TScaleConfig;
    FThread: TScaleReaderThread;
    FIsConnected: Boolean;
    FLastWeightGrams: Double;
    FLastIsStable: Boolean;
    FLastRawData: string;
    FOnWeight: TScaleWeightEvent;
    FOnStatus: TScaleStatusEvent;
  public
    constructor Create(const AConfig: TScaleConfig);
    destructor Destroy; override;

    function Connect: Boolean;
    procedure Disconnect;
    procedure Tare;
    procedure Zero;
    procedure SetSimulatedWeight(AWeightGrams: Double);

    property IsConnected: Boolean read FIsConnected;
    property LastWeightGrams: Double read FLastWeightGrams;
    property LastIsStable: Boolean read FLastIsStable;
    property LastRawData: string read FLastRawData;
    property Config: TScaleConfig read FConfig write FConfig;

    property OnWeight: TScaleWeightEvent read FOnWeight write FOnWeight;
    property OnStatus: TScaleStatusEvent read FOnStatus write FOnStatus;
  end;

implementation

uses
  System.Math, System.Character;

{ TScaleReaderThread }

constructor TScaleReaderThread.Create(AOwner: TObject; const AConfig: TScaleConfig);
begin
  inherited Create(True); // Suspendido
  FOwner := AOwner;
  FConfig := AConfig;
  FCommHandle := INVALID_HANDLE_VALUE;
  FStopEvent := TEvent.Create(nil, True, False, '');
  FTareGrams := 0.0;
  FSimulatedTargetWeight := 0.0;
  FSimulatedCurrentWeight := 0.0;

  SetLength(FLastWeights, Max(3, FConfig.StableSamples));
  FLastWeightIndex := 0;
  FreeOnTerminate := False;
end;

destructor TScaleReaderThread.Destroy;
begin
  Stop;
  FStopEvent.Free;
  inherited Destroy;
end;

procedure TScaleReaderThread.Stop;
begin
  Terminate;
  if Assigned(FStopEvent) then
    FStopEvent.SetEvent;
  ClosePort;
  WaitFor;
end;

procedure TScaleReaderThread.SetTare(const AValue: Double);
begin
  FTareGrams := AValue;
end;

procedure TScaleReaderThread.SetSimulatedTarget(const ATarget: Double);
begin
  FSimulatedTargetWeight := ATarget;
end;

function TScaleReaderThread.OpenPort: Boolean;
var
  LPortStr: string;
  LDCB: TDCB;
  LTimeouts: TCommTimeouts;
begin
  Result := False;
  if FConfig.Simulated then
  begin
    NotifyStatus(True, 'Modo báscula simulada activo');
    Exit(True);
  end;

  LPortStr := '\\.\' + FConfig.PortName;
  FCommHandle := CreateFileW(
    PWideChar(LPortStr),
    GENERIC_READ or GENERIC_WRITE,
    0,
    nil,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    0
  );

  if FCommHandle = INVALID_HANDLE_VALUE then
  begin
    NotifyStatus(False, Format('No se pudo abrir el puerto %s (Error Win32: %d)', [FConfig.PortName, GetLastError]));
    Exit(False);
  end;

  FillChar(LDCB, SizeOf(LDCB), 0);
  LDCB.DCBlength := SizeOf(LDCB);
  if not GetCommState(FCommHandle, LDCB) then
  begin
    ClosePort;
    NotifyStatus(False, 'Error al obtener estado del puerto COM');
    Exit(False);
  end;

  LDCB.BaudRate := FConfig.BaudRate;
  LDCB.ByteSize := FConfig.DataBits;
  LDCB.Parity := FConfig.Parity;
  LDCB.StopBits := FConfig.StopBits;
  LDCB.Flags := 1; // fBinary

  if not SetCommState(FCommHandle, LDCB) then
  begin
    ClosePort;
    NotifyStatus(False, 'Error al configurar parámetros del puerto COM');
    Exit(False);
  end;

  FillChar(LTimeouts, SizeOf(LTimeouts), 0);
  LTimeouts.ReadIntervalTimeout := 50;
  LTimeouts.ReadTotalTimeoutConstant := 100;
  LTimeouts.ReadTotalTimeoutMultiplier := 10;
  SetCommTimeouts(FCommHandle, LTimeouts);

  PurgeComm(FCommHandle, PURGE_TXCLEAR or PURGE_RXCLEAR);
  NotifyStatus(True, Format('Conectado a %s a %d baudios', [FConfig.PortName, FConfig.BaudRate]));
  Result := True;
end;

procedure TScaleReaderThread.ClosePort;
begin
  if FCommHandle <> INVALID_HANDLE_VALUE then
  begin
    CloseHandle(FCommHandle);
    FCommHandle := INVALID_HANDLE_VALUE;
  end;
end;

function TScaleReaderThread.ReadPortData(var ABuffer: AnsiString): Boolean;
var
  LBytesRead: DWORD;
  LChunk: array[0..255] of AnsiChar;
begin
  Result := False;
  if FCommHandle = INVALID_HANDLE_VALUE then Exit;

  if ReadFile(FCommHandle, LChunk, SizeOf(LChunk) - 1, LBytesRead, nil) then
  begin
    if LBytesRead > 0 then
    begin
      LChunk[LBytesRead] := #0;
      ABuffer := ABuffer + AnsiString(PAnsiChar(@LChunk[0]));
      Result := True;
    end;
  end;
end;

function TScaleReaderThread.ParseWeightLine(const ALine: string; var AWeightGrams: Double): Boolean;
var
  LS: string;
  I: Integer;
  LNumStr: string;
  LIsKg: Boolean;
  LVal: Double;
  LFormatSettings: TFormatSettings;
begin
  Result := False;
  LS := Trim(ALine);
  if LS = '' then Exit;

  LIsKg := (Pos('KG', UpperCase(LS)) > 0);
  LNumStr := '';

  for I := 1 to Length(LS) do
  begin
    if CharInSet(LS[I], ['0'..'9', '.', ',', '-', '+']) then
      LNumStr := LNumStr + LS[I];
  end;

  if LNumStr = '' then Exit;

  LFormatSettings := TFormatSettings.Create('es-ES');
  LFormatSettings.DecimalSeparator := '.';
  LNumStr := StringReplace(LNumStr, ',', '.', [rfReplaceAll]);

  // Si tiene signo repetido o texto extraño, buscar el último float válido
  if TryStrToFloat(LNumStr, LVal, LFormatSettings) then
  begin
    if LIsKg then
      AWeightGrams := LVal * 1000.0
    else
      AWeightGrams := LVal;

    AWeightGrams := AWeightGrams - FTareGrams;
    if AWeightGrams < 0 then
      AWeightGrams := 0.0;

    Result := True;
  end;
end;

function TScaleReaderThread.EvaluateStability(AWeight: Double): Boolean;
var
  I: Integer;
  LMin, LMax: Double;
begin
  FLastWeights[FLastWeightIndex] := AWeight;
  FLastWeightIndex := (FLastWeightIndex + 1) mod Length(FLastWeights);

  LMin := FLastWeights[0];
  LMax := FLastWeights[0];
  for I := 1 to High(FLastWeights) do
  begin
    if FLastWeights[I] < LMin then LMin := FLastWeights[I];
    if FLastWeights[I] > LMax then LMax := FLastWeights[I];
  end;

  Result := (Abs(LMax - LMin) <= FConfig.ToleranceGrams);
end;

procedure TScaleReaderThread.NotifyWeight(AWeight: Double; AStable: Boolean; const ARaw: string);
var
  LReader: TScaleSerialReader;
begin
  LReader := TScaleSerialReader(FOwner);
  TThread.Queue(nil,
    procedure
    begin
      if Assigned(LReader) then
      begin
        LReader.FLastWeightGrams := AWeight;
        LReader.FLastIsStable := AStable;
        LReader.FLastRawData := ARaw;
        if Assigned(LReader.FOnWeight) then
          LReader.FOnWeight(LReader, AWeight, AStable, ARaw);
      end;
    end);
end;

procedure TScaleReaderThread.NotifyStatus(AConnected: Boolean; const AMsg: string);
var
  LReader: TScaleSerialReader;
begin
  LReader := TScaleSerialReader(FOwner);
  TThread.Queue(nil,
    procedure
    begin
      if Assigned(LReader) then
      begin
        LReader.FIsConnected := AConnected;
        if Assigned(LReader.FOnStatus) then
          LReader.FOnStatus(LReader, AConnected, AMsg);
      end;
    end);
end;

procedure TScaleReaderThread.Execute;
var
  LBuffer: AnsiString;
  LP: Integer;
  LLine: string;
  LWeight: Double;
  LStable: Boolean;
  LStep: Double;
begin
  if not OpenPort then
    Exit;

  LBuffer := '';
  while not Terminated do
  begin
    if FConfig.Simulated then
    begin
      // Simular aproximación gradual al peso objetivo con leve ruido
      if Abs(FSimulatedCurrentWeight - FSimulatedTargetWeight) > 0.5 then
      begin
        LStep := (FSimulatedTargetWeight - FSimulatedCurrentWeight) * 0.25;
        FSimulatedCurrentWeight := FSimulatedCurrentWeight + LStep;
      end
      else
        FSimulatedCurrentWeight := FSimulatedTargetWeight;

      LWeight := FSimulatedCurrentWeight - FTareGrams;
      if LWeight < 0 then LWeight := 0;
      LStable := EvaluateStability(LWeight);
      NotifyWeight(LWeight, LStable, Format('SIMULATED: %.2f g', [LWeight]));

      if FStopEvent.WaitFor(Max(50, FConfig.PollIntervalMs)) = wrSignaled then
        Break;
      Continue;
    end;

    // Lectura de puerto real
    if ReadPortData(LBuffer) then
    begin
      repeat
        LP := Pos(#10, string(LBuffer));
        if LP <= 0 then
          LP := Pos(#13, string(LBuffer));

        if LP > 0 then
        begin
          LLine := Trim(Copy(string(LBuffer), 1, LP));
          Delete(LBuffer, 1, LP);

          if ParseWeightLine(LLine, LWeight) then
          begin
            LStable := EvaluateStability(LWeight);
            NotifyWeight(LWeight, LStable, LLine);
          end;
        end;
      until LP = 0;
    end;

    if FStopEvent.WaitFor(Max(30, FConfig.PollIntervalMs)) = wrSignaled then
      Break;
  end;

  ClosePort;
  NotifyStatus(False, 'Puerto cerrado');
end;

{ TScaleSerialReader }

constructor TScaleSerialReader.Create(const AConfig: TScaleConfig);
begin
  inherited Create;
  FConfig := AConfig;
  FIsConnected := False;
  FLastWeightGrams := 0.0;
  FLastIsStable := False;
  FLastRawData := '';
end;

destructor TScaleSerialReader.Destroy;
begin
  Disconnect;
  inherited Destroy;
end;

function TScaleSerialReader.Connect: Boolean;
begin
  Disconnect;
  FThread := TScaleReaderThread.Create(Self, FConfig);
  FThread.Start;
  FIsConnected := True;
  Result := True;
end;

procedure TScaleSerialReader.Disconnect;
begin
  if Assigned(FThread) then
  begin
    FThread.Stop;
    FreeAndNil(FThread);
  end;
  FIsConnected := False;
end;

procedure TScaleSerialReader.Tare;
begin
  if Assigned(FThread) then
    FThread.SetTare(FLastWeightGrams);
end;

procedure TScaleSerialReader.Zero;
begin
  if Assigned(FThread) then
    FThread.SetTare(0.0);
end;

procedure TScaleSerialReader.SetSimulatedWeight(AWeightGrams: Double);
begin
  if Assigned(FThread) then
    FThread.SetSimulatedTarget(AWeightGrams);
end;

end.
