unit frm_MainBascula;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  System.UITypes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls,
  Vcl.ComCtrls, Vcl.Clipbrd, System.IOUtils,
  uSerialComm, uBasculaParser, uConfigBascula;

type
  TfrmMainBascula = class(TForm)
    pnlHeader: TPanel;
    lblTitulo: TLabel;
    lblSubtitulo: TLabel;
    shpEstadoConexion: TShape;
    lblEstadoConexion: TLabel;
    pnlBarraConexion: TPanel;
    lblPuerto: TLabel;
    cbPuertos: TComboBox;
    btnRefrescarPuertos: TButton;
    btnEscanearBluetooth: TButton;
    lblBaudRate: TLabel;
    cbBaudRate: TComboBox;
    btnConectar: TButton;
    chkAutoConectar: TCheckBox;
    btnTestDiagnostico: TButton;
    shpLedRx: TShape;
    lblBytesRx: TLabel;
    pnlVisor: TPanel;
    lblStatusEstabilidad: TLabel;
    lblPesoDigital: TLabel;
    lblUnidad: TLabel;
    pnlSubVisor: TPanel;
    lblBruto: TLabel;
    lblTara: TLabel;
    lblUltimaTrama: TLabel;
    pnlBotonesAccion: TPanel;
    btnCero: TButton;
    btnTara: TButton;
    btnBorrarTara: TButton;
    btnCapturar: TButton;
    btnCopiar: TButton;
    pgcDiagnostico: TPageControl;
    tsTerminal: TTabSheet;
    pnlTerminalTop: TPanel;
    chkVerHex: TCheckBox;
    chkPausarScroll: TCheckBox;
    btnLimpiarTerminal: TButton;
    lblComando: TLabel;
    edtComando: TEdit;
    btnEnviarComando: TButton;
    memTerminal: TMemo;
    tsDiagnostico: TTabSheet;
    pnlDiagTop: TPanel;
    btnEjecutarDiagnostico: TButton;
    btnAutoBaud: TButton;
    btnProbarP: TButton;
    btnProbarW: TButton;
    btnProbarCR: TButton;
    btnProbarSI: TButton;
    btnConmutarDTR: TButton;
    btnConmutarRTS: TButton;
    lblStatusCTS: TLabel;
    lblStatusDSR: TLabel;
    memDiagnostico: TMemo;
    gbGuiaCables: TGroupBox;
    memGuiaCables: TMemo;
    tsHistorial: TTabSheet;
    pnlHistorialTop: TPanel;
    btnExportarCSV: TButton;
    btnLimpiarHistorial: TButton;
    lblTotalCapturas: TLabel;
    lvHistorial: TListView;
    tsAjustes: TTabSheet;
    gbPuertoSerie: TGroupBox;
    lblDataBits: TLabel;
    cbDataBits: TComboBox;
    lblParidad: TLabel;
    cbParidad: TComboBox;
    lblStopBits: TLabel;
    cbStopBits: TComboBox;
    lblControlFlujo: TLabel;
    cbControlFlujo: TComboBox;
    gbProtocoloBascula: TGroupBox;
    lblProtocolo: TLabel;
    cbProtocolo: TComboBox;
    chkPolling: TCheckBox;
    lblPollingCmd: TLabel;
    edtPollingCmd: TEdit;
    lblPollingInterval: TLabel;
    edtPollingInterval: TEdit;
    chkSimulador: TCheckBox;
    btnGuardarAjustes: TButton;
    gbInfoSHB35: TGroupBox;
    memInfoSHB35: TMemo;
    tmrPolling: TTimer;
    tmrSimulador: TTimer;
    tmrLedRxOff: TTimer;
    tmrMonitorModem: TTimer;
    dlgSaveCSV: TSaveDialog;

    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);

    procedure btnRefrescarPuertosClick(Sender: TObject);
    procedure btnEscanearBluetoothClick(Sender: TObject);
    procedure btnConectarClick(Sender: TObject);
    procedure btnCeroClick(Sender: TObject);
    procedure btnTaraClick(Sender: TObject);
    procedure btnBorrarTaraClick(Sender: TObject);
    procedure btnCapturarClick(Sender: TObject);
    procedure btnCopiarClick(Sender: TObject);
    procedure btnLimpiarTerminalClick(Sender: TObject);
    procedure btnEnviarComandoClick(Sender: TObject);
    procedure btnExportarCSVClick(Sender: TObject);
    procedure btnLimpiarHistorialClick(Sender: TObject);
    procedure btnGuardarAjustesClick(Sender: TObject);
    procedure chkSimuladorClick(Sender: TObject);
    procedure tmrPollingTimer(Sender: TObject);
    procedure tmrSimuladorTimer(Sender: TObject);
    procedure tmrLedRxOffTimer(Sender: TObject);
    procedure tmrMonitorModemTimer(Sender: TObject);

    procedure btnTestDiagnosticoClick(Sender: TObject);
    procedure btnEjecutarDiagnosticoClick(Sender: TObject);
    procedure btnAutoBaudClick(Sender: TObject);
    procedure btnProbarPClick(Sender: TObject);
    procedure btnProbarWClick(Sender: TObject);
    procedure btnProbarCRClick(Sender: TObject);
    procedure btnProbarSIClick(Sender: TObject);
    procedure btnConmutarDTRClick(Sender: TObject);
    procedure btnConmutarRTSClick(Sender: TObject);
  private
    FSerial: TSerialPort;
    FParser: TBasculaParser;
    FConfig: TBasculaConfig;
    FSimulacionPeso: Double;
    FSimulacionOscilacion: Integer;
    FDTRActivo: Boolean;
    FRTSActivo: Boolean;

    procedure CargarConfiguracionUI;
    procedure GuardarConfiguracionDesdeUI;
    procedure RefrescarListaPuertos;
    procedure ActualizarEstadoConexionUI(AConectado: Boolean; const AMensaje: string);
    procedure ActualizarModemStatusUI;

    procedure OnSerialStatusChange(Sender: TObject; Connected: Boolean; const Msg: string);
    procedure OnSerialError(Sender: TObject; const ErrorMsg: string);
    procedure OnSerialRxString(Sender: TObject; const Text: string);
    procedure OnParserPeso(Sender: TObject; PesoNeto, PesoBruto, Tara: Double;
      Estable: Boolean; const Unidad: string; const TramaOriginal: string);
    procedure OnParserRawData(Sender: TObject; const RawStr, RawHex: string);

    procedure EjecutarDiagnosticoCompleto;
    procedure ProbarEnvioComandoDiag(const ACmd, ADesc: string);
  public
  end;

var
  frmMainBascula: TfrmMainBascula;

implementation

{$R *.dfm}

{ TfrmMainBascula }

procedure TfrmMainBascula.FormCreate(Sender: TObject);
begin
  KeyPreview := True;
  FConfig := TBasculaConfig.Create;
  FConfig.Load;

  FDTRActivo := True;
  FRTSActivo := True;

  FSerial := TSerialPort.Create;
  FSerial.OnStatusChange := OnSerialStatusChange;
  FSerial.OnError := OnSerialError;
  FSerial.OnRxString := OnSerialRxString;

  FParser := TBasculaParser.Create;
  FParser.OnPeso := OnParserPeso;
  FParser.OnRawData := OnParserRawData;

  FSimulacionPeso := 12.450;
  FSimulacionOscilacion := 0;

  RefrescarListaPuertos;
  CargarConfiguracionUI;

  pgcDiagnostico.ActivePageIndex := 0;
  ActualizarEstadoConexionUI(False, 'Desconectado. Selecciona el puerto COM del DSD TECH SH-B35 y pulsa Conectar.');

  if FConfig.AutoConnect then
    btnConectarClick(nil);
end;

procedure TfrmMainBascula.FormDestroy(Sender: TObject);
begin
  tmrPolling.Enabled := False;
  tmrMonitorModem.Enabled := False;
  FSerial.Close;
  FreeAndNil(FSerial);
  FreeAndNil(FParser);
  FreeAndNil(FConfig);
end;

procedure TfrmMainBascula.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  GuardarConfiguracionDesdeUI;
end;

procedure TfrmMainBascula.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_F2:
    begin
      btnCeroClick(nil);
      Key := 0;
    end;
    VK_F3:
    begin
      btnTaraClick(nil);
      Key := 0;
    end;
    VK_F4:
    begin
      btnBorrarTaraClick(nil);
      Key := 0;
    end;
    VK_F5:
    begin
      btnCapturarClick(nil);
      Key := 0;
    end;
  end;
end;

procedure TfrmMainBascula.RefrescarListaPuertos;
var
  Lista: TStringList;
  I, SelIdx: Integer;
  PuertoLimpio: string;
begin
  Lista := TStringList.Create;
  try
    TSerialPort.GetAvailablePorts(Lista);
    cbPuertos.Items.Assign(Lista);

    SelIdx := -1;
    for I := 0 to cbPuertos.Items.Count - 1 do
    begin
      PuertoLimpio := cbPuertos.Items[I];
      if Pos(' ', PuertoLimpio) > 0 then
        PuertoLimpio := Copy(PuertoLimpio, 1, Pos(' ', PuertoLimpio) - 1);

      if SameText(PuertoLimpio, FConfig.Port) then
      begin
        SelIdx := I;
        Break;
      end;
    end;

    if SelIdx >= 0 then
      cbPuertos.ItemIndex := SelIdx
    else if cbPuertos.Items.Count > 0 then
      cbPuertos.ItemIndex := 0;
  finally
    Lista.Free;
  end;
end;

procedure TfrmMainBascula.CargarConfiguracionUI;
var
  Idx: Integer;
begin
  Idx := cbBaudRate.Items.IndexOf(IntToStr(FConfig.BaudRate));
  if Idx >= 0 then cbBaudRate.ItemIndex := Idx else cbBaudRate.ItemIndex := 3; // 9600

  case FConfig.DataBits of
    7: cbDataBits.ItemIndex := 0;
    else cbDataBits.ItemIndex := 1; // 8
  end;

  cbParidad.ItemIndex := FConfig.Parity;
  cbStopBits.ItemIndex := FConfig.StopBits;
  cbControlFlujo.ItemIndex := FConfig.FlowControl;
  cbProtocolo.ItemIndex := FConfig.Protocolo;

  chkAutoConectar.Checked := FConfig.AutoConnect;
  chkPolling.Checked := FConfig.PollingEnabled;
  edtPollingCmd.Text := FConfig.PollingCommand;
  edtPollingInterval.Text := IntToStr(FConfig.PollingIntervalMs);
  chkSimulador.Checked := FConfig.Simulacion;

  tmrSimulador.Enabled := FConfig.Simulacion;
  FParser.Protocolo := TProtocoloBascula(FConfig.Protocolo);
  FParser.Decimales := FConfig.Decimales;
  FParser.Unidad := FConfig.Unidad;
end;

procedure TfrmMainBascula.GuardarConfiguracionDesdeUI;
var
  P: string;
begin
  P := cbPuertos.Text;
  if Pos(' ', P) > 0 then
    P := Copy(P, 1, Pos(' ', P) - 1);

  FConfig.Port := P;
  FConfig.BaudRate := StrToIntDef(cbBaudRate.Text, 9600);
  if cbDataBits.ItemIndex = 0 then FConfig.DataBits := 7 else FConfig.DataBits := 8;
  FConfig.Parity := cbParidad.ItemIndex;
  FConfig.StopBits := cbStopBits.ItemIndex;
  FConfig.FlowControl := cbControlFlujo.ItemIndex;
  FConfig.Protocolo := cbProtocolo.ItemIndex;
  FConfig.AutoConnect := chkAutoConectar.Checked;
  FConfig.PollingEnabled := chkPolling.Checked;
  FConfig.PollingCommand := edtPollingCmd.Text;
  FConfig.PollingIntervalMs := StrToIntDef(edtPollingInterval.Text, 300);
  FConfig.Simulacion := chkSimulador.Checked;

  FConfig.Save;
end;

procedure TfrmMainBascula.ActualizarEstadoConexionUI(AConectado: Boolean; const AMensaje: string);
begin
  if AConectado then
  begin
    shpEstadoConexion.Brush.Color := $0022C55E; // Verde brillante
    lblEstadoConexion.Caption := 'CONECTADO (' + FSerial.PortName + ')';
    lblEstadoConexion.Font.Color := $00166534;
    btnConectar.Caption := 'DESCONECTAR';
    btnConectar.Font.Color := clRed;
    cbPuertos.Enabled := False;
    cbBaudRate.Enabled := False;
    btnRefrescarPuertos.Enabled := False;
    btnEscanearBluetooth.Enabled := False;
    tmrMonitorModem.Enabled := True;
  end
  else
  begin
    shpEstadoConexion.Brush.Color := $00475569; // Gris / Rojo
    lblEstadoConexion.Caption := 'DESCONECTADO';
    lblEstadoConexion.Font.Color := $00475569;
    btnConectar.Caption := 'CONECTAR';
    btnConectar.Font.Color := clGreen;
    cbPuertos.Enabled := True;
    cbBaudRate.Enabled := True;
    btnRefrescarPuertos.Enabled := True;
    btnEscanearBluetooth.Enabled := True;
    lblStatusEstabilidad.Caption := '---';
    lblStatusEstabilidad.Font.Color := clGray;
    shpLedRx.Brush.Color := clGray;
    lblBytesRx.Caption := 'Bytes RX: 0';
    lblStatusCTS.Caption := 'CTS: --';
    lblStatusDSR.Caption := 'DSR: --';
    tmrMonitorModem.Enabled := False;
  end;

  if AMensaje <> '' then
    memTerminal.Lines.Add('[' + FormatDateTime('hh:nn:ss', Now) + '] ' + AMensaje);
end;

procedure TfrmMainBascula.ActualizarModemStatusUI;
var
  CTS, DSR, Ring, RLSD: Boolean;
begin
  if FSerial.Connected and FSerial.GetModemStatus(CTS, DSR, Ring, RLSD) then
  begin
    if CTS then
    begin
      lblStatusCTS.Caption := 'CTS: ACTIVO (ON)';
      lblStatusCTS.Font.Color := clGreen;
    end
    else
    begin
      lblStatusCTS.Caption := 'CTS: INACTIVO (OFF)';
      lblStatusCTS.Font.Color := clMaroon;
    end;

    if DSR then
    begin
      lblStatusDSR.Caption := 'DSR: ACTIVO (ON)';
      lblStatusDSR.Font.Color := clGreen;
    end
    else
    begin
      lblStatusDSR.Caption := 'DSR: INACTIVO (OFF)';
      lblStatusDSR.Font.Color := clMaroon;
    end;
  end
  else
  begin
    lblStatusCTS.Caption := 'CTS: --';
    lblStatusDSR.Caption := 'DSR: --';
  end;
end;

procedure TfrmMainBascula.btnRefrescarPuertosClick(Sender: TObject);
begin
  RefrescarListaPuertos;
  ShowMessage(Format('Búsqueda completada. Se detectaron %d puerto(s) serie disponibles.', [cbPuertos.Items.Count]));
end;

procedure TfrmMainBascula.btnEscanearBluetoothClick(Sender: TObject);
var
  BTDevices: TStringList;
  I, MatchIdx: Integer;
  DevText: string;
begin
  BTDevices := TStringList.Create;
  try
    TSerialPort.GetPairedBluetoothDevices(BTDevices);

    memTerminal.Lines.Add('=== Dispositivos Bluetooth Emparejados en Windows ===');
    MatchIdx := -1;
    for I := 0 to BTDevices.Count - 1 do
    begin
      DevText := BTDevices[I];
      memTerminal.Lines.Add('  - ' + DevText);
      if (Pos('SH-B35', UpperCase(DevText)) > 0) or (Pos('DSD', UpperCase(DevText)) > 0) or
         (Pos('BASCULA', UpperCase(DevText)) > 0) then
      begin
        MatchIdx := I;
      end;
    end;

    RefrescarListaPuertos;

    if MatchIdx >= 0 then
    begin
      ShowMessage('¡Dispositivo detectado!: ' + BTDevices[MatchIdx] + sLineBreak +
        'Revisa los puertos marcados con [Bluetooth SPP] en el selector para conectar.');
    end
    else
    begin
      ShowMessage('Dispositivos Bluetooth encontrados: ' + IntToStr(BTDevices.Count) + sLineBreak +
        'Detalles añadidos a la pestaña Terminal.' + sLineBreak + sLineBreak +
        'Nota: Para que el adaptador DSD TECH SH-B35 cree un puerto COM, debe estar previamente emparejado ' +
        'en la Configuración de Bluetooth de Windows (PIN habitual: 1234).');
    end;
  finally
    BTDevices.Free;
  end;
end;

procedure TfrmMainBascula.btnConectarClick(Sender: TObject);
var
  PuertoSeleccionado: string;
begin
  if FSerial.Connected then
  begin
    tmrPolling.Enabled := False;
    FSerial.Close;
    Exit;
  end;

  PuertoSeleccionado := cbPuertos.Text;
  if Pos(' ', PuertoSeleccionado) > 0 then
    PuertoSeleccionado := Copy(PuertoSeleccionado, 1, Pos(' ', PuertoSeleccionado) - 1);

  if PuertoSeleccionado = '' then
  begin
    ShowMessage('Por favor selecciona un puerto COM válido.');
    Exit;
  end;

  GuardarConfiguracionDesdeUI;

  FSerial.PortName := PuertoSeleccionado;
  FSerial.BaudRate := StrToIntDef(cbBaudRate.Text, 9600);
  if cbDataBits.ItemIndex = 0 then FSerial.DataBits := 7 else FSerial.DataBits := 8;

  case cbParidad.ItemIndex of
    1: FSerial.Parity := ptOdd;
    2: FSerial.Parity := ptEven;
    3: FSerial.Parity := ptMark;
    4: FSerial.Parity := ptSpace;
    else FSerial.Parity := ptNone;
  end;

  case cbStopBits.ItemIndex of
    1: FSerial.StopBits := sbtOnePointFive;
    2: FSerial.StopBits := sbtTwo;
    else FSerial.StopBits := sbtOne;
  end;

  case cbControlFlujo.ItemIndex of
    1: FSerial.FlowControl := fctRtsCts;
    2: FSerial.FlowControl := fctXonXoff;
    else FSerial.FlowControl := fctNone;
  end;

  FParser.Protocolo := TProtocoloBascula(cbProtocolo.ItemIndex);
  FParser.Reset;

  if FSerial.Open then
  begin
    ActualizarModemStatusUI;
    if chkPolling.Checked then
    begin
      tmrPolling.Interval := StrToIntDef(edtPollingInterval.Text, 300);
      tmrPolling.Enabled := True;
    end;
  end;
end;

procedure TfrmMainBascula.OnSerialStatusChange(Sender: TObject; Connected: Boolean; const Msg: string);
begin
  ActualizarEstadoConexionUI(Connected, Msg);
end;

procedure TfrmMainBascula.OnSerialError(Sender: TObject; const ErrorMsg: string);
begin
  memTerminal.Lines.Add('[ERROR] ' + ErrorMsg);
  memDiagnostico.Lines.Add('[ERROR] ' + ErrorMsg);
  ShowMessage(ErrorMsg);
end;

procedure TfrmMainBascula.OnSerialRxString(Sender: TObject; const Text: string);
begin
  // Parpadeo LED RX y contador de bytes
  shpLedRx.Brush.Color := $0022C55E; // Verde brillante
  tmrLedRxOff.Enabled := False;
  tmrLedRxOff.Enabled := True;
  lblBytesRx.Caption := Format('Bytes RX: %d', [FSerial.TotalBytesReceived]);

  FParser.FeedData(Text);
end;

procedure TfrmMainBascula.OnParserRawData(Sender: TObject; const RawStr, RawHex: string);
var
  Linea: string;
begin
  lblUltimaTrama.Caption := 'ÚLTIMA TRAMA: ' + RawStr.Trim;

  if chkPausarScroll.Checked then
    Exit;

  if chkVerHex.Checked then
    Linea := Format('[RX] %s | HEX: %s', [RawStr.Trim, RawHex])
  else
    Linea := Format('[RX] %s', [RawStr.Trim]);

  if memTerminal.Lines.Count > 1000 then
    memTerminal.Clear;

  memTerminal.Lines.Add(Linea);
end;

procedure TfrmMainBascula.OnParserPeso(Sender: TObject; PesoNeto, PesoBruto, Tara: Double;
  Estable: Boolean; const Unidad: string; const TramaOriginal: string);
var
  FormatoNum: string;
begin
  FormatoNum := '0.' + StringOfChar('0', FConfig.Decimales);

  lblPesoDigital.Caption := FormatFloat(FormatoNum, PesoNeto);
  lblUnidad.Caption := Unidad;

  lblBruto.Caption := 'BRUTO: ' + FormatFloat(FormatoNum, PesoBruto) + ' ' + Unidad;
  lblTara.Caption := 'TARA: ' + FormatFloat(FormatoNum, Tara) + ' ' + Unidad;

  if Estable then
  begin
    lblStatusEstabilidad.Caption := '● ESTABLE';
    lblStatusEstabilidad.Font.Color := $0022C55E; // Verde
    lblPesoDigital.Font.Color := $004ADE80; // Verde claro
  end
  else
  begin
    lblStatusEstabilidad.Caption := '◌ EN MOVIMIENTO';
    lblStatusEstabilidad.Font.Color := $0000D7FF; // Ámbar / Naranja
    lblPesoDigital.Font.Color := $00FACC15; // Amarillo brillante
  end;
end;

procedure TfrmMainBascula.btnCeroClick(Sender: TObject);
begin
  FParser.PonerCero;
  if FSerial.Connected then
    FSerial.SendString('Z'#13#10);
end;

procedure TfrmMainBascula.btnTaraClick(Sender: TObject);
begin
  FParser.HacerTara;
  if FSerial.Connected then
    FSerial.SendString('T'#13#10);
end;

procedure TfrmMainBascula.btnBorrarTaraClick(Sender: TObject);
begin
  FParser.BorrarTara;
end;

procedure TfrmMainBascula.btnCapturarClick(Sender: TObject);
var
  Item: TListItem;
  FormatoNum: string;
begin
  FormatoNum := '0.' + StringOfChar('0', FConfig.Decimales);

  Item := lvHistorial.Items.Add;
  Item.Caption := IntToStr(lvHistorial.Items.Count);
  Item.SubItems.Add(FormatDateTime('yyyy-mm-dd', Now));
  Item.SubItems.Add(FormatDateTime('hh:nn:ss', Now));
  Item.SubItems.Add(FormatFloat(FormatoNum, FParser.UltimoPesoNeto));
  Item.SubItems.Add(FormatFloat(FormatoNum, FParser.TaraManual));
  Item.SubItems.Add(FormatFloat(FormatoNum, FParser.UltimoPesoBruto));
  Item.SubItems.Add(FParser.Unidad);
  if FParser.Estable then
    Item.SubItems.Add('Estable')
  else
    Item.SubItems.Add('Inestable');

  Item.MakeVisible(True);
  lblTotalCapturas.Caption := Format('Total capturas: %d', [lvHistorial.Items.Count]);

  MessageBeep(MB_OK);
  memTerminal.Lines.Add(Format('[CAPTURA] Guardado: %s %s (Neto)',
    [FormatFloat(FormatoNum, FParser.UltimoPesoNeto), FParser.Unidad]));
end;

procedure TfrmMainBascula.btnCopiarClick(Sender: TObject);
begin
  Clipboard.AsText := lblPesoDigital.Caption;
  ShowMessage('Peso copiado al portapapeles: ' + lblPesoDigital.Caption + ' ' + lblUnidad.Caption);
end;

procedure TfrmMainBascula.btnLimpiarTerminalClick(Sender: TObject);
begin
  memTerminal.Clear;
end;

procedure TfrmMainBascula.btnEnviarComandoClick(Sender: TObject);
var
  Cmd: string;
begin
  if not FSerial.Connected then
  begin
    ShowMessage('El puerto serie no está conectado.');
    Exit;
  end;

  Cmd := edtComando.Text;
  if Cmd <> '' then
  begin
    FSerial.SendString(Cmd + #13#10);
    memTerminal.Lines.Add('[TX] ' + Cmd);
  end;
end;

procedure TfrmMainBascula.btnExportarCSVClick(Sender: TObject);
var
  SL: TStringList;
  I: Integer;
  Item: TListItem;
  Linea: string;
begin
  if lvHistorial.Items.Count = 0 then
  begin
    ShowMessage('No hay registros en el historial para exportar.');
    Exit;
  end;

  dlgSaveCSV.FileName := Format('pesadas_%s.csv', [FormatDateTime('yyyymmdd_hhnn', Now)]);
  if not dlgSaveCSV.Execute then
    Exit;

  SL := TStringList.Create;
  try
    SL.Add('Num;Fecha;Hora;PesoNeto;Tara;PesoBruto;Unidad;Estado');
    for I := 0 to lvHistorial.Items.Count - 1 do
    begin
      Item := lvHistorial.Items[I];
      Linea := Format('%s;%s;%s;%s;%s;%s;%s;%s', [
        Item.Caption,
        Item.SubItems[0],
        Item.SubItems[1],
        Item.SubItems[2],
        Item.SubItems[3],
        Item.SubItems[4],
        Item.SubItems[5],
        Item.SubItems[6]
      ]);
      SL.Add(Linea);
    end;
    SL.SaveToFile(dlgSaveCSV.FileName, TEncoding.UTF8);
    ShowMessage('Historial exportado correctamente a: ' + dlgSaveCSV.FileName);
  finally
    SL.Free;
  end;
end;

procedure TfrmMainBascula.btnLimpiarHistorialClick(Sender: TObject);
begin
  if MessageDlg('¿Deseas vaciar el historial de pesadas capturadas?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
  begin
    lvHistorial.Items.Clear;
    lblTotalCapturas.Caption := 'Total capturas: 0';
  end;
end;

procedure TfrmMainBascula.btnGuardarAjustesClick(Sender: TObject);
begin
  GuardarConfiguracionDesdeUI;
  ShowMessage('Configuración guardada correctamente.');
end;

procedure TfrmMainBascula.chkSimuladorClick(Sender: TObject);
begin
  tmrSimulador.Enabled := chkSimulador.Checked;
  if chkSimulador.Checked then
    memTerminal.Lines.Add('[INFO] Modo simulación activado.')
  else
    memTerminal.Lines.Add('[INFO] Modo simulación desactivado.');
end;

procedure TfrmMainBascula.tmrPollingTimer(Sender: TObject);
begin
  if FSerial.Connected and (edtPollingCmd.Text <> '') then
    FSerial.SendString(edtPollingCmd.Text);
end;

procedure TfrmMainBascula.tmrSimuladorTimer(Sender: TObject);
var
  TramaSim: string;
  Variacion: Double;
begin
  Inc(FSimulacionOscilacion);
  if (FSimulacionOscilacion mod 6) = 0 then
  begin
    Variacion := (Random(11) - 5) * 0.005;
    FSimulacionPeso := FSimulacionPeso + Variacion;
    if FSimulacionPeso < 0 then FSimulacionPeso := 0;
  end;

  if (FSimulacionOscilacion mod 4) = 0 then
    TramaSim := Format('ST,GS,+%8.3fkg'#13#10, [FSimulacionPeso])
  else
    TramaSim := Format('US,GS,+%8.3fkg'#13#10, [FSimulacionPeso]);

  FParser.FeedData(TramaSim);
end;

procedure TfrmMainBascula.tmrLedRxOffTimer(Sender: TObject);
begin
  shpLedRx.Brush.Color := $00166534; // Verde oscuro / reposo
  tmrLedRxOff.Enabled := False;
end;

procedure TfrmMainBascula.tmrMonitorModemTimer(Sender: TObject);
begin
  ActualizarModemStatusUI;
end;

{ ==============================================================================
  RUTINAS DE DIAGNÓSTICO Y COMPROBACIÓN DE COMUNICACIÓN CON LA BÁSCULA
  ============================================================================== }

procedure TfrmMainBascula.btnTestDiagnosticoClick(Sender: TObject);
begin
  pgcDiagnostico.ActivePage := tsDiagnostico;
  btnEjecutarDiagnosticoClick(nil);
end;

procedure TfrmMainBascula.btnConmutarDTRClick(Sender: TObject);
begin
  if not FSerial.Connected then
  begin
    ShowMessage('Conecta primero el puerto COM.');
    Exit;
  end;
  FDTRActivo := not FDTRActivo;
  FSerial.SetLineDTR(FDTRActivo);
  if FDTRActivo then
    memDiagnostico.Lines.Add('[DTR] Línea DTR puesta en ALTO (ON)')
  else
    memDiagnostico.Lines.Add('[DTR] Línea DTR puesta en BAJO (OFF)');
  ActualizarModemStatusUI;
end;

procedure TfrmMainBascula.btnConmutarRTSClick(Sender: TObject);
begin
  if not FSerial.Connected then
  begin
    ShowMessage('Conecta primero el puerto COM.');
    Exit;
  end;
  FRTSActivo := not FRTSActivo;
  FSerial.SetLineRTS(FRTSActivo);
  if FRTSActivo then
    memDiagnostico.Lines.Add('[RTS] Línea RTS puesta en ALTO (ON)')
  else
    memDiagnostico.Lines.Add('[RTS] Línea RTS puesta en BAJO (OFF)');
  ActualizarModemStatusUI;
end;

procedure TfrmMainBascula.ProbarEnvioComandoDiag(const ACmd, ADesc: string);
var
  BytesIni, BytesFin: Int64;
  TIni: Cardinal;
begin
  if not FSerial.Connected then
  begin
    ShowMessage('Conecta primero el puerto COM.');
    Exit;
  end;

  memDiagnostico.Lines.Add(Format('>> Probando comando %s (%s)...', [ADesc, ACmd.Trim]));
  BytesIni := FSerial.TotalBytesReceived;

  FSerial.SendString(ACmd);

  TIni := GetTickCount;
  while (GetTickCount - TIni < 500) do
  begin
    Application.ProcessMessages;
    Sleep(20);
    if FSerial.TotalBytesReceived > BytesIni then
      Break;
  end;

  BytesFin := FSerial.TotalBytesReceived;
  if BytesFin > BytesIni then
  begin
    memDiagnostico.Lines.Add(Format('   [+] ¡RESPUESTA DETECTADA! Se recibieron %d bytes.', [BytesFin - BytesIni]));
    memDiagnostico.Lines.Add(Format('   [+] Si deseas lecturas periódicas, usa este comando (%s) con Sondeo (Polling) activo.', [ADesc]));
    edtPollingCmd.Text := ACmd;
    chkPolling.Checked := True;
  end
  else
  begin
    memDiagnostico.Lines.Add('   [-] Sin respuesta a este comando.');
  end;
end;

procedure TfrmMainBascula.btnProbarPClick(Sender: TObject);
begin
  ProbarEnvioComandoDiag('P'#13#10, 'P + CRLF (Común Gram/Baxtran)');
end;

procedure TfrmMainBascula.btnProbarWClick(Sender: TObject);
begin
  ProbarEnvioComandoDiag('W'#13#10, 'W + CRLF (Weight Query)');
end;

procedure TfrmMainBascula.btnProbarCRClick(Sender: TObject);
begin
  ProbarEnvioComandoDiag(#13, 'CR (Retorno de carro simple)');
end;

procedure TfrmMainBascula.btnProbarSIClick(Sender: TObject);
begin
  ProbarEnvioComandoDiag('SI'#13#10, 'SI + CRLF (Mettler Toledo / Sartorius)');
end;

procedure TfrmMainBascula.btnAutoBaudClick(Sender: TObject);
var
  Bauds: array[0..5] of Integer;
  Paridades: array[0..1] of TParityType;
  Bits: array[0..1] of Byte;
  B, P: Integer;
  BytesIni: Int64;
  TIni: Cardinal;
  Encontrado: Boolean;
  Puerto: string;
begin
  Puerto := cbPuertos.Text;
  if Pos(' ', Puerto) > 0 then
    Puerto := Copy(Puerto, 1, Pos(' ', Puerto) - 1);

  memDiagnostico.Lines.Add('====================================================');
  memDiagnostico.Lines.Add('INICIANDO ESCANEO AUTOMÁTICO DE VELOCIDADES (AUTO-BAUD)');
  memDiagnostico.Lines.Add('====================================================');

  if FSerial.Connected then
    FSerial.Close;

  Bauds[0] := 9600;
  Bauds[1] := 4800;
  Bauds[2] := 2400;
  Bauds[3] := 19200;
  Bauds[4] := 1200;
  Bauds[5] := 115200;

  Paridades[0] := ptNone; // 8-N-1
  Bits[0] := 8;

  Paridades[1] := ptEven; // 7-E-1 (Muy común en básculas)
  Bits[1] := 7;

  Encontrado := False;
  FSerial.PortName := Puerto;

  for B := 0 to High(Bauds) do
  begin
    for P := 0 to High(Paridades) do
    begin
      FSerial.BaudRate := Bauds[B];
      FSerial.DataBits := Bits[P];
      FSerial.Parity := Paridades[P];
      FSerial.StopBits := sbtOne;

      memDiagnostico.Lines.Add(Format('Probando: %d baudios, %d bits, Paridad=%d...',
        [Bauds[B], Bits[P], Ord(Paridades[P])]));

      if FSerial.Open then
      begin
        BytesIni := FSerial.TotalBytesReceived;

        // Escuchar continuo 600ms
        TIni := GetTickCount;
        while (GetTickCount - TIni < 600) do
        begin
          Application.ProcessMessages;
          Sleep(20);
          if FSerial.TotalBytesReceived > BytesIni then
            Break;
        end;

        // Si no, probar enviando 'P' y 'CR'
        if FSerial.TotalBytesReceived = BytesIni then
        begin
          FSerial.SendString('P'#13#10);
          Sleep(150);
          Application.ProcessMessages;
        end;
        if FSerial.TotalBytesReceived = BytesIni then
        begin
          FSerial.SendString(#13);
          Sleep(150);
          Application.ProcessMessages;
        end;

        if FSerial.TotalBytesReceived > BytesIni then
        begin
          memDiagnostico.Lines.Add(Format('¡¡DATOS DETECTADOS A %d BAUDIOS (Bits: %d, Paridad: %d)!!',
            [Bauds[B], Bits[P], Ord(Paridades[P])]));
          cbBaudRate.Text := IntToStr(Bauds[B]);
          if Bits[P] = 7 then cbDataBits.ItemIndex := 0 else cbDataBits.ItemIndex := 1;
          cbParidad.ItemIndex := Ord(Paridades[P]);
          Encontrado := True;
          Break;
        end;

        FSerial.Close;
      end;
    end;
    if Encontrado then Break;
  end;

  if not Encontrado then
  begin
    memDiagnostico.Lines.Add('----------------------------------------------------');
    memDiagnostico.Lines.Add('[-] No se recibieron bytes en ninguna velocidad probada.');
    memDiagnostico.Lines.Add('REVISIÓN CRÍTICA:');
    memDiagnostico.Lines.Add('1. Comprueba si la báscula requiere pulsar la tecla PRINT / ENVIAR en su teclado.');
    memDiagnostico.Lines.Add('2. Comprueba si el cable requiere adaptador cruzado (Null-Modem TX/RX).');
    memDiagnostico.Lines.Add('3. Comprueba en el menú del visor de la báscula que la salida RS232 esté habilitada.');
  end
  else
  begin
    ShowMessage('¡Configuración correcta detectada y aplicada en pantalla!');
  end;
end;

procedure TfrmMainBascula.btnEjecutarDiagnosticoClick(Sender: TObject);
begin
  EjecutarDiagnosticoCompleto;
end;

procedure TfrmMainBascula.EjecutarDiagnosticoCompleto;
var
  CTS, DSR, Ring, RLSD: Boolean;
  BytesIni, BytesFin: Int64;
  TIni: Cardinal;
  RespondioContinuo: Boolean;
  I: Integer;
  Cmds: array[0..5] of string;
  CmdNombres: array[0..5] of string;
  CmdExito: string;
begin
  memDiagnostico.Clear;
  memDiagnostico.Lines.Add('========================================================');
  memDiagnostico.Lines.Add('TEST DE COMUNICACIÓN CON BÁSCULA - DSD TECH SH-B35');
  memDiagnostico.Lines.Add('Fecha y hora: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now));
  memDiagnostico.Lines.Add('========================================================');

  // Paso 1: Conexión del puerto
  if not FSerial.Connected then
  begin
    memDiagnostico.Lines.Add('[1/4] Abriendo puerto serie ' + cbPuertos.Text + ' a ' + cbBaudRate.Text + ' baudios...');
    btnConectarClick(nil);
  end;

  if not FSerial.Connected then
  begin
    memDiagnostico.Lines.Add('[-] ERROR: No se pudo abrir el puerto serie. Comprueba que el SH-B35 esté emparejado.');
    Exit;
  end;

  memDiagnostico.Lines.Add('[+] Puerto serie abierto correctamente.');

  // Paso 2: Estado de líneas de control
  memDiagnostico.Lines.Add('[2/4] Verificando líneas de control de hardware...');
  FSerial.SetLineDTR(True);
  FSerial.SetLineRTS(True);
  memDiagnostico.Lines.Add('   -> Líneas DTR y RTS puestas en ALTO (alimentación / señal lista).');

  if FSerial.GetModemStatus(CTS, DSR, Ring, RLSD) then
  begin
    memDiagnostico.Lines.Add(Format('   -> Señales recibidas de la báscula: CTS=%s, DSR=%s',
      [BoolToStr(CTS, True), BoolToStr(DSR, True)]));
    if CTS or DSR then
      memDiagnostico.Lines.Add('   [+] ¡Señal eléctrica detectada! La báscula tiene conexión física con el adaptador.')
    else
      memDiagnostico.Lines.Add('   [i] Nota: Las líneas CTS/DSR están inactivas (normal en cables de 3 hilos TX/RX/GND).');
  end;

  // Paso 3: Escucha en modo continuo
  memDiagnostico.Lines.Add('[3/4] Escuchando si la báscula emite en modo continuo (1.5 segundos)...');
  BytesIni := FSerial.TotalBytesReceived;
  TIni := GetTickCount;
  while (GetTickCount - TIni < 1500) do
  begin
    Application.ProcessMessages;
    Sleep(20);
    if FSerial.TotalBytesReceived > BytesIni then
      Break;
  end;

  BytesFin := FSerial.TotalBytesReceived;
  RespondioContinuo := (BytesFin > BytesIni);

  if RespondioContinuo then
  begin
    memDiagnostico.Lines.Add(Format('   [+] ¡ÉXITO TOTAL! La báscula está transmitiendo en modo continuo (%d bytes recibidos).', [BytesFin - BytesIni]));
    memDiagnostico.Lines.Add('   -> Revisa el visor digital y la pestaña "Terminal" para ver las tramas recibidas.');
    Exit;
  end
  else
  begin
    memDiagnostico.Lines.Add('   [-] No se recibieron datos en modo continuo. La báscula puede requerir comando o pulsación.');
  end;

  // Paso 4: Pruebas de comandos de interrogación (Polling)
  memDiagnostico.Lines.Add('[4/4] Enviando comandos de petición de peso a la báscula...');
  Cmds[0] := 'P'#13#10;     CmdNombres[0] := 'P + CRLF (Estándar Gram, Baxtran, Dibal)';
  Cmds[1] := #13;           CmdNombres[1] := 'CR (Retorno de carro simple)';
  Cmds[2] := 'W'#13#10;     CmdNombres[2] := 'W + CRLF (Weight Query)';
  Cmds[3] := 'SI'#13#10;    CmdNombres[3] := 'SI + CRLF (Mettler Toledo / Sartorius)';
  Cmds[4] := 'Q'#13#10;     CmdNombres[4] := 'Q + CRLF (Query)';
  Cmds[5] := 'READ'#13#10;  CmdNombres[5] := 'READ + CRLF';

  CmdExito := '';
  for I := 0 to High(Cmds) do
  begin
    BytesIni := FSerial.TotalBytesReceived;
    FSerial.SendString(Cmds[I]);

    TIni := GetTickCount;
    while (GetTickCount - TIni < 400) do
    begin
      Application.ProcessMessages;
      Sleep(20);
      if FSerial.TotalBytesReceived > BytesIni then
        Break;
    end;

    if FSerial.TotalBytesReceived > BytesIni then
    begin
      CmdExito := Cmds[I];
      memDiagnostico.Lines.Add(Format('   [+] ¡RESPUESTA RECIBIDA con comando: %s! (%d bytes)',
        [CmdNombres[I], FSerial.TotalBytesReceived - BytesIni]));
      Break;
    end
    else
    begin
      memDiagnostico.Lines.Add('   [-] Sin respuesta a: ' + CmdNombres[I]);
    end;
  end;

  if CmdExito <> '' then
  begin
    memDiagnostico.Lines.Add('--------------------------------------------------------');
    memDiagnostico.Lines.Add('¡BÁSCULA DETECTADA EN MODO BAJO DEMANDA!');
    memDiagnostico.Lines.Add('Se ha configurado automáticamente el comando de sondeo.');
    edtPollingCmd.Text := CmdExito;
    chkPolling.Checked := True;
    tmrPolling.Interval := StrToIntDef(edtPollingInterval.Text, 300);
    tmrPolling.Enabled := True;
  end
  else
  begin
    memDiagnostico.Lines.Add('========================================================');
    memDiagnostico.Lines.Add('DIAGNÓSTICO FINAL: NO SE HAN RECIBIDO BYTES');
    memDiagnostico.Lines.Add('========================================================');
    memDiagnostico.Lines.Add('Posibles causas y soluciones:');
    memDiagnostico.Lines.Add('1. TECLA PRINT:');
    memDiagnostico.Lines.Add('   Muchas básculas solo envían el peso cuando el operario pulsa la tecla "PRINT", "ENVIAR" o "ENTER" en el visor.');
    memDiagnostico.Lines.Add('   -> Prueba a pulsar PRINT ahora mientras miras la pestaña Terminal.');
    memDiagnostico.Lines.Add('');
    memDiagnostico.Lines.Add('2. CRUCE DE PINES (CABLE NULL-MODEM vs DIRECTO):');
    memDiagnostico.Lines.Add('   El adaptador DSD TECH SH-B35 tiene conector DB9 con pines estándar (Pin 2=RX, Pin 3=TX, Pin 5=GND).');
    memDiagnostico.Lines.Add('   Si el visor de la báscula también tiene el Pin 2 como RX y el Pin 3 como TX (ambos son DTE), se necesita un');
    memDiagnostico.Lines.Add('   pequeño adaptador cruzado "Null-Modem" o cambiar el conector.');
    memDiagnostico.Lines.Add('');
    memDiagnostico.Lines.Add('3. VELOCIDAD Y PARIDAD DEL VISOR:');
    memDiagnostico.Lines.Add('   Pulsa el botón "Escanear Velocidades (Auto-Baud)" para probar a 4800, 2400, 19200 o paridad Par (7-E-1).');
  end;
end;

end.
