Skip to content

Frame 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 frame_scanning.cpp
 * @brief Example demonstrating how to read profiles from all the scan heads
 * in the scan system.
 *
 * "Frame Scanning" is the recommended mode of scanning for most applications.
 * It allows the data from the scan heads to be returned as an organized
 * collection of profiles that can be processed together.
 *
 * Further information regarding Frame Scanning can be found online:
 *
 *    http://api.joescan.com/doc/v16/articles/frame-scanning.html
 */

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

#ifdef _WIN32
#include "Windows.h"
#include "processthreadsapi.h"
#endif

static std::atomic<bool> _is_scanning(false);
static std::atomic<uint32_t> _frame_count(0);
static std::atomic<uint32_t> _profile_count(0);
static std::atomic<uint32_t> _invalid_count(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 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 set scan head configuration", r);
    }

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

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

    r = jsScanHeadSetCableOrientation(scan_head, JS_CABLE_ORIENTATION_UPSTREAM);
    if (0 > r) {
      throw ApiError("failed to set cable orientation", r);
    }
  }
}

/**
 * @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 This function receives frames of profile data from all the scan heads
 * in the scan system.
 *
 * @param scan_system Reference to the scan sysetm to recieve profile data from.
 */
static void receiver(jsScanSystem scan_system,
                     std::vector<uint32_t> serial_numbers)
{
  // For applications with heavy CPU load, it is advised to boost the priority
  // of the thread reading out the frame data. If the thread reading out the
  // scan data falls behind, data will be dropped, causing problems later on
  // when trying to analyze what was scanned.
#ifdef _WIN32
  SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
#endif
  jsProfile* profiles = nullptr;

  try {
    // Before receiving frames, we need to allocate memory for a buffer to
    // receive the frame data. To find the size of the buffer, we can call the
    // below function.
    int32_t r = jsScanSystemGetProfilesPerFrame(scan_system);
    if (0 >= r) {
      throw ApiError("failed to read frame size", r);
    }

    // We now have the buffer size and can allocate memory for it.
    uint32_t profiles_per_frame = uint32_t(r);
    profiles = new jsProfile[uint32_t(profiles_per_frame)];

    while (_is_scanning) {
      // Keep the CPU idle until a frame is ready to read out.
      r = jsScanSystemWaitUntilFrameAvailable(scan_system, 1000000);
      if (0 == r) {
        continue;
      } else if (0 > r) {
        throw ApiError("failed to wait for frame", r);
      }

      // After receiving a successful return code from
      // `jsScanSystemWaitUntilFrameAvailable`, we are now ready to read out
      // a frame of scan data.
      r = jsScanSystemGetFrame(scan_system, profiles);
      if (0 == r) {
        _invalid_count += profiles_per_frame;
        continue;
      } else if (0 > r) {
        throw ApiError("failed to read frame", r);
      }

      // Positive return code `r` indicates the total number of valid profiles
      // in the received scan frame. This value can be equal to or less than
      // the frame buffer size. If equal, we have a complete frame of scan data.
      // If less than, we have a partial frame, meaning something went wrong
      // and not all of the slots hold a profile with scan data.
      _profile_count += r;
      _frame_count += 1;

      // Iterate through the frame buffer and inspect the profiles. For this
      // application, we will just check if
      uint32_t valid_count = 0;
      for (uint32_t n = 0; n < profiles_per_frame; n++) {
        // Check to see if the profile is valid before processing it. This is
        // important when dealing with partial frames where not all of the
        // profiles in the buffer are guaranteed to be valid.
        if (jsRawProfileIsValid(profiles[n])) {
          // A valid profile was found.
          valid_count++;
        } else {
          // An invalid profile was found; this is a partial frame.
          // For this example, we will print a message to the console.
          _invalid_count++;
          std::cout << "Invalid: " << profiles[n].sequence_number << " "
            << serial_numbers[profiles[n].scan_head_id];
          if (JS_CAMERA_A == profiles[n].camera) {
            std::cout << ".A.";
          } else {
            std::cout << ".B.";
          }
          std::cout << profiles[n].laser << std::endl;
        }
      }
      if (valid_count != profiles_per_frame) {
        std::cout << "received " << valid_count << " of " << profiles_per_frame
          << std::endl;
      }
    }
  } catch (ApiError& e) {
    std::cout << "ERROR: " << e.what() << std::endl;
    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;
    }
  }

  // We're done scanning and no longer need the frame buffer.
  if (nullptr != profiles) {
    delete[] profiles;
  }
}

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

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

  // Grab the serial number(s) passed in through the command line.
  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 & configure all the scan heads.
    initialize_scan_heads(scan_system, scan_heads, serial_numbers);

    r = jsScanSystemConnect(scan_system, 10);
    if (0 > r) {
      throw ApiError("failed to connect", r);
    } else if (jsScanSystemGetNumberScanHeads(scan_system) != r) {
      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_phase_table(scan_system, scan_heads);

    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 = jsScanSystemStartFrameScanning(scan_system,
                                       min_period_us,
                                       data_format);
    if (0 > r) {
      throw ApiError("failed to start scanning", r);
    }

    _is_scanning = true;
    thread = std::thread(receiver, scan_system, serial_numbers);

    // 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));
  } 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;
    }
  }

  _is_scanning = false;
  if (thread.joinable()) {
    thread.join();
  }

  std::cout << "stop scanning" << std::endl;
  r = jsScanSystemStopScanning(scan_system);
  if (0 > r) {
    std::cout << "ERROR: failed to stop scanning" << std::endl;
  }

  std::cout << "read " << _frame_count << " frames (" << _profile_count
            << " profiles, " << _invalid_count << " invalid)" << std::endl;

  r = jsScanSystemDisconnect(scan_system);
  if (0 > r) {
    std::cout << "ERROR: failed to disconnect" << std::endl;
  }

  jsScanSystemFree(scan_system);

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