Low-Level Scanning¶
Updated Examples
For the most up-to-date examples, please see the public repository on GitHub at https://github.com/JoeScan-Inc/pinchot-net-api/tree/master/examples or the "examples" folder in the software distribution.
// Copyright(c) JoeScan Inc. All Rights Reserved.
//
// Licensed under the BSD 3 Clause License. See LICENSE.txt in the project
// root for license information.
// NOTE: Frame Scanning is the recommended mode of scanning for most
// applications. The below example is provided for legacy purposes and
// for customers requiring low level access.
//
// While Frame Scanning is recommended for most applications, users looking
// for greater manual control of their scan heads, or needing to distribute the
// CPU load across multiple cores, can make use of "Low Level Scanning". When
// scanning in this manner, the profiles are returned individually for each
// scan head and it is the responsibility of the developer to reassemble the
// profiles. It is recommened with Low Level Scanning mode to create a thread
// for each individual scan head in order to achieve the highest performance.
using JoeScan.Pinchot;
// Global variable that will contain the total count
// of profiles received from all heads.
int totalProfiles = 0;
// Set up a token and bind it it Ctrl+C to allow stopping the application early.
using CancellationTokenSource cts = new();
var token = cts.Token;
Console.CancelKeyPress += (_, e) =>
{
Console.WriteLine("Cancelled");
cts.Cancel();
e.Cancel = true;
};
Console.WriteLine($"Pinchot API version: {VersionInformation.Version}");
if (args.Length == 0)
{
Console.WriteLine("Must provide one or more scan head serial numbers as arguments.");
return;
}
// Grab the serial number of the scan head(s) from the command line.
var serialNumbers = new List<uint>();
foreach (string argument in args)
{
if (!uint.TryParse(argument, out uint serial))
{
Console.WriteLine($"Argument {argument} cannot be parsed as a uint.");
return;
}
serialNumbers.Add(serial);
}
// First step is to create a scan system to manage the scan heads.
using var scanSystem = new ScanSystem(ScanSystemUnits.Inches);
// Create a scan head for each serial number passed in through the command line.
// We'll assign each one a unique ID (starting at zero) and use this as the
// index for associating profile data with a given scan head.
uint id = 0;
foreach (uint serialNumber in serialNumbers)
{
var scanHead = scanSystem.CreateScanHead(serialNumber, id++);
Console.WriteLine($"{serialNumber} version: {scanHead.Version}");
}
// Configure all the heads with the same settings and window.
var configuration = new ScanHeadConfiguration();
configuration.SetLaserOnTime(100, 100, 1000);
var scanWindow = ScanWindow.CreateScanWindowRectangular(20.0, -20.0, -20.0, 20.0);
foreach (var scanHead in scanSystem.ScanHeads)
{
scanHead.Configure(configuration);
scanHead.SetWindow(scanWindow);
scanHead.Orientation = ScanHeadOrientation.CableIsUpstream;
scanHead.SetAlignment(0, 0, 0);
}
// Now that the scan heads are configured, we'll connect to the heads.
var connectTimeout = TimeSpan.FromSeconds(3);
var scanHeadsThatFailedToConnect = scanSystem.Connect(connectTimeout);
if (scanHeadsThatFailedToConnect.Count > 0)
{
foreach (var scanHead in scanHeadsThatFailedToConnect)
{
Console.WriteLine($"Failed to connect to scan head {scanHead.SerialNumber}.");
}
return;
}
// For this example we will create a simple phase table where each camera or laser
// has its own phase in the phase table. Note that certain scan head types are
// "camera driven" and use Camera types as arguments whereas others are "laser driven"
// and use Laser types.
foreach (var scanHead in scanSystem.ScanHeads)
{
if (scanHead.Type is ProductType.JS50WX or ProductType.JS50WSC or ProductType.JS50MX)
{
foreach (var camera in scanHead.Cameras)
{
scanSystem.AddPhase();
scanSystem.AddPhaseElement(scanHead.ID, camera);
}
}
else if (scanHead.Type is ProductType.JS50X6B20 or ProductType.JS50X6B30 or ProductType.JS50Z820 or ProductType.JS50Z830)
{
foreach (var laser in scanHead.Lasers)
{
scanSystem.AddPhase();
scanSystem.AddPhaseElement(scanHead.ID, laser);
}
}
else
{
throw new InvalidOperationException($"Invalid scan head type {scanHead.Type}");
}
}
// Once the phase table is created, we can then read the minimum scan period
// of the scan system. This value depends on how many phases there are in the
// phase table and each scan head's laser on time and window configuration.
uint minScanPeriodUs = scanSystem.GetMinScanPeriod();
Console.WriteLine($"Min scan period of {minScanPeriodUs} µs");
// To begin scanning on all of the scan heads, all we need to do is
// command the scan system to start scanning. This will cause all of the
// scan heads associated with it to begin scanning at the specified rate
// and data format.
const DataFormat format = DataFormat.XYBrightnessFull;
scanSystem.StartScanning(minScanPeriodUs, format);
Console.WriteLine("Scanning...");
// In order to achieve a performant application, we'll create a thread
// for each scan head. This allows the CPU load of reading out profiles
// to be distributed across all the cores available on the system rather
// than keeping the heavy lifting in an application within a single process.
var threads = new List<Thread>();
foreach (var scanHead in scanSystem.ScanHeads)
{
var thread = new Thread(() => Receiver(scanHead));
thread.Start();
threads.Add(thread);
}
// The current thread can now go on to do other things while the receiver
// threads gather profiles. This is especially important in GUI applications
// as to not lock up the main UI thread.
try
{
// Wait for a time to allow receiver threads to
// collect and process a number of profiles.
var scanTime = TimeSpan.FromSeconds(5);
await Task.Delay(scanTime, token);
}
catch (TaskCanceledException) { }
// We've collected all of our data and now it's time to stop scanning.
// Calling this function will cause each scan head within the entire
// scan system to stop scanning.
scanSystem.StopScanning();
Console.WriteLine("Stop scanning");
threads.ForEach(thread => thread.Join());
// We can verify that we received all of the profiles sent by the scan
// heads by reading each scan head's status message and summing up the
// number of profiles that were sent. If everything went well and the
// CPU load didn't exceed what the system can manage, this value should
// be equal to the number of profiles we received in this application.
long expectedProfilesCount = scanSystem.ScanHeads.Sum(sh => sh.RequestStatus().ProfilesSentCount);
Console.WriteLine($"Number of profiles received: {totalProfiles}");
Console.WriteLine($"Number of profiles expected: {expectedProfilesCount}");
/// <summary>
/// This function receives profile data from a given scan head. We start
/// a thread for each scan head to pull out the data as fast as possible.
/// </summary>
void Receiver(ScanHead scanHead)
{
try
{
var profiles = new List<IProfile>();
while (scanSystem.IsScanning || scanHead.NumberOfProfilesAvailable > 0)
{
if (!scanHead.TryTakeNextProfile(out IProfile profile, TimeSpan.FromSeconds(1), token))
{
continue;
}
profiles.Add(profile);
Interlocked.Increment(ref totalProfiles);
// Wait for 100 profiles before processing.
if (profiles.Count < 100)
{
continue;
}
// For this example, we'll grab some profiles and then act on the data before
// repeating this process again. Note that for high performance applications,
// printing to the console while receiving data should be avoided as it
// can add significant latency. This example only prints to the console to
// provide some illustrative feedback to the user, indicating that data
// is actively being worked on in multiple threads.
var maxPoint = new Point2D();
foreach (var p in profiles)
{
// Not all points in a profile contain valid data so filter
// out the bad ones with this convenience function.
foreach (var point in p.GetValidXYPoints())
{
if (point.Y > maxPoint.Y)
{
maxPoint = point;
}
}
}
Console.WriteLine($"Scan head {scanHead.ID}: [{maxPoint.X:F3}, {maxPoint.Y:F3}]");
profiles.Clear();
}
}
catch (OperationCanceledException)
{
// Thrown by TryTakeNextProfile when the token cancelled, gracefully exit
}
}