This document details the connection points between the Frontend (ImGui/DirectX) and the Backend (AcquisitionController). It maps specific UI elements to the underlying C++ logic.
This section outlines the line-by-line connection between the GUI elements in mainMultiThread.cpp and the Backend functions in AcquisitionControllerDetectLine.
| User Action | GUI Code | Backend Call | Notes |
|---|---|---|---|
| Clicks "Discover Boards" | if (ImGui::Button("Discover Boards")) |
g_AcqController.DiscoverBoards(); |
Initializes hardware handles. |
| Clicks "Apply Config" | if (ImGui::Button("Apply Config")) |
g_AcqController.ConfigureAllBoards(g_boardConfig); |
Sends settings to Alazar cards. |
| Clicks "START ACQUISITION" | acquisitionThread = std::thread([acq]() { ... }); |
g_AcqController.RunAcquisition(acq); |
Spawns a new thread so the GUI doesn't freeze. |
- Purpose: To get the waveform (ChA) and the red peak lines simultaneously so they don't jitter relative to each other.
- Backend Call:
g_AcqController.GetLatestSnapshot(snapshotWave, snapshotPeaks); - Internal Mechanism: This function locks
m_guiDataMutex, copiesm_guiSnapshotWaveformandm_guiSnapshotPeaks, and unlocks.
- Purpose: To get the raw data for M1 (Fast), M2 (Slow), or Aux channels.
- Backend Call:
g_AcqController.GetLatestScopeData(data, boardID, channelIndex);
- Purpose: To update the peak detection thresholds in real-time.
- Backend Call:
g_AcqController.SetAlgoParams(tHigh, tLow, tDist);
- Purpose: To get X and Y data arrays to plot against each other.
- Backend Call:
g_AcqController.GetLatestScopeData(dataX, ...)andGetLatestScopeData(dataY, ...) - Note: This reuses the generic "Scope Data" getter but maps it to X and Y axes in the plot based on user selection.
- Purpose: To see the results of the "One-Shot" synchronization test.
- Backend Call:
g_AcqController.GetSyncSnapshotMulti(boardID, channelID, ...) - Mechanism: Accesses the specific
m_syncSnapshotsmap populated byRunSyncTest.
- Purpose: To retrieve the fully constructed 2D image (1024x1024).
- Backend Call:
g_AcqController.GetLatestImage(image2D); - Mechanism: This copies the
m_finalImagearray (filled by the Generator Thread) into a local vector for ImPlot to render.
Below is a breakdown of the critical functions responsible for managing the data pipeline.
- Role: Main Entry Point
- Function: Arms the hardware, configures DMA buffers, and enters the primary
while()loop that waits for the hardware to signal "Buffer Full." It serves as the heartbeat of the ingestion process.
- Role: Raw Data Router
- Function: Called immediately when a DMA transfer completes. It performs three critical tasks:
- De-interleaves the raw multiplexed stream into Channels A, B, C, and D.
- Updates the thread-safe vectors for the "Live Signals" GUI.
- Packages the data into
DataChunkstructs and pushes them into theProcessingQueueandSaveQueue.
- Role: Circular Buffer Manager & Analyst
- Function: This thread sits in a loop waiting for data from the
ProcessingQueue.- Writes: It copies new data into the Circular Buffer (Ring Memory), handling the wrap-around logic.
- Analyzes: It runs the Peak Detection algorithm on Channel B (Fast Axis) to identify the start and stop indices of every scan line.
- Notifies: Once lines are found, it signals the Generator Thread.
- Role: Image Reconstruction
- Function: This thread sleeps until notified by the Detector.
- Reads: It looks into the Circular Buffer using the indices provided by the Detector.
- Processes: It grabs the pixels for a specific line, applies Bidirectional Reversal (if necessary), and maps the data to the correct Y-row based on the Channel C voltage.
- Outputs: Writes the final pixels to the 2D
m_finalImagearray for the Heatmap display.
- Role: Disk I/O
- Function: A completely independent thread that pulls data from the
SaveQueueand streams binary data to the SSD. It runs at a lower priority to ensure that file saving never blocks the live viewing or acquisition processes.
- Role: Calibration Math
- Function: Performs a Cross-Correlation calculation between two signals (usually Channel A from two different boards). It returns the integer sample delay (Lag) required to align the boards perfectly.
- Role: GUI Accessors
- Function: Thread-safe "Getter" functions that lock mutexes and return copies of the current data state. These allow the GUI (ImGui/DirectX) to render at 60 FPS without crashing the high-speed acquisition threads.
- Main Thread (GUI): Runs
main.cpp. It owns theg_AcqControllerobject. It polls (asks for) data every frame (60 times a second). - Acquisition Thread: Spawned when you click "Start". It runs
RunAcquisition. It pumps data from hardware to the Controller's queues. - Helper Threads (Detector/Generator/Save): Created inside the
AcquisitionControllerconstructor. They process data in the background and update the variables that the GUI eventually reads.
[Image of software architecture diagram]
graph TD
%% --- LAYERS ---
subgraph "Frontend: Main Thread (main.cpp)"
GuiLoop[ImGui Render Loop]
subgraph "GUI Components"
Controls[Control Panel Window]
TabLive[Tab: Live Signals]
TabXY[Tab: XY Scan Path]
TabSync[Tab: Sync Check]
WinImage[Window: Images]
end
end
subgraph "Backend: Public Interface (AcquisitionController)"
%% Configuration & Commands
CmdDiscover[DiscoverBoards]
CmdConfig[ConfigureAllBoards]
CmdStart[RunAcquisition]
CmdSync[RunSyncTest]
CmdParams[SetAlgoParams]
%% Data Accessors (Getters)
GetScope[GetLatestScopeData]
GetSnap[GetLatestSnapshot]
GetSync[GetSyncSnapshotMulti]
GetImg[GetLatestImage]
end
subgraph "Backend: Worker Threads"
ThreadAcq[Acquisition Thread]
ThreadProc[Processing Threads]
end
%% --- INTERACTIONS ---
%% 1. Startup & Config
GuiLoop --> Controls
Controls -- "Click Discover" --> CmdDiscover
Controls -- "Click Apply Config" --> CmdConfig
%% 2. Command Execution
Controls -- "Click START" --> SpawnAcq1[Spawn Thread]
SpawnAcq1 --> CmdStart
CmdStart --> ThreadAcq
Controls -- "Click Sync Test" --> SpawnSync[Spawn Thread]
SpawnSync --> CmdSync
CmdSync --> ThreadAcq
%% 3. Data Flow (Producer -> Data)
ThreadAcq -- "Raw Data" --> ThreadProc
ThreadProc -- "Update Internal State" --> BackendState[(Mutex Protected Data)]
%% 4. Visualization (Polling Loop)
GuiLoop --> TabLive
TabLive -- "Every Frame" --> GetSnap
TabLive -- "Drag Slider" --> CmdParams
GetSnap -.-> BackendState
GuiLoop --> TabXY
TabXY -- "Every Frame" --> GetScope
GetScope -.-> BackendState
GuiLoop --> TabSync
TabSync -- "Every Frame" --> GetSync
GetSync -.-> BackendState
GuiLoop --> WinImage
WinImage -- "Every Frame" --> GetImg
GetImg -.-> BackendState
The system follows a Producer-Consumer pattern with detached processing threads to ensure zero-latency acquisition.
graph TD
%% Define Nodes
subgraph "Thread 1: Acquisition (Producer)"
H[Hardware: Alazar Card] -->|DMA Transfer| P[ProcessBufferData]
P -->|Raw Data Chunk| Q1[Processing Queue]
P -->|Raw Data Chunk| Q2[Save Queue]
P -->|Visual Snapshot| G1[GUI Scope Data]
end
subgraph "Thread 2: Detector (Consumer)"
Q1 -->|Pop Chunk| D[DetectorThreadLoop]
D -->|Write| CB[Circular Buffer]
CB -->|Read Window| PD[Peak Detection Algorithm]
PD -->|Found Line| LP[Line Parameters]
LP -->|Notify| CV[Condition Variable]
end
subgraph "Thread 3: Generator (Processor)"
CV -->|Wake Up| GEN[GeneratorThreadLoop]
GEN -->|Read Lines| CB
GEN -->|Map Voltage to Y| IMG[Final Image Array]
end
subgraph "Thread 4: Saver (Disk I/O)"
Q2 -->|Pop Chunk| S[SaveThreadLoop]
S -->|Write Binary| HDD[Hard Drive .bin]
end
%% Styles
style CB fill:#f9f,stroke:#333,stroke-width:4px
style IMG fill:#ff9,stroke:#333,stroke-width:2px
The core of this data acquisition software is a Circular Buffer (or Ring Buffer). This data structure acts as a high-speed, temporary storage reservoir that decouples the hardware (Producer) from the software processing (Consumer).
In high-speed laser scanning, data arrives continuously and cannot be paused. If the software pauses to update the GUI or save a file, the hardware must have a place to continue dumping data, or that data is lost forever. The circular buffer provides this "safety margin."
The system is designed as a pipeline with three distinct stages. Data flows through these stages using memory buffers to ensure no stage blocks the others.
- Source: AlazarTech ATS9440 Digitizer.
- Mechanism: Direct Memory Access (DMA). The card writes directly into PC RAM without CPU intervention.
- Unit of Transfer: A "Buffer" (or Block). The card does not send individual samples; it fills a large block of memory (e.g., 8MB) and triggers an interrupt when full.
- Variable:
buffersPerAcquisitiondetermines how many of these blocks are captured (set to infinite/continuous for live scanning).
- Location:
DetectorThreadLoopinAcquisitionController.cpp. - Action: When a DMA Buffer arrives, this thread wakes up, copies the raw data into the Circular Buffer, and immediately tells the card "I'm done, you can reuse that DMA memory."
- Key Logic: It uses a "Write Head" index to place data. When it reaches the end of the allocated memory, it wraps around to the beginning (index 0).
- Location:
DetectorThreadLoop(Analysis) andGeneratorThreadLoop(Visualization). - Action: These functions "chase" the Write Head. They read data that has just been written to find signal peaks and construct the image.
The stability of the system depends on balancing Throughput (Data Rate) vs. Latency (CPU Overhead). This balance is controlled by three key variables.
- Definition: The number of data points constituting one "unit" of physical reality (e.g., one laser line, or one A-scan).
- Constraint: Must be a multiple of the digitizer's memory alignment (usually 64 or 128 bytes).
- Impact: If this is too small, you break the image structure. If too large, you capture dead time between triggers.
- Definition: How many "Lines" or "Records" are bundled together into one DMA transfer interrupt.
-
The Math of CPU Load: Every time a buffer fills, the CPU receives an interrupt to process
ProcessBufferData.-
Scenario 1 (Too Small):
- 1 Record per Buffer at 100 kHz Trigger Rate.
- Interrupt Rate: 100,000 times/second.
- Result: API Failure / System Freeze. The CPU spends 100% of its time entering/exiting interrupt handlers (Context Switching) and has zero time to actually process the data.
-
Scenario 2 (Too Large):
- 10,000 Records per Buffer.
- Update Rate: Once every 10 seconds.
- Result: High Latency. The GUI looks frozen because it only updates huge chunks at a time. The Circular Buffer must be massive to hold this surge of data.
-
Scenario 3 (Optimal):
- Target an interrupt rate of roughly 60 Hz to 100 Hz (video frame rate).
- Formula:
$$N_{records} \approx \frac{\text{Trigger Frequency (Hz)}}{\text{Target Update Rate (Hz)}}$$
-
Scenario 1 (Too Small):
-
Definition: The total size of the software ring buffer (
m_cb_Img). -
Formula:
$$\text{Total Size} = \text{SegmentsPerBuffer} \times \text{BufferCount}$$ -
The Math of Overflow:
If your processing algorithms (Peak Detection + Saving) are slower than the incoming data rate, the Write Head will eventually lap the Read Pointer.
-
Time to Overflow:
$$T_{safety} = \frac{\text{Buffer Size (MB)}}{\text{Data Rate (MB/s)} - \text{Processing Rate (MB/s)}}$$ - If Processing Rate > Data Rate, the buffer never overflows.
- If Processing Rate < Data Rate,
$T_{safety}$ is how long you can run before data corruption occurs.
-
Time to Overflow:
The buffer is a standard std::vector (linear memory). We create the "circular" behavior using the Modulo Operator (%).
Inside DetectorThreadLoop, new data arrives in chunk.chA. We copy it to the main buffer m_cb_Img at the position m_cb_write_head.
// 1. Calculate where to write
size_t samples_to_write = chunk.chA.size();
for (int i = 0; i < samples_to_write; i++) {
// 2. Modulo (%) ensures we wrap from Index 999 back to 0
size_t circular_index = (m_cb_write_head + i) % CIRC_BUFFER_SIZE;
// 3. Write data
m_cb_Img[circular_index] = chunk.chA[i];
}
// 4. Advance the head
m_cb_write_head = (m_cb_write_head + samples_to_write) % CIRC_BUFFER_SIZE;graph TD
%% Define the Incoming Data Source
subgraph Producer [Producer: DetectorThreadLoop]
Chunk[Incoming Data Chunk
(e.g., 4096 samples from Queue)]
end
%% Define the Circular Buffer Structure
subgraph CircularBuffer ["The Circular Buffer (m_cb_Img)"]
direction LR
%% Conceptualizing linear memory as segments for visualization
MemStart[Index 0] --- MemOld[Processed Data] --- MemReadGen[Generator Reading Here] --- MemReadDet[Detector Reading Here] --- MemWrite[Write Head <br/>(Currently Writing)] --- MemEmpty[Free Space] --- MemEnd["Index N-1<br/>(CIRC_BUFFER_SIZE)"]
%% The crucial Wrap-Around link
MemEnd -.->|"Wrap-Around Logic (%)"| MemStart
end
%% Define Pointers/Heads
WriteHead(m_cb_write_head) --> MemWrite
ReadGenPtr(Generator Read Ptr) --> MemReadGen
ReadDetPtr(Detector Read Ptr) --> MemReadDet
%% Data Flow Action
Chunk -->|"memcpy into buffer<br/>at write_head index"| MemWrite
%% Styling for visual clarity
classDef memory fill:#e1e1e1,stroke:#333,stroke-width:2px;
classDef activeWrite fill:#9f9,stroke:#333,stroke-width:2px;
classDef activeRead fill:#ccf,stroke:#333,stroke-width:2px;
classDef processed fill:#f9f,stroke:#333,stroke-width:1px,stroke-dasharray: 5 5;
class MemStart,MemEmpty,MemEnd memory;
class MemWrite activeWrite;
class MemReadGen,MemReadDet activeRead;
class MemOld processed;