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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