Skip to content

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-c-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.
 */

/**
 * @file low_level_scanning.cpp
 * @brief Example demonstrating how to read profile data from scan heads
 * individually.
 *
 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 * 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.
 */

#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include "joescan_pinchot.h"

static std::mutex lock;
static uint64_t received_profiles = 0;

class ApiError : public std::runtime_error {
 private:
  jsError m_return_code;

 public:
  ApiError(const char* what, int32_t return_code) : std::runtime_error(what)
  {
    if ((0 < return_code) || (JS_ERROR_UNKNOWN > return_code)) {
      m_return_code = JS_ERROR_UNKNOWN;
    } else {
      m_return_code = (jsError) return_code;
    }
  }

  jsError return_code() const
  {
    return m_return_code;
  }
};

/**
 * @brief This function is a small utility function used to explore profile
 * data. In this case, it will iterate over the valid profile data and
 * find the highest measurement in the Y axis.
 *
 * @param profiles Array of profiles from a single scan head.
 * @param num_profiles Total number of profiles contained in array.
 * @return jsProfileData The profile measurement with the greatest Y axis value
 * from the array of profiles passed in.
 */
jsProfileData find_scan_profile_highest_point(jsProfile *profiles,
                                              uint32_t num_profiles)
{
  jsProfileData p = {0, 0, 0};

  for (unsigned int i = 0; i < num_profiles; i++) {
    for (unsigned int j = 0; j < profiles[i].data_len; j++) {
      if (profiles[i].data[j].y > p.y) {
        p.brightness = profiles[i].data[j].brightness;
        p.x = profiles[i].data[j].x;
        p.y = profiles[i].data[j].y;
      }
    }
  }

  return p;
}

/**
 * @brief 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.
 *
 * @param scan_head Reference to the scan head to recieve profile data from.
 */
static void receiver(jsScanHead scan_head)
{
  // Allocate memory to recieve profile data for this scan head.
  const int max_profiles = 100;
  jsProfile *profiles = nullptr;
  profiles = new jsProfile[max_profiles];
  auto id = jsScanHeadGetId(scan_head);

  {
    std::lock_guard<std::mutex> guard(lock);
    std::cout << "begin receiving on scan head ID " << id << std::endl;
  }

  // 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 standard out while receiving data should be avoided as it
  // can add significant latency. This example only prints to standard out to
  // provide some illustrative feedback to the user, indicating that data
  // is actively being worked on in multiple threads.
  uint32_t timeout_us = 1000000;
  int32_t r = 0;
  do {
    jsScanHeadWaitUntilProfilesAvailable(scan_head, max_profiles, timeout_us);
    r = jsScanHeadGetProfiles(scan_head, profiles, max_profiles);
    if (0 < r) {
      auto p = find_scan_profile_highest_point(profiles, r);
      {
        std::lock_guard<std::mutex> guard(lock);
        std::cout << "highest point for scan head ID " << id << " is x=" << p.x
                  << ",y=" << p.y << ",brightness=" << p.brightness
                  << std::endl;
        received_profiles += r;
      }
    }
  } while (0 < r);

  {
    std::lock_guard<std::mutex> guard(lock);
    std::cout << "end receiving on scan head ID " << id << std::endl;
  }

  delete[] profiles;
}

/**
 * @brief Creates a basic phase table using all the scan heads managed by the
 * scan system.
 *
 * @param scan_system Reference to the scan system.
 * @param scan_heads Reference to vector of all created scan heads.
 */
void initialize_phase_table(jsScanSystem &scan_system,
                            std::vector<jsScanHead> &scan_heads)
{
  int32_t r = 0;

  // Assume that the system is comprised of scan heads that are all the same.
  jsScanHeadType type = jsScanHeadGetType(scan_heads[0]);

  // For this example we will create a phase table that interleaves lasers
  // seen by Camera A and Camera B. This allows fast and efficient scanning
  // by allowing one camera to scan while the other has the scan data read out
  // & processed; if the same camera is used back to back, a time penalty
  // will be incurred to ensure scan data isn't overwritten.
  switch (type) {
  case (JS_SCAN_HEAD_JS50X6B20):
  case (JS_SCAN_HEAD_JS50X6B30):
    // Phase | Laser | Camera
    //   1   |   1   |   B
    //   2   |   4   |   A
    //   3   |   2   |   B
    //   4   |   5   |   A
    //   5   |   3   |   B
    //   6   |   6   |   A

    for (int n = 0; n < 3; n++) {
      jsLaser laser = JS_LASER_INVALID;

      // Lasers associated with Camera B
      r = jsScanSystemPhaseCreate(scan_system);
      if (0 != r) {
        throw ApiError("failed to create phase", r);
      }

      laser = (jsLaser) (JS_LASER_1 + n);
      for (auto scan_head : scan_heads) {
        r = jsScanSystemPhaseInsertLaser(scan_system, scan_head, laser);
        if (0 != r) {
          throw ApiError("failed to insert into phase", r);
        }
      }

      // Lasers associated with Camera A
      r = jsScanSystemPhaseCreate(scan_system);
      if (0 != r) {
        throw ApiError("failed to create phase", r);
      }

      laser = (jsLaser) (JS_LASER_4 + n);
      for (auto scan_head : scan_heads) {
        r = jsScanSystemPhaseInsertLaser(scan_system, scan_head, laser);
        if (0 != r) {
          throw ApiError("failed to insert into phase", r);
        }
      }
    }
    break;

  case (JS_SCAN_HEAD_JS50Z820):
  case (JS_SCAN_HEAD_JS50Z830):
    // Phase | Laser | Camera
    //   1   |   1   |   B
    //   2   |   5   |   A
    //   3   |   2   |   B
    //   4   |   6   |   A
    //   5   |   3   |   B
    //   6   |   7   |   A
    //   7   |   4   |   B
    //   8   |   8   |   A

    for (int n = 0; n < 4; n++) {
      jsLaser laser = JS_LASER_INVALID;

      // Lasers associated with Camera B
      r = jsScanSystemPhaseCreate(scan_system);
      if (0 != r) {
        throw ApiError("failed to create phase", r);
      }

      laser = (jsLaser) (JS_LASER_1 + n);
      for (auto scan_head : scan_heads) {
        r = jsScanSystemPhaseInsertLaser(scan_system, scan_head, laser);
        if (0 != r) {
          throw ApiError("failed to insert into phase", r);
        }
      }

      // Lasers associated with Camera A
      r = jsScanSystemPhaseCreate(scan_system);
      if (0 != r) {
        throw ApiError("failed to create phase", r);
      }

      laser = (jsLaser) (JS_LASER_5 + n);
      for (auto scan_head : scan_heads) {
        r = jsScanSystemPhaseInsertLaser(scan_system, scan_head, laser);
        if (0 != r) {
          throw ApiError("failed to insert into phase", r);
        }
      }
    }
    break;

  case (JS_SCAN_HEAD_JS50WSC):
  case (JS_SCAN_HEAD_JS50MX):
    // Phase | Laser | Camera
    //   1   |   1   |   A

    r = jsScanSystemPhaseCreate(scan_system);
    if (0 != r) {
      throw ApiError("failed to create phase", r);
    }

    for (auto scan_head : scan_heads) {
      r = jsScanSystemPhaseInsertCamera(scan_system, scan_head, JS_CAMERA_A);
      if (0 != r) {
        throw ApiError("failed to insert into phase", r);
      }
    }
    break;

  case (JS_SCAN_HEAD_JS50WX):
    // Phase | Laser | Camera
    //   1   |   1   |   A
    //   2   |   1   |   B

    r = jsScanSystemPhaseCreate(scan_system);
    if (0 != r) {
      throw ApiError("failed to create phase", r);
    }

    for (auto scan_head : scan_heads) {
      r = jsScanSystemPhaseInsertCamera(scan_system, scan_head, JS_CAMERA_A);
      if (0 != r) {
        throw ApiError("failed to insert into phase", r);
      }
    }

    r = jsScanSystemPhaseCreate(scan_system);
    if (0 != r) {
      throw ApiError("failed to create phase", r);
    }

    for (auto scan_head : scan_heads) {
      r = jsScanSystemPhaseInsertCamera(scan_system, scan_head, JS_CAMERA_B);
      if (0 != r) {
        throw ApiError("failed to insert into phase", r);
      }
    }
    break;

  case (JS_SCAN_HEAD_INVALID_TYPE):
  default:
    throw ApiError("invalid scan head type", 0);
  }
}

/**
 * @brief Initializes and configures scan heads.
 *
 * @param scan_system Reference to the scan system.
 * @param scan_heads Reference to vector to be filled with created scan heads.
 * @param serial_numbers Reference to vector of serial numbers of scan heads
 * to initialize.
 */
void initialize_scan_heads(jsScanSystem &scan_system,
                           std::vector<jsScanHead> &scan_heads,
                           std::vector<uint32_t> &serial_numbers)
{
  int32_t r = 0;

  jsScanHeadConfiguration config;
  config.laser_on_time_min_us = 100;
  config.laser_on_time_def_us = 100;
  config.laser_on_time_max_us = 1000;
  config.laser_detection_threshold = 120;
  config.saturation_threshold = 800;
  config.saturation_percentage = 30;

  // Create a scan head for each serial number passed in on the command line
  // and configure each one with the same parameters. Note that there is
  // nothing stopping users from configuring each scan head independently.
  for (unsigned int i = 0; i < serial_numbers.size(); i++) {
    uint32_t serial = serial_numbers[i];
    auto scan_head = jsScanSystemCreateScanHead(scan_system, serial, i);
    if (0 > scan_head) {
      throw ApiError("failed to create scan head", (int32_t) scan_head);
    }
    scan_heads.push_back(scan_head);

    uint32_t major, minor, patch;
    r = jsScanHeadGetFirmwareVersion(scan_head, &major, &minor, &patch);
    if (0 > r) {
      throw ApiError("failed to read firmware version", r);
    }

    std::cout << serial << " v" << major << "." << minor << "." << patch
              << std::endl;

    r = jsScanHeadSetConfiguration(scan_head, &config);
    if (0 > r) {
      throw ApiError("failed to configure scan head", r);
    }

    r = jsScanHeadSetWindowRectangular(scan_head, 20.0, -20.0, -20.0, 20.0);
    if (0 > r) {
      throw ApiError("failed to set scan window", r);
    }

    r = jsScanHeadSetAlignment(scan_head, 0.0, 0.0, 0.0);
    if (0 > r) {
      throw ApiError("failed to set alignment", r);
    }
  }
}

int main(int argc, char *argv[])
{
  jsScanSystem scan_system;
  std::vector<jsScanHead> scan_heads;
  std::thread *threads = nullptr;
  int32_t r = 0;

  if (argc < 2) {
    std::cout << "Usage: " << argv[0] << " SERIAL..." << std::endl;
    return 0;
  }

  std::vector<uint32_t> serial_numbers;
  for (int i = 1; i < argc; i++) {
    serial_numbers.emplace_back(strtoul(argv[i], NULL, 0));
  }

  {
    const char *version_str;
    jsGetAPIVersion(&version_str);
    std::cout << "joescanapi " << version_str << std::endl;
  }

  try {
    scan_system = jsScanSystemCreate(JS_UNITS_INCHES);
    if (0 > scan_system) {
      throw ApiError("failed to create scan system", (int32_t) scan_system);
    }

    // Initialize all the scan heads in the scan system and configure them
    // for scanning.
    initialize_scan_heads(scan_system, scan_heads, serial_numbers);

    // Now that the scan heads are configured, we'll connect to the heads.
    r = jsScanSystemConnect(scan_system, 10);
    if (0 > r) {
      // This error condition indicates that something wrong happened during
      // the connection process itself and should be understood by extension
      // that none of the scan heads are connected.
      throw ApiError("failed to connect", r);
    } else if (jsScanSystemGetNumberScanHeads(scan_system) != r) {
      // On this error condition, connection was successful to some of the scan
      // heads in the system. We can query the scan heads to determine which
      // one successfully connected and which ones failed.
      for (auto scan_head : scan_heads) {
        if (false == jsScanHeadIsConnected(scan_head)) {
          uint32_t serial = jsScanHeadGetSerial(scan_head);
          std::cout << serial << " is NOT connected" << std::endl;
        }
      }
      throw ApiError("failed to connect to all scan heads", 0);
    }

    // Initialize the phase table for the scan system. This will schedule
    // scanning time for all of the scan heads.
    initialize_phase_table(scan_system, scan_heads);

    // 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 & window
    // configuration.
    int32_t min_period_us = jsScanSystemGetMinScanPeriod(scan_system);
    if (0 >= min_period_us) {
      throw ApiError("failed to read min scan period", min_period_us);
    }
    std::cout << "min scan period is " << min_period_us << " us" << std::endl;

    std::cout << "start scanning" << std::endl;
    jsDataFormat data_format = JS_DATA_FORMAT_XY_BRIGHTNESS_FULL;
    r = jsScanSystemStartScanning(scan_system, min_period_us, data_format);
    if (0 > r) {
      throw ApiError("failed to start scanning", r);
    }

    // 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.
    threads = new std::thread[scan_heads.size()];
    for (unsigned long i = 0; i < scan_heads.size(); i++) {
      threads[i] = std::thread(receiver, scan_heads[i]);
    }

    // Put this thread to sleep until the total scan time is done.
    unsigned long int scan_time_sec = 10;
    std::this_thread::sleep_for(std::chrono::seconds(scan_time_sec));

    r = jsScanSystemStopScanning(scan_system);
    if (0 > r) {
      throw ApiError("failed to stop scanning", r);
    }

    for (unsigned long i = 0; i < scan_heads.size(); i++) {
      threads[i].join();
    }
    delete[] threads;
    threads = nullptr;
    std::cout << "stop scanning" << std::endl;

    // 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.
    jsScanHeadStatus status;
    unsigned int expected = 0;
    for (auto scan_head : scan_heads) {
      r = jsScanHeadGetStatus(scan_head, &status);
      if (0 > r) {
        throw ApiError("failed to obtain status message", r);
      }
      expected += status.num_profiles_sent;
    }
    std::cout << "received " << received_profiles << " profiles" << std::endl;
    std::cout << "expected " << expected << " profiles" << std::endl;

    r = jsScanSystemDisconnect(scan_system);
    if (0 > r) {
      throw ApiError("failed to disconnect", r);
    }
  } catch (ApiError &e) {
    std::cout << "ERROR: " << e.what() << std::endl;
    r = 1;

    const char *err_str = nullptr;
    jsError err = e.return_code();
    if (JS_ERROR_NONE != err) {
      jsGetError(err, &err_str);
      std::cout << "jsError (" << err << "): " << err_str << std::endl;
    }
  }

  // Free memory allocated for threads if not already done.
  if (nullptr != threads) {
    for (unsigned long i = 0; i < scan_heads.size(); i++) {
      threads[i].join();
    }
    delete[] threads;
  }

  jsScanSystemFree(scan_system);

  return (0 == r) ? 0 : 1;
}

Comments