Jump to content
DavidJr.

Python4Delphi in a thread

Recommended Posts

Hi,

 

I am trying to call a Python (with Open3D) app UI from a Delphi form (11.3),  using a Tthread ..  it seems to call the method just fine but I do not see the window.  When not using it in a thread the window comes up just fine...  however I want to be able to terminate the process at will (from within the Delphi Form).  What do I need to do in order to make the python app be visible?    (Note: I am also using SDL Components for the input fields that handle numbers.)

 

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Threading, System.Math,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, PythonEngine, PythonVersions, ShellAPI,
  SDL_NumIO, SDL_stringl, Vcl.ExtCtrls, LJUDDelphi, Vcl.WinXCtrls, TlHelp32;

const
  PYHARV_HOME = 'C:\Users\printeruser\Desktop\3DScan_Work\photoneo-python-examples\GigEV\harvesters';
  VENV_PATH = PYHARV_HOME + '\.venv';
  VENV_PYTHON_EXE = VENV_PATH + '\Scripts\python.exe';
  PHOTONEO_GENICAM_PATH = PYHARV_HOME + '\advanced\photoneo_genicam';

  LJ_TIMER_INTERVAL  = 100;

  SCAN_MODE_MANUAL   = 0;
  SCAN_MODE_TIMER    = 1;
  SCAN_MODE_RELAY    = 2;
  // THREAD EVENT_ID_NUMS:
  CloseScanResultEventID        = 0;
  RunScanGetResultEventID       = 1;


type
  TPythonScriptThread = class(TThread)
  private
    FPythonEngine: TPythonEngine;
    FPythonInputOutput: TPythonInputOutput;
    FPyVersion: TPythonVersion;

    FScript: TStringList;

    FDevSerialNumber: String;
    FHorizontalRotation: Double;
    FVerticalRotation: Double;
    FScanZoomFactor: Double;
    FScriptRunning: Boolean;
    FPythonOpen3DPID: DWORD;

    FCloseScanResultEvent: THandle;
    FRunScanGetResultEvent: THandle;

    procedure SetEnvironmentVariables;
    procedure ConfigurePythonEnvironment;

    procedure Set_DevSerialNumber(Value: String);
    procedure Set_HorizontalRotation(Value: Double);
    procedure Set_VerticalRotation(Value: Double);
    procedure Set_ScanZoomFactor(Value: Double);

    // Thread Event triggered methods:
    function _ExecuteStopPythonScript: Boolean;
    function _ExecuteRunPythonScript: Boolean;

    // Private Thread member methods that are called by UI invoking a thread-safe event method:
    function mFindPythonOpen3DSubprocess: DWORD;

  protected
    procedure Execute; override;
  public
    constructor Create;
    destructor Destroy; override;

    // Read Only Property:
    property ScriptRunning: Boolean read FScriptRunning;


    // The publicly accessible methods:
    function StopPythonScript: Boolean;
    function RunPythonScript(DevID: String; HRot: Double = 0; VRot: Double = 0; Zoom: Double = 1): Boolean;
  end;

  TForm1 = class(TForm)
    Panel1: TPanel;
    nioVerticalRotation: TNumIO2;
    PythonEngine1: TPythonEngine;
    RadioGroup1: TRadioGroup;
    editDevSerialNumber: TEdit;
    nioHorizontalRotation: TNumIO2;
    nioZoomFactor: TNumIO2;
    btnScanNow: TButton;
    lblDeviceSerialNum_Label: TLabel;
    Timer1: TTimer;
    nioTimerInterval: TNumIO2;
    lblLabJackStatus: TLabel;
    uiRelay_0_Voltage: TLabel;
    uiRelay_1_Voltage: TLabel;
    procedure btnScanNowClick(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure RadioGroup1Click(Sender: TObject);
    procedure nioTimerIntervalChange(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure nioZoomFactorChange(Sender: TObject);
    procedure Panel1DblClick(Sender: TObject);

  private
    { Private declarations }
    PythonScriptThread: TPythonScriptThread;

    // LabJack Vars for the class:
    FLabJackConnected: Boolean;
    FRelayMonitorLatch: Boolean;
    FLJRelay_0_ON, FLJRelay_1_ON: Boolean;
    FLJErrorcode: LJ_ERROR;   // LabJack Error Code
    FLJHandle: LJ_HANDLE;     // Handle for LabJack device
    FLJVoltage_0, FLJVoltage_1 : double;
    // Timer & TDateTime Trigger:
    LastTriggerTime: TDateTime;
    // The rest of th eclass vars:
    FTimerTriggeredScan: Boolean;
    FRelayTriggeredScan: Boolean;
    FTimeTrigScanInterval: Integer;


    procedure RequestElevationIfNeeded;
    function IsUserAnAdmin: Boolean;
    // Trigger procedures:
    procedure TriggerByUserToScan;
    procedure TriggerByTimerToScan;
    procedure TriggerByRelayToScan;
    // LabJack:
    procedure Initialize_U3HV;
    procedure ErrorHandler(LJErrorcode: LJ_ERROR; LJIteration: longint);
    procedure SetDACVoltage(Channel: Integer; TheSetVoltage: Double);
    procedure GetAnalogVoltage(Channel: Integer; var TheVoltage: Double);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

// The Rest of the Code:
function TForm1.IsUserAnAdmin: Boolean;
var
  hToken: THandle;
  Elevation: TOKEN_ELEVATION;
  cbSize: DWORD;
begin
  Result := False;
  if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, hToken) then
  try
    cbSize := SizeOf(TOKEN_ELEVATION);
    if GetTokenInformation(hToken, TokenElevation, @Elevation, cbSize, cbSize) then
      Result := Elevation.TokenIsElevated <> 0;
  finally
    CloseHandle(hToken);
  end;
end;


procedure TForm1.RequestElevationIfNeeded;
var
  sei: TShellExecuteInfo;
  exeName: string;
begin
  if not IsUserAnAdmin then
  begin
    ZeroMemory(@sei, SizeOf(sei));
    sei.cbSize := SizeOf(sei);
    sei.Wnd := Handle; // Parent window
    sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
    sei.lpVerb := 'runas'; // Request elevation
    exeName := ParamStr(0);
    sei.lpFile := PChar(exeName);
    sei.lpParameters := PChar(''); // No parameters
    sei.nShow := SW_SHOWNORMAL;

    if not ShellExecuteEx(@sei) then
    begin
      ShowMessage('Failed to create elevated process. Error code: ' + IntToStr(GetLastError));
      Application.Terminate; // Terminate the unelevated instance
    end
    else
      Application.Terminate; // Terminate the unelevated instance
  end;
end;

// LabJack:
procedure TForm1.Initialize_U3HV;
var
  numChannels, quickSample, longSettling: longint;
  LabJack_Check : integer;
begin
  // This performs the standard initialization of the U3-HV system
  // Some initial values
  LabJack_Check := 0;
  FLJHandle := 0;
  numChannels := 16; //Number of AIN channels, 0-16.
  quickSample := 0; //Set to TRUE for quick AIN sampling.
  longSettling := 1; //Set to TRUE for extra AIN settling time.

  // Now let's open the labjack
  FLJErrorcode := OpenLabJack(LJ_dtU3, LJ_ctUSB, '1', 1, FLJHandle);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);


  // Now let's reset the configuration
  FLJErrorcode := ePut(FLJHandle, LJ_ioPIN_CONFIGURATION_RESET, 0, 0, 0);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  //Configure quickSample.
  FLJErrorcode := ePut(FLJHandle, LJ_ioPUT_CONFIG, LJ_chAIN_RESOLUTION, quickSample, 0);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  //Configure longSettling.
  FLJErrorcode := ePut(FLJHandle, LJ_ioPUT_CONFIG, LJ_chAIN_SETTLING_TIME, longSettling, 0);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  //Configure the lines as analog.
  FLJErrorcode := ePut(FLJHandle, LJ_ioPUT_ANALOG_ENABLE_PORT, 0, (IntPower(2, numChannels) - 1), numChannels);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  if LabJack_Check = 0 then
    begin
      lblLabJackStatus.Font.Color := clLime;
      lblLabJackStatus.Caption := 'LabJack Connected';
    end
  else
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
    end;

  // Let's set the DAC0 and DAC1 to 0.0 Volts at boot-up
  SetDACVoltage(0, 0.0);
  SetDACVoltage(1, 0.0);
end;

procedure TForm1.ErrorHandler(LJErrorcode: LJ_ERROR; LJIteration: longint);
var
  err: array[0..254] of AnsiChar;
begin
  if LJErrorcode <> LJE_NOERROR then
    begin
      ErrorToString(LJErrorcode, @err);
      //StatusUpdate('Error number = ' + IntToStr(lngErrorcode)+ ' Error string = ' + string(err) + ' Iteration = ' + IntToStr(LJIteration));
      FLabJackConnected := False; // there was an error!
    end
  else
    begin
      // LabJack Operation is OK
      FLabJackConnected := True;
    end;
end;

procedure TForm1.SetDACVoltage(Channel: Integer; TheSetVoltage: Double);
begin
  if FLabJackConnected then
    begin
      // This uses the global lngHandle to refer to the U3-HV
      if ABS(TheSetVoltage) < 10 then
        ePUT(FLJHandle, LJ_ioPUT_DAC, Channel, TheSetVoltage, 0)
      else
        begin
          // Add an error statement if the DAC is set overrange...!
          lblLabJackStatus.Font.Color := clRed;
          lblLabJackStatus.Caption := 'LabJack Connected & Overrange!';
        end;
    end
  else
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
      FLabJackConnected := False;
      ShowMessage('Problem connecting to LabJack! Check Power and USB connection.');
    end;
end;

procedure TForm1.GetAnalogVoltage(Channel: Integer; var TheVoltage: Double);
begin
  // This uses the global lngHandle to read a voltage from the Analog Channels
  //
  // The "LJ_ioGET_AIN" is the integer that determines the operation. Refer to
  // LJUDDelphi.pas for details
  if FLabJackConnected then
    begin
      eGET(FLJHandle, LJ_ioGET_AIN, Channel, TheVoltage, 0);
    end
  else
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
      FLabJackConnected := False;
      ShowMessage('Problem connecting to LabJack! Check Power and USB connection.');
    end;

  // Update the Text readings
  if Channel = 0 then
    uiRelay_0_Voltage.Caption := 'Relay 0 Voltage = '+ Strff(FLJVoltage_0, 4, 3)+' V';
  if Channel = 1 then
    uiRelay_1_Voltage.Caption := 'Relay 1 Voltage = '+ Strff(FLJVoltage_1, 4, 3)+' V';


end;

// Python script thread implementation
constructor TPythonScriptThread.Create;
begin
  inherited Create(False);
  FPythonEngine := TPythonEngine.Create(Form1);
  FPythonInputOutput := TPythonInputOutput.Create(Form1);

  if PythonVersionFromPath(PYHARV_HOME + '\.venv\', FPyVersion) then
  begin
    // Create the Events:
    FCloseScanResultEvent := CreateEvent(nil, True, False, nil);
    FRunScanGetResultEvent := CreateEvent(nil, True, False, nil);

    FPythonEngine.VenvPythonExe := VENV_PYTHON_EXE;
    FPythonEngine.SetPythonHome(PYHARV_HOME + '\.venv\');
    FPyVersion.AssignTo(FPythonEngine);
    FPythonEngine.LoadDll;
    // ShowMessage('Python DLL loaded successfully. ' + PythonEngine1.DllName);
    ConfigurePythonEnvironment;
  end
end;

destructor TPythonScriptThread.Destroy;
begin
  FScript.Free;
  inherited;
end;

// The publicly accessible methods:
function TPythonScriptThread.StopPythonScript: Boolean;
begin
  //CloseScanResultEventID
  SetEvent(FCloseScanResultEvent);
end;

function TPythonScriptThread.RunPythonScript(DevID: String; HRot: Double = 0; VRot: Double = 0; Zoom: Double = 1): Boolean;
begin
  Result := False;
  // Set the Params:
  FDevSerialNumber := DevID;
  FHorizontalRotation := HRot;
  FVerticalRotation := VRot;
  FScanZoomFactor:= (100 / Zoom);
  //RunScanGetResultEventID
  if(FDevSerialNumber <> '') AND
    (Not FHorizontalRotation.IsNan) AND (Not FVerticalRotation.IsNan) AND (Not FScanZoomFactor.IsNan) then
      begin
        SetEvent(FRunScanGetResultEvent);
        Result := True;
      end;
end;

procedure TPythonScriptThread.Execute;
var
  EventHandles: array[0..1] of THandle;
  EventIndex: DWORD;

begin
  EventHandles[CloseScanResultEventID] := FCloseScanResultEvent;
  EventHandles[RunScanGetResultEventID] := FRunScanGetResultEvent;

  while(Not Terminated) do
    begin
      EventIndex := WaitForMultipleObjects(2, @EventHandles, False, 60);

      case EventIndex of
        WAIT_OBJECT_0 + CloseScanResultEventID: // FJogStopEvent signaled
          begin
            // Handle FCloseScanResultEvent signaled case
            _ExecuteStopPythonScript;
            ResetEvent(FCloseScanResultEvent);
          end;

        WAIT_OBJECT_0 + RunScanGetResultEventID: // FJogStopEvent signaled
          begin
            // Handle FRunScanGetResultEvent signaled case
            _ExecuteRunPythonScript;
            ResetEvent(FRunScanGetResultEvent);
          end;
      end;
    end;

  CloseHandle(FCloseScanResultEvent);
  CloseHandle(FRunScanGetResultEvent);
end;

// Python uses Open3D Library modules:
function TPythonScriptThread.mFindPythonOpen3DSubprocess: DWORD;
var
  Snapshot: THandle;
  ProcessEntry: TProcessEntry32;
begin
  Result := 0;
  Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if Snapshot <> INVALID_HANDLE_VALUE then
  try
    ProcessEntry.dwSize := SizeOf(ProcessEntry);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if Pos('Open3D', ProcessEntry.szExeFile) > 0 then
        begin
          Result := ProcessEntry.th32ProcessID;
          Break;
        end;
      until Not Process32Next(Snapshot, ProcessEntry);
    end;
  finally
    CloseHandle(Snapshot);
  end;
end;

function TPythonScriptThread._ExecuteStopPythonScript: Boolean;
begin

end;

// The Actual Python execution:
function TPythonScriptThread._ExecuteRunPythonScript: Boolean;
var
  Script: TStringList;
  ScriptFilePath, HorizontalRotation, VerticalRotation, ZoomFactor: string;
begin
  // Prevent Double Clicking...  since the python script needs to complete first before re-running:
  FScriptRunning := True;

  Script := TStringList.Create;
  try
    // Retrieve the values from the form inputs


    ScriptFilePath := PYHARV_HOME + '\advanced\GridLoigic_pointcloud_with_normals_and_texture.py';

    if FileExists(ScriptFilePath) then
    begin
      Script.LoadFromFile(ScriptFilePath);

      HorizontalRotation := FHorizontalRotation.ToString;
      VerticalRotation := FVerticalRotation.ToString;
      ZoomFactor := FScanZoomFactor.ToString;

      // Insert the sys.path and debug statements
      Script.Insert(2, 'sys.path.append(r"' + PYHARV_HOME + '\advanced")');
      Script.Insert(3, 'print("Starting script execution")');
      Script.Insert(4, 'try:');
      Script.Insert(5, '    import photoneo_genicam');
      Script.Insert(6, '    print("photoneo_genicam imported successfully")');
      Script.Insert(7, 'except ImportError as e:');
      Script.Insert(8, '    print("Failed to import photoneo_genicam:", str(e))');
      Script.Insert(9, '    raise');

      // Add the parameters as comments for reference
      Script.Insert(10, '# Parameters passed from Delphi');
      Script.Insert(11, 'device_id = "' + FDevSerialNumber + '"');
      Script.Insert(12, 'horizontal_angle = ' + HorizontalRotation);
      Script.Insert(13, 'vertical_angle = ' + VerticalRotation);
      Script.Insert(14, 'zoom_factor = ' + ZoomFactor);

      // Replace main function call with parameters
      //Script.Add('main(device_id, horizontal_angle, vertical_angle, zoom_factor)');

      try
        FPythonEngine.ExecStrings(Script);
        //ShowMessage('Script executed successfully.');
      except
        on E: EPySystemExit do
        begin
          // Handle the SystemExit exception
          ShowMessage('Script terminated by SystemExit.');
        end;
        on E: EPythonError do
        begin
          ShowMessage('Python Error: ' + E.Message);
        end;
        on E: Exception do
        begin
          ShowMessage('Error: ' + E.Message);
        end;
      end;
    end
    else
    begin
      ShowMessage('Script file does not exist: ' + ScriptFilePath);
    end;
  finally
    Script.Free;
  end;
end;

procedure TPythonScriptThread.SetEnvironmentVariables;
begin
  SetEnvironmentVariable('VIRTUAL_ENV', PChar(VENV_PATH));
  SetEnvironmentVariable('PYTHONHOME', nil);  // Unset PYTHONHOME
  SetEnvironmentVariable('PATH', PChar(VENV_PATH + '\Scripts;' + GetEnvironmentVariable('PATH')));
end;

procedure TPythonScriptThread.ConfigurePythonEnvironment;
begin
  MaskFPUExceptions(True);

  // Ensure the virtual environment paths are correctly set
  SetEnvironmentVariables;

  // Configure Python environment
  FPythonEngine.ExecString(Format(
    'import sys; import os; ' +
    'sys.path.append(r"%s"); ' +
    'sys.path.append(r"%s\Lib\site-packages"); ' +
    'sys.path.append(r"%s"); ' +  // Add path to photoneo_genicam
    'os.environ["VIRTUAL_ENV"] = r"%s"; ' +
    'os.environ["PATH"] = r"%s\Scripts;" + os.environ["PATH"]; ' +
    'print("sys.path:", sys.path); ' +
    'print("VIRTUAL_ENV:", os.environ["VIRTUAL_ENV"]); ' +
    'print("PATH:", os.environ["PATH"])',
    [VENV_PATH, VENV_PATH, PHOTONEO_GENICAM_PATH, VENV_PATH, VENV_PATH]
  ));
end;

procedure TPythonScriptThread.Set_DevSerialNumber(Value: String);
begin
  FDevSerialNumber := Value;
end;

procedure TPythonScriptThread.Set_HorizontalRotation(Value: Double);
begin
  FHorizontalRotation := Value;
end;

procedure TPythonScriptThread.Set_VerticalRotation(Value: Double);
begin
  FVerticalRotation := Value;
end;

procedure TPythonScriptThread.Set_ScanZoomFactor(Value: Double);
begin
  FScanZoomFactor := Value;
end;

// Form stuff:

procedure TForm1.nioTimerIntervalChange(Sender: TObject);
begin
  try
    if(Not nioTimerInterval.Value.IsNan) then
      FTimeTrigScanInterval := nioTimerInterval.IntegerValue;

  except
    FTimeTrigScanInterval := 10;
    nioTimerInterval.Value := FTimeTrigScanInterval;
  end;
end;

procedure TForm1.nioZoomFactorChange(Sender: TObject);
begin
  try
    if(nioZoomFactor.Value.IsNan) AND (Assigned(PythonScriptThread)) then
      nioTimerInterval.Value := 200;

  except

  end;
end;

procedure TForm1.Panel1DblClick(Sender: TObject);
begin
  // Reset fields:
  if(GetKeyState(VK_SHIFT) < 0) then
    begin
      nioHorizontalRotation.Value := 360;
      nioVerticalRotation.Value := 180;
      nioZoomFactor.Value := 200;
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  RequestElevationIfNeeded;

  FTimerTriggeredScan := False;
  FRelayTriggeredScan := False;

  Timer1.Interval := LJ_TIMER_INTERVAL;
end;

procedure TForm1.FormActivate(Sender: TObject);
begin
  if(Not DirectoryExists(PYHARV_HOME)) then
    begin
      ShowMessage(PYHARV_HOME + ' doesn''t exist!');
    end
  else
    begin

      // Load the Python DLL using the virtual environment's Python executable
      PythonScriptThread := TPythonScriptThread.Create;

      Sleep(10);
      Application.ProcessMessages;
      Sleep(10);
      Application.ProcessMessages;

      if(Assigned(PythonScriptThread)) then
        begin
          try
            FTimeTrigScanInterval := nioTimerInterval.IntegerValue;

          except
            on E: Exception do
              ShowMessage('Failed to load Python DLL: ' + E.Message);
          end;


          FTimeTrigScanInterval := nioTimerInterval.IntegerValue;

          LastTriggerTime := Now;

          // try to connect to Lab Jack:
          Initialize_U3HV;
          // Now Check if its connected:
          if(Not FLabJackconnected) then
            begin
              lblLabJackStatus.Font.Color := clRed;
              lblLabJackStatus.Caption := 'LabJack Disonnected!';
            end;
        end;
    end;

end;

procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
  case RadioGroup1.ItemIndex of
    SCAN_MODE_MANUAL:
      begin
        FTimerTriggeredScan := False;
        FRelayTriggeredScan := False;
        btnScanNow.Enabled := (Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan);
        nioTimerInterval.Enabled := FTimerTriggeredScan;
      end;

    SCAN_MODE_TIMER:
      begin
        FTimerTriggeredScan := True;
        FRelayTriggeredScan := False;
        btnScanNow.Enabled := (Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan);
        nioTimerInterval.Enabled := FTimerTriggeredScan;
      end;

    SCAN_MODE_RELAY:
      begin
        FTimerTriggeredScan := False;
        FRelayTriggeredScan := True;
        btnScanNow.Enabled := (Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan);
        nioTimerInterval.Enabled := FTimerTriggeredScan;
      end;

    else
      begin
        RadioGroup1.ItemIndex := 0;
        btnScanNow.Enabled := True;
        nioTimerInterval.Enabled := False;
      end;
  end;
end;

procedure TForm1.btnScanNowClick(Sender: TObject);
begin
  btnScanNow.Enabled := False;
  if PythonScriptThread.ScriptRunning then
    begin
      ShowMessage('Scan is still busy.');
      Exit;
    end;
  Application.ProcessMessages;
  if(Tbutton(Sender).Name = 'btnScanNow') then
    begin
      TriggerByUserToScan;
    end;
  btnScanNow.Enabled := (Not PythonScriptThread.ScriptRunning);
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if(FTimerTriggeredScan OR FRelayTriggeredScan) then
    begin
      Timer1.Enabled := False;

      //Check for Triggers:
      if(Not FTimerTriggeredScan) then
        TriggerByRelayToScan;

      if(Not FRelayTriggeredScan) then
        TriggerByTimerToScan;

      Timer1.Enabled := True;
    end;
end;

procedure TForm1.TriggerByUserToScan;
begin
  if((Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan)) then
    begin
      if(Not PythonScriptThread.RunPythonScript(
                                      editDevSerialNumber.Text,
                                      nioHorizontalRotation.Value,
                                      nioVerticalRotation.Value,
                                      nioZoomFactor.Value
                                                  )) then
        begin
          //Error:
          ShowMessage('Could not call process!');
        end;
    end;
end;

procedure TForm1.TriggerByTimerToScan;
var
  myCurrentTime: TDateTime;
  myTriggerTimeElapsed: Double;

begin
  myCurrentTime := Now;

  // Calculate time elapsed since the last execution of UpdateMassChart
  myTriggerTimeElapsed := (myCurrentTime - LastTriggerTime) * 86400; // Convert to seconds

  if(myTriggerTimeElapsed >= FTimeTrigScanInterval) then
    begin
      PythonScriptThread.StopPythonScript;
      LastTriggerTime := myCurrentTime;
      PythonScriptThread.RunPythonScript(
                                      editDevSerialNumber.Text,
                                      nioHorizontalRotation.Value,
                                      nioVerticalRotation.Value,
                                      nioZoomFactor.Value
                                                  );
    end;
end;

procedure TForm1.TriggerByRelayToScan;
var
  ActiveFileName : String;
  myActiveFileCheckSum: String;

begin
  // Enable the Print Monitor Button
  if(FLabJackConnected AND FRelayTriggeredScan AND (Not FTimerTriggeredScan)) then
    begin
      // After the first read.....The Monitorlatch is FALSE....it should only be true
      // again if BOTH voltages are less than 2.0V!!!!

      // Just get the voltage readings from the Labjack
      GetAnalogVoltage(0, FLJVoltage_0);
      GetAnalogVoltage(1, FLJVoltage_1);

      if ((FLJVoltage_0 < 2.0) and (FLJVoltage_1 < 2.0)) then
        FRelayMonitorLatch := True;


      // Update the Relay Lights Status
      if(FLJVoltage_0 > 2.0) then
        begin
          // Use 2V as the trigger voltage level for now
          //uiRelay_0_Light.Fill.Color := TAlphaColorRec.Lime;
          FLJRelay_0_ON := True;
        end
      else
        begin
          //uiRelay_0_Light.Fill.Color := TAlphaColorRec.Crimson;
          FLJRelay_0_ON := False;
        end;

      if(FLJVoltage_1 > 2.0) then
        begin
          // Use 2V as the trigger voltage level for now
          //uiRelay_1_Light.Fill.Color := TAlphaColorRec.Lime;
          FLJRelay_1_ON := True;
        end
      else
        begin
          //uiRelay_1_Light.Fill.Color := TAlphaColorRec.Crimson;
          FLJRelay_1_ON := False;
        end;


      // Now let's figure out if we need to take a photo
      //
      // The camera will be "inactive" most of the time
      // Make it active first....
      //
      // then wait for Relay_1 to turn ON
      //
      // Take the photo and turn Relay 1 OFF and Turn Relay 0 OFF
      //
      if FLJRelay_0_ON then
        begin
          // This means the camera is "active" and updating images
          //
          // Now let's see if Relay 1 is ON
          if FLJRelay_1_ON then
            begin
              // Here you should execute scan!
              //
              if FRelayMonitorLatch then
                begin
                  PythonScriptThread.StopPythonScript;
                  // Now let's write the file!
                  PythonScriptThread.RunPythonScript(
                                      editDevSerialNumber.Text,
                                      nioHorizontalRotation.Value,
                                      nioVerticalRotation.Value,
                                      nioZoomFactor.Value
                                                  );
                end;

              // Now set the MonitorLatch to False!
              FRelayMonitorLatch := False;
            end;
        end;

    end;

end;

end.

 

 

Here is the python script:

 

#!/usr/bin/env python
import sys
from pathlib import Path

import open3d as o3d
import numpy as np
from genicam.genapi import NodeMap
from harvesters.core import Component2DImage, Harvester

from photoneo_genicam.components import enable_components, enabled_components
from photoneo_genicam.default_gentl_producer import producer_path
from photoneo_genicam.features import enable_software_trigger
from photoneo_genicam.pointcloud import create_3d_vector, map_texture
from photoneo_genicam.user_set import load_default_user_set
from photoneo_genicam.visualizer import render_static

def rotate_point_cloud(point_cloud, horizontal_angle_degrees, vertical_angle_degrees):
    # Convert angles from degrees to radians
    horizontal_angle_radians = np.deg2rad(horizontal_angle_degrees)
    vertical_angle_radians = np.deg2rad(vertical_angle_degrees)

    # Define the rotation matrix for horizontal rotation (around Y-axis)
    horizontal_rotation_matrix = np.array([
        [np.cos(horizontal_angle_radians), 0, np.sin(horizontal_angle_radians)],
        [0, 1, 0],
        [-np.sin(horizontal_angle_radians), 0, np.cos(horizontal_angle_radians)]
    ])

    # Define the rotation matrix for vertical rotation (around X-axis)
    vertical_rotation_matrix = np.array([
        [1, 0, 0],
        [0, np.cos(vertical_angle_radians), -np.sin(vertical_angle_radians)],
        [0, np.sin(vertical_angle_radians), np.cos(vertical_angle_radians)]
    ])

    # Apply the rotations to the point cloud
    point_cloud.rotate(horizontal_rotation_matrix, center=(0, 0, 0))
    point_cloud.rotate(vertical_rotation_matrix, center=(0, 0, 0))

def render_with_zoom(point_cloud, zoom_factor):
    vis = o3d.visualization.Visualizer()
    vis.create_window()
    vis.add_geometry(point_cloud)
    
    # Get the view control and set the zoom level
    view_control = vis.get_view_control()
    view_control.set_zoom(zoom_factor)

    vis.run()
    vis.destroy_window()

def main(device_sn: str, horizontal_angle: float, vertical_angle: float, zoom_factor: float):
    with Harvester() as h:
        h.add_file(str(producer_path), check_existence=True, check_validity=True)
        h.update()

        with h.create({"serial_number": device_sn}) as ia:
            features: NodeMap = ia.remote_device.node_map

            load_default_user_set(features)
            enable_software_trigger(features)

            features.Scan3dOutputMode.value = "CalibratedABC_Grid"
            enable_components(features, ["Intensity", "Range", "Normal"])

            ia.start()
            features.TriggerSoftware.execute()
            with ia.fetch(timeout=10) as buffer:
                components = dict(zip(enabled_components(features), buffer.payload.components))
                intensity_component: Component2DImage = components["Intensity"]
                point_cloud_raw: Component2DImage = components["Range"]
                normal_component: Component2DImage = components["Normal"]

                point_cloud = o3d.geometry.PointCloud()
                point_cloud.points = create_3d_vector(point_cloud_raw.data)
                point_cloud.normals = create_3d_vector(normal_component.data)
                point_cloud.colors = map_texture(intensity_component)

                # Rotate the point cloud as specified by the command-line arguments
                rotate_point_cloud(point_cloud, horizontal_angle, vertical_angle)

                # Render the point cloud with the specified zoom factor
                render_with_zoom(point_cloud, zoom_factor)

if __name__ == "__main__":
    # Set default values
    #device_id = "FJE-061"
    #horizontal_angle = 360
    #vertical_angle = 180
    #zoom_factor = 0.1

    # Override default values with command-line arguments if provided
    if len(sys.argv) > 1:
        device_id = sys.argv[1]
    if len(sys.argv) > 2:
        horizontal_angle = float(sys.argv[2])
    if len(sys.argv) > 3:
        vertical_angle = float(sys.argv[3])
    if len(sys.argv) > 4:
        zoom_factor = float(sys.argv[4])

    # Run main function with values
    if len(sys.argv) > 4:
        main(device_id, horizontal_angle, vertical_angle, zoom_factor)
 

 

Share this post


Link to post

Using the "ThreadPythonExec" method, I tried this:

 

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Threading, System.Math,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, PythonEngine, PythonVersions, ShellAPI,
  SDL_NumIO, SDL_stringl, Vcl.ExtCtrls, LJUDDelphi, Vcl.WinXCtrls, TlHelp32;

const
  PYHARV_HOME = 'C:\Users\printeruser\Desktop\3DScan_Work\photoneo-python-examples\GigEV\harvesters';
  VENV_PATH = PYHARV_HOME + '\.venv';
  VENV_PYTHON_EXE = VENV_PATH + '\Scripts\python.exe';
  PHOTONEO_GENICAM_PATH = PYHARV_HOME + '\advanced\photoneo_genicam';

  LJ_TIMER_INTERVAL  = 100;

  SCAN_MODE_MANUAL   = 0;
  SCAN_MODE_TIMER    = 1;
  SCAN_MODE_RELAY    = 2;

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    nioVerticalRotation: TNumIO2;
    PythonInputOutput1: TPythonInputOutput;
    PythonEngine1: TPythonEngine;
    RadioGroup1: TRadioGroup;
    editDevSerialNumber: TEdit;
    nioHorizontalRotation: TNumIO2;
    nioZoomFactor: TNumIO2;
    btnScanNow: TButton;
    lblDeviceSerialNum_Label: TLabel;
    Timer1: TTimer;
    nioTimerInterval: TNumIO2;
    lblLabJackStatus: TLabel;
    uiRelay_0_Voltage: TLabel;
    uiRelay_1_Voltage: TLabel;
    procedure btnScanNowClick(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure RadioGroup1Click(Sender: TObject);
    procedure nioTimerIntervalChange(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure nioZoomFactorChange(Sender: TObject);
    procedure Panel1DblClick(Sender: TObject);

  private
    { Private declarations }
    // LabJack Vars for the class:
    FLabJackConnected: Boolean;
    FRelayMonitorLatch: Boolean;
    FLJRelay_0_ON, FLJRelay_1_ON: Boolean;
    FLJErrorcode: LJ_ERROR;   // LabJack Error Code
    FLJHandle: LJ_HANDLE;     // Handle for LabJack device
    FLJVoltage_0, FLJVoltage_1 : double;
    // Timer & TDateTime Trigger:
    LastTriggerTime: TDateTime;
    // The rest of th eclass vars:
    FTimerTriggeredScan: Boolean;
    FRelayTriggeredScan: Boolean;
    FTimeTrigScanInterval: Integer;
    FScanZoomFactor: Double;
    FScriptRunning: Boolean;
    FPythonOpen3DPID: DWORD;
    PyVersion: TPythonVersion;

    procedure SetEnvironmentVariables;
    procedure ConfigurePythonEnvironment;
    function FindPythonOpen3DSubprocess: DWORD;
    function StopPythonScript: Boolean;
    function RunPythonScript: Boolean;
    procedure RequestElevationIfNeeded;
    function IsUserAnAdmin: Boolean;
    // Trigger procedures:
    procedure TriggerByUserToScan;
    procedure TriggerByTimerToScan;
    procedure TriggerByRelayToScan;
    // LabJack:
    procedure Initialize_U3HV;
    procedure ErrorHandler(LJErrorcode: LJ_ERROR; LJIteration: longint);
    procedure SetDACVoltage(Channel: Integer; TheSetVoltage: Double);
    procedure GetAnalogVoltage(Channel: Integer; var TheVoltage: Double);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

// LabJack:
procedure TForm1.Initialize_U3HV;
var
  numChannels, quickSample, longSettling: longint;
  LabJack_Check : integer;
begin
  // This performs the standard initialization of the U3-HV system
  // Some initial values
  LabJack_Check := 0;
  FLJHandle := 0;
  numChannels := 16; //Number of AIN channels, 0-16.
  quickSample := 0; //Set to TRUE for quick AIN sampling.
  longSettling := 1; //Set to TRUE for extra AIN settling time.

  // Now let's open the labjack
  FLJErrorcode := OpenLabJack(LJ_dtU3, LJ_ctUSB, '1', 1, FLJHandle);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);


  // Now let's reset the configuration
  FLJErrorcode := ePut(FLJHandle, LJ_ioPIN_CONFIGURATION_RESET, 0, 0, 0);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  //Configure quickSample.
  FLJErrorcode := ePut(FLJHandle, LJ_ioPUT_CONFIG, LJ_chAIN_RESOLUTION, quickSample, 0);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  //Configure longSettling.
  FLJErrorcode := ePut(FLJHandle, LJ_ioPUT_CONFIG, LJ_chAIN_SETTLING_TIME, longSettling, 0);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  //Configure the lines as analog.
  FLJErrorcode := ePut(FLJHandle, LJ_ioPUT_ANALOG_ENABLE_PORT, 0, (IntPower(2, numChannels) - 1), numChannels);
  ErrorHandler(FLJErrorcode, 0);
  if FLJErrorcode > 0 then INC(LabJack_Check);

  if LabJack_Check = 0 then
    begin
      lblLabJackStatus.Font.Color := clLime;
      lblLabJackStatus.Caption := 'LabJack Connected';
    end
  else
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
    end;

  // Let's set the DAC0 and DAC1 to 0.0 Volts at boot-up
  SetDACVoltage(0, 0.0);
  SetDACVoltage(1, 0.0);
end;

procedure TForm1.ErrorHandler(LJErrorcode: LJ_ERROR; LJIteration: longint);
var
  err: array[0..254] of AnsiChar;
begin
  if LJErrorcode <> LJE_NOERROR then
    begin
      ErrorToString(LJErrorcode, @err);
      //StatusUpdate('Error number = ' + IntToStr(lngErrorcode)+ ' Error string = ' + string(err) + ' Iteration = ' + IntToStr(LJIteration));
      FLabJackConnected := False; // there was an error!
    end
  else
    begin
      // LabJack Operation is OK
      FLabJackConnected := True;
    end;
end;

procedure TForm1.SetDACVoltage(Channel: Integer; TheSetVoltage: Double);
begin
  if FLabJackConnected then
    begin
      // This uses the global lngHandle to refer to the U3-HV
      if ABS(TheSetVoltage) < 10 then
        ePUT(FLJHandle, LJ_ioPUT_DAC, Channel, TheSetVoltage, 0)
      else
        begin
          // Add an error statement if the DAC is set overrange...!
          lblLabJackStatus.Font.Color := clRed;
          lblLabJackStatus.Caption := 'LabJack Connected & Overrange!';
        end;
    end
  else
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
      FLabJackConnected := False;
      ShowMessage('Problem connecting to LabJack! Check Power and USB connection.');
    end;
end;

procedure TForm1.GetAnalogVoltage(Channel: Integer; var TheVoltage: Double);
begin
  // This uses the global lngHandle to read a voltage from the Analog Channels
  //
  // The "LJ_ioGET_AIN" is the integer that determines the operation. Refer to
  // LJUDDelphi.pas for details
  if FLabJackConnected then
    begin
      eGET(FLJHandle, LJ_ioGET_AIN, Channel, TheVoltage, 0);
    end
  else
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
      FLabJackConnected := False;
      ShowMessage('Problem connecting to LabJack! Check Power and USB connection.');
    end;

  // Update the Text readings
  if Channel = 0 then
    uiRelay_0_Voltage.Caption := 'Relay 0 Voltage = '+ Strff(FLJVoltage_0, 4, 3)+' V';
  if Channel = 1 then
    uiRelay_1_Voltage.Caption := 'Relay 1 Voltage = '+ Strff(FLJVoltage_1, 4, 3)+' V';


end;

// The Rest of the Code:
function TForm1.IsUserAnAdmin: Boolean;
var
  hToken: THandle;
  Elevation: TOKEN_ELEVATION;
  cbSize: DWORD;
begin
  Result := False;
  if OpenProcessToken(GetCurrentProcess, TOKEN_QUERY, hToken) then
  try
    cbSize := SizeOf(TOKEN_ELEVATION);
    if GetTokenInformation(hToken, TokenElevation, @Elevation, cbSize, cbSize) then
      Result := Elevation.TokenIsElevated <> 0;
  finally
    CloseHandle(hToken);
  end;
end;


procedure TForm1.RequestElevationIfNeeded;
var
  sei: TShellExecuteInfo;
  exeName: string;
begin
  if not IsUserAnAdmin then
  begin
    ZeroMemory(@sei, SizeOf(sei));
    sei.cbSize := SizeOf(sei);
    sei.Wnd := Handle; // Parent window
    sei.fMask := SEE_MASK_FLAG_DDEWAIT or SEE_MASK_FLAG_NO_UI;
    sei.lpVerb := 'runas'; // Request elevation
    exeName := ParamStr(0);
    sei.lpFile := PChar(exeName);
    sei.lpParameters := PChar(''); // No parameters
    sei.nShow := SW_SHOWNORMAL;

    if not ShellExecuteEx(@sei) then
    begin
      ShowMessage('Failed to create elevated process. Error code: ' + IntToStr(GetLastError));
      Application.Terminate; // Terminate the unelevated instance
    end
    else
      Application.Terminate; // Terminate the unelevated instance
  end;
end;

procedure TForm1.SetEnvironmentVariables;
begin
  SetEnvironmentVariable('VIRTUAL_ENV', PChar(VENV_PATH));
  SetEnvironmentVariable('PYTHONHOME', nil);  // Unset PYTHONHOME
  SetEnvironmentVariable('PATH', PChar(VENV_PATH + '\Scripts;' + GetEnvironmentVariable('PATH')));
end;

procedure TForm1.ConfigurePythonEnvironment;
begin
  MaskFPUExceptions(True);

  // Ensure the virtual environment paths are correctly set
  SetEnvironmentVariables;

  // Configure Python environment
  PythonEngine1.ExecString(Format(
    'import sys; import os; ' +
    'sys.path.append(r"%s"); ' +
    'sys.path.append(r"%s\Lib\site-packages"); ' +
    'sys.path.append(r"%s"); ' +  // Add path to photoneo_genicam
    'os.environ["VIRTUAL_ENV"] = r"%s"; ' +
    'os.environ["PATH"] = r"%s\Scripts;" + os.environ["PATH"]; ' +
    'print("sys.path:", sys.path); ' +
    'print("VIRTUAL_ENV:", os.environ["VIRTUAL_ENV"]); ' +
    'print("PATH:", os.environ["PATH"])',
    [VENV_PATH, VENV_PATH, PHOTONEO_GENICAM_PATH, VENV_PATH, VENV_PATH]
  ));
end;

procedure TForm1.nioTimerIntervalChange(Sender: TObject);
begin
  try
    if(Not nioTimerInterval.Value.IsNan) then
      FTimeTrigScanInterval := nioTimerInterval.IntegerValue;

  except
    FTimeTrigScanInterval := 10;
    nioTimerInterval.Value := FTimeTrigScanInterval;
  end;
end;

procedure TForm1.nioZoomFactorChange(Sender: TObject);
begin
  try
    if(Not nioZoomFactor.Value.IsNan) then
      FScanZoomFactor := (100 / nioZoomFactor.IntegerValue);

  except
    FScanZoomFactor := 0.3;
    nioTimerInterval.Value := (100/FScanZoomFactor);
  end;
end;

procedure TForm1.Panel1DblClick(Sender: TObject);
begin
  // Reset fields:
  if(GetKeyState(VK_SHIFT) < 0) then
    begin
      nioHorizontalRotation.Value := 360;
      nioVerticalRotation.Value := 180;
      nioZoomFactor.Value := 200;
      FScanZoomFactor := (100 / nioZoomFactor.IntegerValue);
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  RequestElevationIfNeeded;

  FTimerTriggeredScan := False;
  FRelayTriggeredScan := False;
  FScriptRunning := False;

  Timer1.Interval := LJ_TIMER_INTERVAL;

  // Load the Python DLL using the virtual environment's Python executable
  try
    FTimeTrigScanInterval := nioTimerInterval.IntegerValue;
    FScanZoomFactor := (100 / nioZoomFactor.IntegerValue);

    if PythonVersionFromPath(PYHARV_HOME + '\.venv\', PyVersion) then
    begin
      PythonEngine1.VenvPythonExe := VENV_PYTHON_EXE;
      PythonEngine1.SetPythonHome(PYHARV_HOME + '\.venv\');
      PyVersion.AssignTo(PythonEngine1);
      PythonEngine1.LoadDll;
      // ShowMessage('Python DLL loaded successfully. ' + PythonEngine1.DllName);
      ConfigurePythonEnvironment;
    end
    else
    begin
      ShowMessage('No vEnv found!');
    end;
  except
    on E: Exception do
      ShowMessage('Failed to load Python DLL: ' + E.Message);
  end;
end;

procedure TForm1.FormActivate(Sender: TObject);
begin
  if(Not DirectoryExists(PYHARV_HOME)) then
    begin
      ShowMessage(PYHARV_HOME + ' doesn''t exist!');
    end;

  FTimeTrigScanInterval := nioTimerInterval.IntegerValue;

  LastTriggerTime := Now;

  // try to connect to Lab Jack:
  Initialize_U3HV;
  // Now Check if its connected:
  if(Not FLabJackconnected) then
    begin
      lblLabJackStatus.Font.Color := clRed;
      lblLabJackStatus.Caption := 'LabJack Disonnected!';
    end;
end;

procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
  case RadioGroup1.ItemIndex of
    SCAN_MODE_MANUAL:
      begin
        FTimerTriggeredScan := False;
        FRelayTriggeredScan := False;
        btnScanNow.Enabled := (Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan);
        nioTimerInterval.Enabled := FTimerTriggeredScan;
      end;

    SCAN_MODE_TIMER:
      begin
        FTimerTriggeredScan := True;
        FRelayTriggeredScan := False;
        btnScanNow.Enabled := (Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan);
        nioTimerInterval.Enabled := FTimerTriggeredScan;
      end;

    SCAN_MODE_RELAY:
      begin
        FTimerTriggeredScan := False;
        FRelayTriggeredScan := True;
        btnScanNow.Enabled := (Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan);
        nioTimerInterval.Enabled := FTimerTriggeredScan;
      end;

    else
      begin
        RadioGroup1.ItemIndex := 0;
        btnScanNow.Enabled := True;
        nioTimerInterval.Enabled := False;
      end;
  end;
end;

procedure TForm1.btnScanNowClick(Sender: TObject);
begin
  btnScanNow.Enabled := False;
  if FScriptRunning then
    begin
      ShowMessage('Scan is still busy.');
      Exit;
    end;
  Application.ProcessMessages;
  if(Tbutton(Sender).Name = 'btnScanNow') then
    begin
      TTask.Run(
        procedure
          var t: Integer;
          begin
            for t := 0 to 9000 do
              begin
                Sleep(10);
              end;
            TThread.Synchronize(nil,
              procedure
                begin
                  //Py_Exit
                  PythonEngine1.ExecString('exit(0)');
                  Application.ProcessMessages;
                end);
          end);
      TriggerByUserToScan;
    end;
  btnScanNow.Enabled := (Not FScriptRunning);
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  if(FTimerTriggeredScan OR FRelayTriggeredScan) then
    begin
      Timer1.Enabled := False;

      //Check for Triggers:
      if(Not FTimerTriggeredScan) then
        TriggerByRelayToScan;

      if(Not FRelayTriggeredScan) then
        TriggerByTimerToScan;

      Timer1.Enabled := True;
    end;
end;

procedure TForm1.TriggerByUserToScan;
begin
  if((Not FTimerTriggeredScan) AND (Not FRelayTriggeredScan)) then
    begin
      RunPythonScript;
    end;
end;

procedure TForm1.TriggerByTimerToScan;
var
  myCurrentTime: TDateTime;
  myTriggerTimeElapsed: Double;

begin
  myCurrentTime := Now;

  // Calculate time elapsed since the last execution of UpdateMassChart
  myTriggerTimeElapsed := (myCurrentTime - LastTriggerTime) * 86400; // Convert to seconds

  if(myTriggerTimeElapsed >= FTimeTrigScanInterval) then
    begin
      StopPythonScript;
      LastTriggerTime := myCurrentTime;
      RunPythonScript;
    end;
end;

procedure TForm1.TriggerByRelayToScan;
var
  ActiveFileName : String;
  myActiveFileCheckSum: String;

begin
  // Enable the Print Monitor Button
  if(FLabJackConnected AND FRelayTriggeredScan AND (Not FTimerTriggeredScan)) then
    begin
      // After the first read.....The Monitorlatch is FALSE....it should only be true
      // again if BOTH voltages are less than 2.0V!!!!

      // Just get the voltage readings from the Labjack
      GetAnalogVoltage(0, FLJVoltage_0);
      GetAnalogVoltage(1, FLJVoltage_1);

      if ((FLJVoltage_0 < 2.0) and (FLJVoltage_1 < 2.0)) then
        FRelayMonitorLatch := True;


      // Update the Relay Lights Status
      if(FLJVoltage_0 > 2.0) then
        begin
          // Use 2V as the trigger voltage level for now
          //uiRelay_0_Light.Fill.Color := TAlphaColorRec.Lime;
          FLJRelay_0_ON := True;
        end
      else
        begin
          //uiRelay_0_Light.Fill.Color := TAlphaColorRec.Crimson;
          FLJRelay_0_ON := False;
        end;

      if(FLJVoltage_1 > 2.0) then
        begin
          // Use 2V as the trigger voltage level for now
          //uiRelay_1_Light.Fill.Color := TAlphaColorRec.Lime;
          FLJRelay_1_ON := True;
        end
      else
        begin
          //uiRelay_1_Light.Fill.Color := TAlphaColorRec.Crimson;
          FLJRelay_1_ON := False;
        end;


      // Now let's figure out if we need to take a photo
      //
      // The camera will be "inactive" most of the time
      // Make it active first....
      //
      // then wait for Relay_1 to turn ON
      //
      // Take the photo and turn Relay 1 OFF and Turn Relay 0 OFF
      //
      if FLJRelay_0_ON then
        begin
          // This means the camera is "active" and updating images
          //
          // Now let's see if Relay 1 is ON
          if FLJRelay_1_ON then
            begin
              // Here you should execute scan!
              //
              if FRelayMonitorLatch then
                begin
                  StopPythonScript;
                  // Now let's write the file!
                  RunPythonScript;
                end;

              // Now set the MonitorLatch to False!
              FRelayMonitorLatch := False;
            end;
        end;

    end;

end;

// Python uses Open3D Library modules:
function TForm1.FindPythonOpen3DSubprocess: DWORD;
var
  Snapshot: THandle;
  ProcessEntry: TProcessEntry32;
begin
  Result := 0;
  Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if Snapshot <> INVALID_HANDLE_VALUE then
  try
    ProcessEntry.dwSize := SizeOf(ProcessEntry);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if Pos('Open3D', ProcessEntry.szExeFile) > 0 then
        begin
          Result := ProcessEntry.th32ProcessID;
          Break;
        end;
      until Not Process32Next(Snapshot, ProcessEntry);
    end;
  finally
    CloseHandle(Snapshot);
  end;
end;

// Terminate Python execution:
function TForm1.StopPythonScript: Boolean;
    procedure TerminateProcessByPID(PID: DWORD);
    var
      ProcessHandle: THandle;
    begin
      ProcessHandle := OpenProcess(PROCESS_TERMINATE, False, PID);
      if ProcessHandle <> 0 then
      try
        TerminateProcess(ProcessHandle, 0);
      finally
        CloseHandle(ProcessHandle);
      end;
    end;

begin
  Result := False;
  // Terminate the Open3D subprocess if it exists
  FPythonOpen3DPID := FindPythonOpen3DSubprocess;
  if FPythonOpen3DPID <> 0 then
    TerminateProcessByPID(FPythonOpen3DPID);

end;

// The Actual Python execution:
function TForm1.RunPythonScript: Boolean;
var
  Script: TStringList;
  ScriptFilePath, DeviceID, HorizontalRotation, VerticalRotation, ZoomFactor: string;
begin
  // Prevent Double Clicking...  since the python script needs to complete first before re-running:
  FScriptRunning := True;

  Script := TStringList.Create;
  try
    // Retrieve the values from the form inputs
    DeviceID := editDevSerialNumber.Text;
    HorizontalRotation := nioHorizontalRotation.Value.ToString;
    VerticalRotation := nioVerticalRotation.Value.ToString;
    ZoomFactor := FScanZoomFactor.ToString;

    ScriptFilePath := PYHARV_HOME + '\advanced\GridLoigic_pointcloud_with_normals_and_texture.py';

    if FileExists(ScriptFilePath) then
    begin
      Script.LoadFromFile(ScriptFilePath);

      // Insert the sys.path and debug statements
      Script.Insert(2, 'sys.path.append(r"' + PYHARV_HOME + '\advanced")');
      Script.Insert(3, 'print("Starting script execution")');
      Script.Insert(4, 'try:');
      Script.Insert(5, '    import photoneo_genicam');
      Script.Insert(6, '    print("photoneo_genicam imported successfully")');
      Script.Insert(7, 'except ImportError as e:');
      Script.Insert(8, '    print("Failed to import photoneo_genicam:", str(e))');
      Script.Insert(9, '    raise');

      // Add the parameters as comments for reference
      Script.Insert(10, '# Parameters passed from Delphi');
      Script.Insert(11, 'device_id = "' + DeviceID + '"');
      Script.Insert(12, 'horizontal_angle = ' + HorizontalRotation);
      Script.Insert(13, 'vertical_angle = ' + VerticalRotation);
      Script.Insert(14, 'zoom_factor = ' + ZoomFactor);

      // Replace main function call with parameters
      //Script.Add('main(device_id, horizontal_angle, vertical_angle, zoom_factor)');

      try
        ThreadPythonExec(
        procedure
          begin
            GetPythonEngine.ExecStrings(Script);
          end,
        procedure
          begin
            FScriptRunning := False;
          end,
          False);

        //PythonEngine1.ExecStrings(Script);
        //ShowMessage('Script executed successfully.');
      except
        on E: EPySystemExit do
        begin
          // Handle the SystemExit exception
          ShowMessage('Script terminated by SystemExit.');
        end;
        on E: EPythonError do
        begin
          ShowMessage('Python Error: ' + E.Message);
        end;
        on E: Exception do
        begin
          ShowMessage('Error: ' + E.Message);
        end;
      end;
    end
    else
    begin
      ShowMessage('Script file does not exist: ' + ScriptFilePath);
    end;
  finally
    Script.Free;
    //PythonEngine1.

    //if(Not (FTimerTriggeredScan or FRelayTriggeredScan)) then
      //FScriptRunning := False;
  end;
end;

end.

 

Share this post


Link to post
2 minutes ago, pyscripter said:

I have attached the version of the project that uses the example (to the best of my understanding) ThreadPythonExec:

 

This snippet:

 

//        ThreadPythonExec(
//        procedure
//          begin
//            PythonEngine1.ExecStrings(Script);
//          end,
//        procedure
//          begin
//            FScriptRunning := False;
//          end,
//          False);

        PythonEngine1.ExecStrings(Script);

I commented out the ThreadPythonExec and uncommented (the last) line.. and it works fine,  but the python app blocks the main UI and I am trying to avoid this.

GLDelphiScanner_BKUP.zip

Share this post


Link to post
Posted (edited)

Have you followed the instructions?

Quote

 

Preparation

When PythonEngine loads the python DLL, the main thread holds the GIL. Before you can run python code in threads you must release the GIL. TPythonThread has two class methods that allow you to release the GIL and then acquire back.

  • class procedure Py_Begin_Allow_Threads;
  • class procedure Py_End_Allow_Threads;

Py_Begin_Allow_Threads saves the thread state and releases the GIL, whilst Py_End_Allow_Threads tries to get hold of GIL and restore the previous thread state. You should only call Py_Begin_Allow_Threads if you alread hold the GIL. Otherwise a deadlock will occur.

You should call Py_Begin_Allow_Threads in your main thread, after the python DLL was loaded, for example in your FormCreate event handler. Also you can call Py_End_Allow_Threads when you are done executing python in threads.

 

 

Edited by pyscripter

Share this post


Link to post

I did not,  I thought that was actually done using the ThreadPythonExec as well as in this:

 

        FTask := TTask.Create(
        procedure
        var
          Py: IPyEngineAndGIL;
        begin
          Py := SafePyEngine;
          Py.PythonEngine.ExecStrings(Script);
        end);
        FTask.Start;

So no matter what  I am required to call Py_Begin_Allow_Threads and Py_End_Allow_Threads?

Share this post


Link to post
Posted (edited)

Dealing with python threads and the Global Interpreter Lock (GIL) is quite tricky.  You need to either:

  1. Understand and use correctly the python API Initialization, Finalization, and Threads — Python 3.12.3 documentation or
  2. Use the high-level encapsulation provided by P4D, following the instructions in https://github.com/pyscripter/python4delphi/wiki/PythonThreads religiously.

Otherwise, things can very easily go wrong.

Edited by pyscripter

Share this post


Link to post
17 hours ago, pyscripter said:

Dealing with python threads and the Global Interpreter Lock (GIL) is quite tricky.  You need to either:

  1. Understand and use correctly the python API Initialization, Finalization, and Threads — Python 3.12.3 documentation or
  2. Use the high-level encapsulation provided by P4D, following the instructions in https://github.com/pyscripter/python4delphi/wiki/PythonThreads religiously.

Otherwise, things can very easily go wrong.

Thanks.  I will.   For the time being I am using CreateProcess and calling the script as a commandline and its working fine. I will revisit this later.  

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×