From a931f657cc248df6a7a575adc83cb064d5345546 Mon Sep 17 00:00:00 2001
From: Zoe Zhang <120383504+ZiyingZhang313@users.noreply.github.com>
Date: Thu, 27 Nov 2025 14:04:37 +0800
Subject: [PATCH 1/4] update the BP209 C++ and C# code to fit 9.3 version SDK
1. Add setThorlabsBeamCompatibleCoordinateSystem(true) method to make all position coordinates compatible with Thorlabs Beam Software
2. Improve the frame rate by avoiding duplicately calling request_scan_data and request_scan_data
---
.../Thorlabs.BP2_CSharpDemo/Form1.cs | 177 +++++++++---------
C++/BP209 2D Output/BP209_2D_output.cpp | 109 ++++++++---
2 files changed, 164 insertions(+), 122 deletions(-)
diff --git a/C sharp/Thorlabs BP209 Beam Profiler 2D Output/Thorlabs.BP2_CSharpDemo/Form1.cs b/C sharp/Thorlabs BP209 Beam Profiler 2D Output/Thorlabs.BP2_CSharpDemo/Form1.cs
index 5ccb7f4..36c65fe 100644
--- a/C sharp/Thorlabs BP209 Beam Profiler 2D Output/Thorlabs.BP2_CSharpDemo/Form1.cs
+++ b/C sharp/Thorlabs BP209 Beam Profiler 2D Output/Thorlabs.BP2_CSharpDemo/Form1.cs
@@ -1,20 +1,22 @@
// Title: BP209 2D Reconstruction C Sharp Example.
// Created Date: 2024 - 10 - 12
-// Last modified date: 2024 - 10 - 12
+// Last modified date: 2025 - 11 - 26
// .NET version: 4.8
-// Thorlabs SDK Version: Beam version 9.1.5787.560
+// Thorlabs SDK Version: Beam version 9.3
// Notes: This example is based on the C sharp example which is installed to
// C:\Program Files (x86)\IVI Foundation\VISA\WinNT\TLBP2\Examples during software installation.
// This example has added the 2D reconstruction algorithm and the reconstructed beam image is displayed.
namespace Thorlabs.BP2_CSharpDemo
{
- using System;
+ using System;
+ using System.Data;
+ using System.Drawing;
+ using System.Drawing.Imaging;
+ using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
- using System.Drawing;
using Thorlabs.TLBP2.Interop;
- using System.Runtime.InteropServices;
///
/// Initializes the form
@@ -36,16 +38,6 @@ public partial class Form1 : Form
///
private Timer scanTimer = null;
- ///
- /// array of data structures for each slit.
- ///
- private bp2_slit_data[] bp2SlitData = new bp2_slit_data[4];
-
- ///
- /// array of calculation structures for each slit.
- ///
- private bp2_calculations[] bp2Calculations = new bp2_calculations[4];
-
///
/// Initializes a new instance of the class.
///
@@ -59,9 +51,6 @@ public Form1()
// implementation with driver functions
this.ConnectToTheFirstDevice();
- // alternative implementation
- ////this.connectToTheFirstDeviceByRM();
-
if (this.bp2Device != null)
{
// get the instrument information
@@ -89,7 +78,6 @@ public Form1()
ushort sampleCount;
double sampleResolution;
this.status = this.bp2Device.clear_drum_speed_offset();
- this.status = this.bp2Device.set_drum_speed(10.0);
this.status = this.bp2Device.set_drum_speed_ex(10.0, out sampleCount, out sampleResolution);
// activate the position correction to have the same calculation results as the Thorlabs Beam Application
@@ -101,11 +89,8 @@ public Form1()
// activate the drum speed correction
this.status = this.bp2Device.set_speed_correction(true);
- // use the offset for 10Hz to be compatible with the release version 5.0
- this.status = this.bp2Device.set_reference_position(0, 4, 100.0);
- this.status = this.bp2Device.set_reference_position(1, 4, -100.0);
- this.status = this.bp2Device.set_reference_position(2, 4, 100.0);
- this.status = this.bp2Device.set_reference_position(3, 4, -100.0);
+ // return all position coordinates from -4500 µm to 4500 µm and flip the x scans
+ this.status = this.bp2Device.setThorlabsBeamCompatibleCoordinateSystem(true);
// poll for a valid scan
this.scanTimer = new Timer();
@@ -152,22 +137,6 @@ private void ConnectToTheFirstDevice()
}
}
- ///
- /// search for connected devices and connect to the first one.
- /// Use the VISA resource manager and simple data types.
- ///
- private void ConnectToTheFirstDeviceByRM()
- {
- // get the resource string of the first device
- string[] bp2Resources = BP2_ResourceManager.FindRscBP2();
-
- if (bp2Resources.Length > 0)
- {
- // connect to the first device
- this.bp2Device = new TLBP2(bp2Resources[0], false, false);
- }
- }
-
///
/// poll for a new measurement and fill the structures with the calculation results.
///
@@ -177,7 +146,7 @@ private void GetMeasurement()
double drumSpeed;
try
{
- if (0 == this.bp2Device.get_drum_speed(out drumSpeed))
+ if (0 == this.bp2Device.get_averaged_drum_speed(out drumSpeed))
{
this.textBox_drumSpeed.Text = drumSpeed.ToString("f2");
}
@@ -200,70 +169,87 @@ private void GetMeasurement()
else if ((deviceStatus & 2) == 2)
this.toolStripStatusLabel1.Text = "Instrument is ready";
}
-
- // the gain and drum speed will be corrected during the measurement
+
double power;
- float powerWindowSaturation;
- if ((deviceStatus & 1) == 1 &&
- 0 == this.bp2Device.get_slit_scan_data(this.bp2SlitData, this.bp2Calculations, out power, out powerWindowSaturation, null))
+ float powerSaturation;
+ ushort peakIndex1, peakIndex2,sampleCount1,sampleCount2,centroidIndexSlit1, centroidIndexSlit2;
+ float peakPositionSlit1, peakPositionSlit2, centroidPositionSlit1, centroidPositionSlit2, peakIntensitySlit1, peakIntensitySlit2;
+ float darkLevelSlit1, darkLevelSlit2;
+
+ if ((deviceStatus & 1) == 1 && 0 == this.bp2Device.request_scan_data(out power, out powerSaturation, null))
{
- this.textBox_peakPositionSlit1.Text = this.bp2Calculations[0].PeakPosition.ToString("f2");
- this.textBox_peakIntensitySlit1.Text = (this.bp2Calculations[0].PeakIntensity * 100.0f / ((float)0x7AFF - this.bp2SlitData[0].SlitDarkLevel)).ToString("f2");
- this.textBox_centroidPositionSlit1.Text = this.bp2Calculations[0].CentroidPos.ToString("f2");
+ // get the peak position and centriod position
+
+ this.bp2Device.get_slit_peak(0, out peakIndex1, out peakPositionSlit1, out peakIntensitySlit1);
+ this.bp2Device.get_slit_peak(1, out peakIndex2, out peakPositionSlit2, out peakIntensitySlit2);
+ this.bp2Device.get_scan_data_information(0, out sampleCount1, out darkLevelSlit1);
+ this.bp2Device.get_scan_data_information(1, out sampleCount2, out darkLevelSlit2);
+ this.bp2Device.get_slit_centroid(0, out centroidIndexSlit1,out centroidPositionSlit1);
+ this.bp2Device.get_slit_centroid(1, out centroidIndexSlit2, out centroidPositionSlit2);
+
+ this.textBox_peakPositionSlit1.Text = peakPositionSlit1.ToString("f2");
+ this.textBox_peakIntensitySlit1.Text = (peakIntensitySlit1 * 100.0f / ((float)0x7AFF - darkLevelSlit1)).ToString("f2");
+ this.textBox_centroidPositionSlit1.Text = centroidPositionSlit1.ToString("f2");
- this.textBox_peakPositionSlit2.Text = this.bp2Calculations[1].PeakPosition.ToString("f2");
- this.textBox_peakIntensitySlit2.Text = (this.bp2Calculations[1].PeakIntensity * 100.0f / ((float)0x7AFF - this.bp2SlitData[1].SlitDarkLevel)).ToString("f2");
- this.textBox_centroidPositionSlit2.Text = this.bp2Calculations[1].CentroidPos.ToString("f2");
+ this.textBox_peakPositionSlit2.Text = peakPositionSlit2.ToString("f2");
+ this.textBox_peakIntensitySlit2.Text = (peakIntensitySlit2 * 100.0f / ((float)0x7AFF - darkLevelSlit2)).ToString("f2");
+ this.textBox_centroidPositionSlit2.Text = centroidPositionSlit2.ToString("f2");
- this.textBox_powerSaturation.Text = (powerWindowSaturation*100.0).ToString("f2");
+ this.textBox_powerSaturation.Text = (powerSaturation*100.0).ToString("f2");
- this.chart25um.Series[0].Points.DataBindXY(bp2SlitData[0].SlitSamplesPositions, bp2SlitData[0].SlitSamplesIntensities);
- this.chart25um.Series[1].Points.DataBindXY(bp2SlitData[1].SlitSamplesPositions, bp2SlitData[1].SlitSamplesIntensities);
- this.chart5um.Series[0].Points.DataBindXY(bp2SlitData[2].SlitSamplesPositions, bp2SlitData[2].SlitSamplesIntensities);
- this.chart5um.Series[1].Points.DataBindXY(bp2SlitData[3].SlitSamplesPositions, bp2SlitData[3].SlitSamplesIntensities);
+ GetChartAnd2DReconstruction();
- //Calculate and display the 2D reconstruction image
- Get2DReconstruction();
}
}
///
/// Calculate the 2D reconstructed beam intensity distribution and display the image on the WinForm
///
- private void Get2DReconstruction()
+ private void GetChartAnd2DReconstruction()
{
- double[] sampleIntensitiesX = new double[7500];
- double[] sampleIntensitiesY = new double[7500];
- double[] samplePositionX = new double[7500];
- double[] samplePositionY = new double[7500];
- double[] gaussianFitIntensitiesX = new double[7500];
- double[] gaussianFitIntensitiesY = new double[7500];
- double power;
- float powerSaturation;
+ double[] sampleIntensities25umX = new double[7500];
+ double[] sampleIntensities25umY = new double[7500];
+ double[] samplePosition25umX = new double[7500];
+ double[] samplePosition25umY = new double[7500];
+ double[] sampleIntensities5umX = new double[7500];
+ double[] sampleIntensities5umY = new double[7500];
+ double[] samplePosition5umX = new double[7500];
+ double[] samplePosition5umY = new double[7500];
+ double[] gaussianFitIntensities25umX = new double[7500];
+ double[] gaussianFitIntensities25umY = new double[7500];
float temp;
-
- //Request the scan data
- this.bp2Device.request_scan_data(out power,out powerSaturation,null);
+
//Get the intensities from the 25um X slit and the 25um Y slit
- this.bp2Device.get_sample_intensities(0, sampleIntensitiesX, samplePositionX);
- this.bp2Device.get_sample_intensities(1, sampleIntensitiesY, samplePositionY);
+ this.bp2Device.get_sample_intensities(0, sampleIntensities25umX, samplePosition25umX);
+ this.bp2Device.get_sample_intensities(1, sampleIntensities25umY, samplePosition25umY);
+
+ //Get the intensities from the 5um X slit and the 5um Y slit
+ this.bp2Device.get_sample_intensities(2, sampleIntensities5umX, samplePosition5umX);
+ this.bp2Device.get_sample_intensities(3, sampleIntensities5umY, samplePosition5umY);
+
+ //Chart display
+ this.chart25um.Series[0].Points.DataBindXY(samplePosition25umX, sampleIntensities25umX);
+ this.chart25um.Series[1].Points.DataBindXY(samplePosition25umY, sampleIntensities25umY);
+ this.chart5um.Series[0].Points.DataBindXY(samplePosition5umX, sampleIntensities5umX);
+ this.chart5um.Series[1].Points.DataBindXY(samplePosition5umY, sampleIntensities5umY);
//Get the gaussian fit intensites from the 25um X slit and the 25um Y slit
- this.bp2Device.get_slit_gaussian_fit(0, out temp,out temp,out temp,gaussianFitIntensitiesX);
- this.bp2Device.get_slit_gaussian_fit(1, out temp, out temp, out temp, gaussianFitIntensitiesY);
+ this.bp2Device.get_slit_gaussian_fit(0, out temp,out temp,out temp, gaussianFitIntensities25umX);
+ this.bp2Device.get_slit_gaussian_fit(1, out temp, out temp, out temp, gaussianFitIntensities25umY);
//2D reconstruction
- double[,] imageData = new double[750, 750];
+ int imageSize = 750;
+ double[,] imageData = new double[imageSize, imageSize];
double imageDataMax = 0;
int ixz, iyz;
- for (int ix = 0; ix < 750; ix++)
+ for (int ix = 0; ix < imageSize; ix++)
{
- for (int iy = 0; iy < 750; iy++)
+ for (int iy = 0; iy < imageSize; iy++)
{
//2D reconstruction algorithm
- ixz = (750 - ix - 1) * 10;
- iyz = (750 - iy - 1) * 10;
- imageData[ix, iy] = sampleIntensitiesX[ixz] * gaussianFitIntensitiesX[ixz] * sampleIntensitiesY[iyz] * gaussianFitIntensitiesY[iyz];
+ ixz = (imageSize - ix - 1) * 7500 / imageSize;
+ iyz = (imageSize - iy - 1) * 7500 / imageSize;
+ imageData[ix, iy] = sampleIntensities25umX[ixz] * gaussianFitIntensities25umX[ixz] * sampleIntensities25umY[iyz] * gaussianFitIntensities25umY[iyz];
//set the negative values to zero
if (imageData[ix, iy] < 0)
@@ -279,22 +265,29 @@ private void Get2DReconstruction()
}
//Normalize intensity values and generate the Bitmap image
- int imageGrayValue;
- Bitmap bitmap = new Bitmap(750, 750);
- for (int x = 0; x < 750; x++)
+ Bitmap bitmap = new Bitmap(imageSize, imageSize, PixelFormat.Format24bppRgb);
+ BitmapData bmpData = bitmap.LockBits(
+ new Rectangle(0, 0, imageSize, imageSize),ImageLockMode.WriteOnly,bitmap.PixelFormat);
+
+ int stride = bmpData.Stride;
+ byte[] pixelData = new byte[stride * imageSize];
+
+ for (int y = 0; y < imageSize; y++)
{
- for (int y = 0; y < 750; y++)
+ for (int x = 0; x < imageSize; x++)
{
- imageGrayValue = (int)(imageData[x, y] * 255 / imageDataMax);
- Color color = Color.FromArgb(imageGrayValue, imageGrayValue, imageGrayValue);
- bitmap.SetPixel(x, y, color);
+ int gray = (int)(imageData[x, y] * 255 / imageDataMax);
+ gray = Math.Min(255, Math.Max(0, gray));
+ int pixelIndex = y * stride + x * 3;
+ pixelData[pixelIndex + 0] = (byte)gray;
+ pixelData[pixelIndex + 1] = (byte)gray;
+ pixelData[pixelIndex + 2] = (byte)gray;
}
}
- // set the bitmap to the Image property
+ Marshal.Copy(pixelData, 0, bmpData.Scan0, pixelData.Length);
+ bitmap.UnlockBits(bmpData);
this.reconstructionPicture.Image = bitmap;
-
-
}
///
/// If a new scan is available, get the data from the instrument and display the calculation results on the form.
@@ -329,5 +322,5 @@ private void Form1_FormClosing(object sender, FormClosingEventArgs e)
this.bp2Device.Dispose();
}
}
- }
+ }
}
diff --git a/C++/BP209 2D Output/BP209_2D_output.cpp b/C++/BP209 2D Output/BP209_2D_output.cpp
index 7f52170..fc0df71 100644
--- a/C++/BP209 2D Output/BP209_2D_output.cpp
+++ b/C++/BP209 2D Output/BP209_2D_output.cpp
@@ -1,7 +1,8 @@
-//Example Date of Creation(YYYY - MM - DD) 2024 - 04 - 24
-//Example Date of Last Modification on Github 2024 - 04 - 24
+//Example Date of Creation(YYYY - MM - DD) 2024 - 04 - 24
+//Example Date of Last Modification on Github 2025 - 11 - 05
//Version of C++ used for Testing and IDE: C++ 14, Visual Studio 2022
-//Version of the Thorlabs SDK used : Beam version 9.1.5787.560
+//Version of OpenCV: OpenCV 4.12.0
+//Version of the Thorlabs SDK used : Beam version 9.3
//Example Description: The sample code shows how to control a BP209 beam profiler in C++.
//In the example the available beam profilers are found, a connection is established, several parameters are set,
//several output values are displayed and a 2D image is shown.
@@ -18,7 +19,7 @@ using namespace cv;
// forward declaration
void print_error_msg(ViStatus err);
void Beam_Profile_Reconstruction();
-ViSession m_instrumentHandle;
+ViSession m_instrumentHandle = 0;
//set the measured laser wavelengh unit: nm
double wavelength = 633;
@@ -55,50 +56,74 @@ int main(int argc, char* argv)
return 0;
}
+ ViChar modelName[256];
+ ViChar serialNo[256];
+ ViChar manufacturer[256];
+ ViBoolean isAvailable;
+ res = TLBP2_getRsrcInfo(0, 0, modelName, serialNo, manufacturer, &isAvailable);
+
// connect with the first device
res = TLBP2_init(resStr[0].resourceString, VI_TRUE, VI_TRUE, &m_instrumentHandle);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
return 0;
}
- char serialNo[128];
- res = TLBP2_get_serial_number(m_instrumentHandle, serialNo);
- printf("%s is connected. \n", serialNo);
+
+ if (isAvailable)
+ {
+ printf("%s (SN: %s) is connected. \n", modelName, serialNo);
+ }
+ else
+ {
+ printf("%s (SN: %s) is not available.\n", modelName, serialNo);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
+ return 0;
+ }
+
// release the buffer for the resource strings
delete[] resStr;
//set auto gain
res = TLBP2_set_auto_gain(m_instrumentHandle, VI_TRUE);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
//set bandwidth
ViReal64 bw_buffer[4] = { 125,125,125,125 };
res = TLBP2_set_bandwidths(m_instrumentHandle, bw_buffer);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
//set wavelength
res = TLBP2_set_wavelength(m_instrumentHandle, wavelength);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
//set power factor
res = TLBP2_set_user_power_factor(m_instrumentHandle, powerCorrectionFactor);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
@@ -113,22 +138,26 @@ int main(int argc, char* argv)
res = TLBP2_set_scanning_method(m_instrumentHandle, 1, scanningMethod);
res = TLBP2_set_scanning_method(m_instrumentHandle, 2, scanningMethod);
res = TLBP2_set_scanning_method(m_instrumentHandle, 3, scanningMethod);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
}
else
{
- printf("Invalid Input! The scanning method is set to slit scanning mode.\n");
+ printf("Invalid Input! The scanning method is set to slit scanning method.\n");
res = TLBP2_set_scanning_method(m_instrumentHandle, 0, 0);
res = TLBP2_set_scanning_method(m_instrumentHandle, 1, 0);
res = TLBP2_set_scanning_method(m_instrumentHandle, 2, 0);
res = TLBP2_set_scanning_method(m_instrumentHandle, 3, 0);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
}
@@ -140,25 +169,41 @@ int main(int argc, char* argv)
res = TLBP2_set_drum_speed_ex(m_instrumentHandle, 10, &sampleCount, &resolution);
else //knife edge mode
res = TLBP2_set_drum_speed_ex(m_instrumentHandle, 2, &sampleCount, &resolution);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
//set position correction
res = TLBP2_set_position_correction(m_instrumentHandle, VI_TRUE);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
+ {
+ print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
+ return 0;
+ }
+
+ //return all position coordinates from -4500 µm to 4500 µm and flip the x scans
+ res = TLBP2_setThorlabsBeamCompatibleCoordinateSystem(m_instrumentHandle, VI_TRUE);
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
//set speed correction
res = TLBP2_set_speed_correction(m_instrumentHandle, VI_TRUE);
- if ((res & _VI_ERROR) > 0)
+ if (res != VI_SUCCESS)
{
print_error_msg(res);
+ // release the device
+ TLBP2_close(m_instrumentHandle);
return 0;
}
@@ -169,12 +214,10 @@ int main(int argc, char* argv)
res = TLBP2_get_device_status(m_instrumentHandle, &device_status);
}
- static BP2_SLIT_DATA slit_data[4], slit_data_kinfe[4]; /// BP2_MAX_SLIT_COUNT = 4
- static BP2_CALCULATIONS calculation_result[4], calculation_result_knife[4];
static ViReal64 power_intensities[7500];
static ViBoolean slit_indices[4] = { VI_TRUE, VI_TRUE, VI_TRUE, VI_TRUE};
ViReal64 power;
- ViReal32 powerSaturation;
+ ViReal32 powerSaturation,centroidPositionX, centroidPositionY,gaussianDiameterX,gaussianDiameterY;
ViUInt8 gain[4];
ViUInt8 gainPower;
@@ -183,7 +226,7 @@ int main(int argc, char* argv)
printf("Adjusting Gain...\n");
for (int i = 0; i < 10; i++)
{
- res = TLBP2_get_slit_scan_data(m_instrumentHandle, slit_data, calculation_result, &power, &powerSaturation, power_intensities);
+ res = TLBP2_request_scan_data(m_instrumentHandle, &power, &powerSaturation, power_intensities);
res = TLBP2_get_gains(m_instrumentHandle, gain, &gainPower);
printf("Gain:\n");
printf(" 25um slit x: %d, 25um slit y: %d\n", gain[0],gain[1]);
@@ -195,19 +238,25 @@ int main(int argc, char* argv)
if (scanningMethod == 0)//slit scanning mode
{
//Get the slit scan data
- res = TLBP2_get_slit_scan_data(m_instrumentHandle, slit_data, calculation_result, &power, &powerSaturation, power_intensities);
+ res = TLBP2_request_scan_data(m_instrumentHandle, &power, &powerSaturation, power_intensities);
if (res == VI_SUCCESS)
{
+ TLBP2_get_slit_centroid(m_instrumentHandle, 2, VI_NULL, ¢roidPositionX);
+ TLBP2_get_slit_centroid(m_instrumentHandle, 3, VI_NULL, ¢roidPositionY);
+ TLBP2_get_slit_gaussian_fit(m_instrumentHandle, 2, VI_NULL, &gaussianDiameterX, VI_NULL, VI_NULL);
+ TLBP2_get_slit_gaussian_fit(m_instrumentHandle, 3, VI_NULL, &gaussianDiameterY, VI_NULL, VI_NULL);
printf("Corrected Power value: %.2f mW\n", power);
- printf("5um Slit X Centroid Position: %.2f\n", calculation_result[2].centroidPosition);
- printf("5um Slit Y Centroid Position: %.2f\n", calculation_result[3].centroidPosition);
- printf("5um Slit X Gaussian Fit Diameter: %.2f\n", calculation_result[2].gaussianFitDiameter);
- printf("5um Slit Y Gaussian Fit Diameter: %.2f\n", calculation_result[3].gaussianFitDiameter);
+ printf("5um Slit X Centroid Position: %.2f\n", centroidPositionX);
+ printf("5um Slit Y Centroid Position: %.2f\n", centroidPositionY);
+ printf("5um Slit X Gaussian Fit Diameter: %.2f\n", gaussianDiameterX);
+ printf("5um Slit Y Gaussian Fit Diameter: %.2f\n", gaussianDiameterY);
Beam_Profile_Reconstruction();
}
}
else//knife edge mode
{
+ static BP2_SLIT_DATA slit_data[4], slit_data_kinfe[4]; /// BP2_MAX_SLIT_COUNT = 4
+ static BP2_CALCULATIONS calculation_result[4], calculation_result_knife[4];
//Calculate the knife edge data from the slit data.
res = TLBP2_get_slit_scan_data(m_instrumentHandle, slit_data, calculation_result, &power, &powerSaturation, power_intensities);
res = TLBP2_get_knife_edge_reconstruction(m_instrumentHandle, slit_data, calculation_result, slit_indices, slit_data_kinfe, calculation_result_knife);
@@ -241,8 +290,6 @@ void Beam_Profile_Reconstruction()
static ViReal64 gaussianFitIntensitiesY[7500];
ViReal32 gaussianFitPercentageX, gaussianFitPercentageY;
- //Request the scan data
- TLBP2_request_scan_data(m_instrumentHandle, VI_NULL, VI_NULL, VI_NULL);
//Get the intensities from the 5um X slit and the 5um Y slit
res = TLBP2_get_sample_intensities(m_instrumentHandle, 2, sampleIntensitiesX, samplePositionsX);
res = TLBP2_get_sample_intensities(m_instrumentHandle, 3, sampleIntensitiesY, samplePositionsY);
@@ -314,10 +361,12 @@ void Beam_Profile_Reconstruction()
reconstructionImage.at(col, row) = (uchar)(255 * intensityTemp[row][col] / intensityMax);
}
}
+
imshow("X Intensity", IntensityXImage);
imshow("X Gaussian Fit Intensity", GaussianXImage);
imshow("2D Reconstruction", reconstructionImage);
- waitKey(0);
+
+ waitKey(8000);
}
// prints the error message from an error code
From dc04f312620ee4aec24fa1e7f178394e7370ed1b Mon Sep 17 00:00:00 2001
From: gboedecker <125995107+gboedecker@users.noreply.github.com>
Date: Wed, 10 Dec 2025 14:01:57 +0100
Subject: [PATCH 2/4] Create CCT_example.m
---
Matlab/CCT Spectrometers/CCT_example.m | 76 ++++++++++++++++++++++++++
1 file changed, 76 insertions(+)
create mode 100644 Matlab/CCT Spectrometers/CCT_example.m
diff --git a/Matlab/CCT Spectrometers/CCT_example.m b/Matlab/CCT Spectrometers/CCT_example.m
new file mode 100644
index 0000000..27b8e61
--- /dev/null
+++ b/Matlab/CCT Spectrometers/CCT_example.m
@@ -0,0 +1,76 @@
+%% Header
+% Title: CCT_example.m
+% Created Date: 2025-12-10
+% Last modified date: 2025-12-10
+% Matlab Version:R2023a
+% Thorlabs DLL version:1.0.20.4045
+%% Notes: The example shows how to connect to a CCT spectrometer, set the exposure time and acquire a spectrum
+% Tested with CCT11
+%
+
+% Load the Compact Spectrometer SDK DLLs
+dll_path='C:\Program Files\Thorlabs\ThorSpectra';
+NET.addAssembly(fullfile(dll_path, 'Thorlabs.ManagedDevice.CompactSpectrographDriver.dll'));
+NET.addAssembly(fullfile(dll_path, 'Thorlabs.ManagedDevice.dll'));
+NET.addAssembly(fullfile(dll_path, 'Microsoft.Extensions.Logging.Abstractions.dll'));
+NET.addAssembly('System.Runtime');
+
+import Thorlabs.ManagedDevice.CompactSpectrographDriver.Workflow.StartupHelperCompactSpectrometer.*;
+import Thorlabs.ManagedDevice.Trace.ExampleLogger.*;
+import Microsoft.Extensions.Logging.Abstractions.*;
+import Thorlabs.ManagedDevice.CompactSpectrographDriver.ICompactSpectrographDriver.*;
+
+logger = Thorlabs.ManagedDevice.Trace.ExampleLogger('ML',Microsoft.Extensions.Logging.LogLevel.Trace,1==1,'MatLab');
+startupHelper = Thorlabs.ManagedDevice.CompactSpectrographDriver.Workflow.StartupHelperCompactSpectrometer(logger);
+
+cts = System.Threading.CancellationTokenSource();
+cancellationToken = cts.Token;
+
+%find devices
+discoveredDevicestask=startupHelper.GetKnownDevicesAsync(cancellationToken);
+discoveredDevicestask.Wait();
+discoveredDevices=discoveredDevicestask.Result;
+
+for i=0:discoveredDevices.Count-1
+ disp("discovered devices:")
+ disp(discoveredDevices.Item(i));
+end
+
+%if spectrometers are found
+if(discoveredDevices.Count>0)
+
+ disp("connecting to first device ...")
+
+ Deviceid=discoveredDevices.Item(0);
+
+ spectrometer=startupHelper.GetCompactSpectrographById(Deviceid);
+
+ %set exposure time
+ exposure=500; %exposure time in milliseconds
+ exposure_result=spectrometer.SetManualExposureAsync(exposure,cancellationToken).Result;
+ if exposure_result==1
+ disp(['exposure time is set to ',num2str(exposure),' ms']);
+ end
+
+ %acquire spectrum
+ spectrumtask=spectrometer.AcquireSingleSpectrumAsync(cancellationToken);
+ spectrumtask.Wait();
+ spectrum=spectrumtask.Result;
+
+ %dispose startup helper
+ startupHelper.Dispose();
+
+ %plot spectrum
+ figure; plot(spectrum.Wavelength,spectrum.Intensity)
+
+
+else
+ disp("No CCT spectrometer connected")
+end
+
+
+
+
+
+
+
From 5fd6807d6f340b0ec7078195cb38d03105456162 Mon Sep 17 00:00:00 2001
From: gboedecker <125995107+gboedecker@users.noreply.github.com>
Date: Wed, 14 Jan 2026 13:56:25 +0100
Subject: [PATCH 3/4] Update Readme.md
Raspberry Pi section added
---
.../SCPI/Readme.md | 70 +++++++++++++++++--
1 file changed, 65 insertions(+), 5 deletions(-)
diff --git a/Python/Thorlabs PMxxx Power Meters/SCPI/Readme.md b/Python/Thorlabs PMxxx Power Meters/SCPI/Readme.md
index f3297dc..7611e54 100644
--- a/Python/Thorlabs PMxxx Power Meters/SCPI/Readme.md
+++ b/Python/Thorlabs PMxxx Power Meters/SCPI/Readme.md
@@ -27,7 +27,7 @@ For closer details refer to [Readme](scopeMode). Available for PM6x, PM103, PM10
Minimal template script ```PMxxx_SCPI_OpenAnyvisa.py``` to open a known instrument resource using anyvisa library.
### Search Anyvisa
-Minimal template script ```PMxxx_SCPI_SearchAnyvisa.py``` to run instrument search and open one of the devices found using anvisa library.
+Minimal template script ```PMxxx_SCPI_SearchAnyvisa.py``` to run instrument search and open one of the devices found using anyvisa library.
## SCPI Command documentation
For most of the Thorlabs Powermeter there is a detail [SCPI command documentation](commandDocu) in .html file format available.
@@ -52,9 +52,10 @@ python -m pip install anyvisa*.whl
### National Instruments :tm: Visa
-If you want to control the Power Meter on Linux, with pyvisa library or with SCPI commands within your CVI or LabView application,
-you have to install National Instruments :tm: Visa Runtime (May be installed already if you installed NI LabView or NI CVI).
-Once installed you must switch the driver for the Power Meter manually by using Thorlabs Driver Switcher or Windows
+If you want to control the Power Meter with the pyvisa library and with SCPI commands,
+you have to install National Instruments :tm: Visa Runtime (May be installed already if you installed NI LabView or NI CVI).
+This works also on many Linux systems.
+On Windows, you must then switch the driver for the Power Meter manually by using Thorlabs Driver Switcher or Windows
Device Manager (Experts only). Once the runtime is installed and driver has been switched you can install pyvisa python library
via command.
@@ -62,4 +63,63 @@ via command.
python -m pip install pyvisa
```
-Note: pyvisa does communicate with Thorlabs Ethernet or Bluetooth LE device interfaces.
+Note: pyvisa does not communicate with Thorlabs Ethernet or Bluetooth LE device interfaces.
+
+### Raspberry Pi 4
+On Raspberry Pi with ARM Linux, both TLVisa and NI Visa do not work. Another approach is using the Pyvisa-Py backend.
+We tested the following procedure on Raspberry Pi 4, Linux 13 (Trixie), PM100D3.
+```
+sudo apt update && sudo apt upgrade -y
+```
+Most Raspberry Pi OS versions come with Python pre-installed. Check with:
+```
+python3 --version
+```
+If Python is not already installed:
+```
+sudo apt install python3 python3-pip -y
+```
+If libusb is not installed:
+```
+sudo apt install libusb-1.0-0-dev
+```
+Install Pyvisa and Pyvisa-Py:
+```
+sudo apt install python3-pyvisa
+sudo apt install python3-pyvisa-py
+sudo apt install python3-usb
+sudo apt install zeroconf
+```
+In order to get the permission to acces the power meter, create usbgroup and add your user:
+```
+sudo groupadd usbgroup
+sudo usermod -aG usbgroup $USER
+```
+then set a udev rule, open the following file to edit:
+```
+sudo nano /etc/udev/rules.d/99-usbgroup.rules
+```
+(create directory and file if it does not exist)
+Insert the following line into the file
+```
+SUBSYSTEM=="usb", GROUP="usbgroup", MODE="0666"
+```
+(This is for all USB devices. You can also set more specific rules)
+Reload and reboot to activate the rules
+```
+sudo udevadm control --reload-rules
+sudo udevadm trigger
+sudo reboot
+```
+Plug in the powermeter and check with lsusb, if the device is present.
+Run in Python:
+```
+import pyvisa
+rm = pyvisa.ResourceManager('@py')
+print(rm.list_resources())
+inst = rm.open_resource('USB0::4883::32921::P000000064::0::INSTR')#substitute with the actual resource string that you get from the previous command
+print(inst.query('*IDN?'))
+print(inst.query('MEAS?'))
+```
+Also see the above ```PMxxx_SCPI_pyvisa.py```
+
From 92c905c12feb7abd1654c5c7e8dd0fea55afabbe Mon Sep 17 00:00:00 2001
From: gboedecker <125995107+gboedecker@users.noreply.github.com>
Date: Wed, 14 Jan 2026 15:48:08 +0100
Subject: [PATCH 4/4] New Power meter app example
---
Matlab/Powermeter/PowermeterAppExample.mlapp | Bin 0 -> 16815 bytes
Matlab/Powermeter/README.md | 4 ++--
2 files changed, 2 insertions(+), 2 deletions(-)
create mode 100644 Matlab/Powermeter/PowermeterAppExample.mlapp
diff --git a/Matlab/Powermeter/PowermeterAppExample.mlapp b/Matlab/Powermeter/PowermeterAppExample.mlapp
new file mode 100644
index 0000000000000000000000000000000000000000..3d55b5ec9aa66d96788830c737c41d66e7429ec3
GIT binary patch
literal 16815
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