Skip to main content

C++ Live Weight Reader Tutorial

Introduction

In this tutorial, we will be creating a simple console application in C++ that opens, claims and enables a scale device and provides a live weight readings of items placed on the scale platter. This tutorial only covers a very basic aspect of development using OPOS with C++, but should suffice as a starting point for any application that looks to read weights from a Datalogic scale device.

Prerequisites

Developing an application for a Datalogic scale device requires you to first have Datalogic's OPOS installed. The OPOS installer ensures the required ActiveX Controls are registered and any supporting files are present.

Creating a project

  1. Using Visual Studio 2019, navigate to File > New > Project ...

  2. In the Create a new project dialog, set the filter to C++, select Console App and select Next

  3. In the Configure your new project dialog, declare your Project name, Location and Solution name as desired and then select Create.

Exposing OPOS control objects

The very first step is to reference the scale ActiveX Control by its ProgID (Programmatic Identifier) using the #import directive.

#import "progid:OPOS.Scale"

int main()
{
return 0;
}

Compiling the project at this point, the #import directive generates two files in your $(Configuration) folder (i.e. Debug/Release). OPOSScale.tlh is the Type Library Header and OPOSScale.tli is the Type Library Interface. These files contain generated type information that bridges the COM object's interface with C++ language constructs.

While this makes the compiler aware of the COM object you're accessing, you'll likely find it helpful to make the Visual Studio IDE aware of of it as well; to do this you'll need to import the type library. Inside OPOSScale.tlh are a series of forward declared structs. The first struct reads:

struct __declspec(uuid("<some-unique-identifier>"))
/* LIBID */ __OposScale_CCO;

Adding #import "libid:<some-unique-identifier>" to your project will inform the IDE of the COM interface and everything in the namespace OposScale_CCO, but is not required for compilation.

Now your application should look similar to:

// This import statement causes generation of OPOSScale.tlh and OPOSScale.tli
#import "progid:OPOS.Scale"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScale_CCO, but is not required for compilation.
#import "libid:ccb90170-b81e-11d2-ab74-0040054c3719"

int main()
{
return 0;
}

Creating the OPOS scale interface

CoInitializeEx() must be called near the start of program to load and initialize the COM library and CoUninitialize() must be called near the end of the program to unload the COM library.

The scale is implemented using the smart pointer IOPOSScalePtr defined in OPOSScale.tlh as _COM_SMARTPTR_TYPEDEF(IOPOSScale, __uuidof(IOPOSScale)), based on the _com_ptr_t class.

An instance of the scale is created by calling _com_ptr_t::CreateInstance(const char *progid) and is release by calling _com_ptr_t::Release() when no longer needed. Failure to release the object can cause problems with an actual POS system.

// This import statement causes generation of OPOSScale.tlh and OPOSScale.tli
#import "progid:OPOS.Scale"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScale_CCO, but is not required for compilation.
#import "libid:ccb90170-b81e-11d2-ab74-0040054c3719"

int main()
{
// Load and initialize the COM library.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

// Create a COM object and point to it.
OposScale_CCO::IOPOSScalePtr scale;
scale.CreateInstance("OPOS.Scale");

//
// more to come ....
//

// Release the COM object
scale.Release();

// Unload the COM library.
CoUninitialize();
return 0;
}

Accessing the OPOS Scale interface

At this point, you are ready to use the scale object.

The OPOS standard defines a general sequence to access a device:

  • open: open a context to the device
  • claim: claim control over the device
  • enable: enable the device's operation
  • disable: disable the device's operation
  • release: release control over the device
  • close: close the context to the device

Opening a scale context requires you to reference a device profile (a device profile simply being a name representing a set of parameters relevant to the device). When OPOS is installed, scale profiles are seen as the subkey names installed under the UPOS-specified registry key HKEY_LOCAL_MACHINE\Software\Wow6432Node\OLEforRetail\ServiceOPOS\SCALE. It is your choice whether you want to programmatically obtain the profile names from the registry or to simply hard-code them in your application.

If the scale context is successfully opened, you can then claim the device, gaining exclusive access to it. And if the device is successfully claimed, you can then enable it to perform subsequent operations.

Enabling live weight reading requires you to include the header file OposScal.h, found in the distribution (typically under C:\Program Files (x86)\DLSOPOS\Controls\OposScal.h).

#include <windows.h>

#include <string>

// For access to scale constants (e.g. SCAL_SN_ENABLED)
#include "OposScal.h"

// This import statement causes generation of OPOSScale.tlh and OPOSScale.tli
#import "progid:OPOS.Scale"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScale_CCO, but is not required for compilation.
#import "libid:ccb90170-b81e-11d2-ab74-0040054c3719"

int main()
{
// Load and initialize the COM library.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

// Create a COM object and point to it.
OposScale_CCO::IOPOSScalePtr scale;
scale.CreateInstance("OPOS.Scale");

// Open a context with the scale (e.g. "USBScale").
std::string profileName("<your selected profile>");
scale->Open(profileName.c_str());

// Claim control of the scale using a 1000 millisecond timeout.
scale->ClaimDevice(1000L);
if (scale->Claimed)
{
// Enable the device.
scale->DeviceEnabled = true;
// Tell the scale we intend to perform "live" weighing.
scale->StatusNotify = SCAL_SN_ENABLED;
// Enable transmission of event data.
scale->DataEventEnabled = true;

//
// more to come ....
//

// Disable, release and close the scale.
scale->DeviceEnabled = false;
scale->ReleaseDevice();
scale->Close();
}

// Release the COM object
scale.Release();

// Unload the COM library.
CoUninitialize();
return 0;
}

Creating a sink for the scale

To make the application useful, we must create a scale sink (an object that receives input from the device).

  1. Using the Visual Studio 2019 Solution Explorer, right mouse click Header Files

  2. And when the dialog appears add a header file named ScaleSink.h

  3. Using the Visual Studio 2019 Solution Explorer, right mouse click Source Files

  4. And when the dialog appears add a source file named ScaleSink.cpp

The sink provides callback functions for use by the COM object. The callback functions provide implementations for the virtual methods of

and the dispatch methods of

  • _IOPOSScaleEvents (defined in OPOSScale.tli)

You will also notice enum ScaleEvent whose values are used by the COM object to map events to the dispatch methods. These will be discussed later in Implementing IDispatch.

The scale sink is derived from OposScale_CCO::_IOPOSScaleEvents as follows

// ScaleSink.h

// This import statement causes generation of OPOSScale.tlh and OPOSScale.tli
#import "progid:OPOS.Scale"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScale_CCO, but is not required for compilation.
#import "libid:ccb90170-b81e-11d2-ab74-0040054c3719"

class ScaleSink: public OposScale_CCO::_IOPOSScaleEvents
{
public:

ScaleSink(OposScale_CCO::IOPOSScale &scaleObject)
: scale(scaleObject)
, ref(0)
{};

// IUnknown methods
IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv);
IFACEMETHODIMP_(ULONG) AddRef();
IFACEMETHODIMP_(ULONG) Release();

// IDispatch methods
IFACEMETHODIMP GetTypeInfoCount(UINT *pctinfo);
IFACEMETHODIMP GetTypeInfo(UINT itinfo,
LCID lcid, ITypeInfo **iti);
IFACEMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *names,
UINT size, LCID lcid,DISPID *rgDispId);
IFACEMETHODIMP Invoke(DISPID dispid, REFIID riid, LCID lcid,
WORD flags,DISPPARAMS *dispparams, VARIANT *result,
EXCEPINFO *exceptioninfo, UINT *argerr);

// _IOPOSScaleEvents methods
HRESULT DataEvent(long Status);
HRESULT DirectIOEvent(
long EventNumber,
long *Data,
BSTR *String);
HRESULT ErrorEvent(
long ResultCode,
long ResultCodeExtended,
long ErrorLocus,
long *ErrorResponse);
HRESULT StatusUpdateEvent(
long Data);

enum ScaleEvent: DISPID
{
Unused = 0,
Data = 1,
DirectIO = 2,
Error = 3,
Reserved = 4,
StatusUpdate = 5,
Count = 6
};

private:

LONG ref;
OposScale_CCO::IOPOSScale &scale;
};

Implementing IUnknown

IUnknown is inherited by every COM interface. IUnknown has three virtual methods:

  • AddRef()
  • QueryInterface()
  • Release()

AddRef() and Release() are used for reference counting and QueryInterface() retrieves the supported interfaces of an object. The implementation of IUnknown is often as straight-forward as seen in this tutorial, which is loosely based off of the MSDN article Implementing IUnknown in C++.

// ScaleSink.cpp
#include "ScaleSink.h"

IFACEMETHODIMP ScaleSink::QueryInterface(REFIID riid, void **ppv)
{
*ppv = nullptr;
IID id = __uuidof(OposScale_CCO::_IOPOSScaleEvents);
HRESULT hr = E_NOINTERFACE;
if (riid == IID_IUnknown || riid == IID_IDispatch || riid == id) {
*ppv = static_cast<OposScale_CCO::_IOPOSScaleEvents *>(this);
AddRef();
hr = S_OK;
}
return hr;
}

IFACEMETHODIMP_(ULONG) ScaleSink::AddRef()
{
return ++ref;
}

IFACEMETHODIMP_(ULONG) ScaleSink::Release()
{
if (--ref == 0)
delete this;
return ref;
}

Implementing IDispatch

IDispatch has four virtual methods:

  • Invoke()
  • GetIDsOfNames()
  • GetTypeInfo()
  • GetTypeInfoCount()

The COM object calls GetIDsOfNames() to obtain the dispatch id (DISPID) of a single class property or method. The dispatch id is then used by the COM object during subsequent calls to Invoke().

Invoke() allows the COM object to call the _IOPOSScaleEvents methods of the sink class. The DISPID parameter identifies the method to call and DISPPARAMS contains the values to be passed to the function.

GetTypeInfo() and GetTypeInfoCount() are used to retrieve type information, but provide no value in our case.

// ScaleSink.cpp
IFACEMETHODIMP ScaleSink::GetTypeInfoCount(UINT *pctinfo)
{
*pctinfo = 0;
return E_NOTIMPL;
}

IFACEMETHODIMP ScaleSink::GetTypeInfo(UINT itinfo, LCID lcid, ITypeInfo **iti)
{
*iti = nullptr;
return E_NOTIMPL;
}

IFACEMETHODIMP ScaleSink::GetIDsOfNames(REFIID riid, LPOLESTR *names,
UINT size, LCID lcid, DISPID *dispids)
{
if (wcscmp(names[0], L"StatusUpdateEvent") == 0)
dispids[0] = ScaleEvent::StatusUpdate;
else if (wcscmp(names[0], L"DirectIOEvent") == 0)
dispids[0] = ScaleEvent::DirectIO;
else if (wcscmp(names[0], L"ErrorEvent") == 0)
dispids[0] = ScaleEvent::Error;
else if (wcscmp(names[0], L"DataEvent") == 0)
dispids[0] = ScaleEvent::Data;
else
dispids[0] = -1;

return ((dispids[0] == -1) ? E_NOTIMPL : S_OK);
}

IFACEMETHODIMP ScaleSink::Invoke(DISPID dispid, REFIID riid, LCID lcid,
WORD flags, DISPPARAMS *dispparams, VARIANT *result,
EXCEPINFO *exceptioninfo, UINT *argerr)
{
if (ScaleEvent::StatusUpdate == dispid)
return StatusUpdateEvent(dispparams->rgvarg[0].lVal);
else
return S_OK;
}

Implementing event functions

With IDispatch implemented the scale sink will invoke its supported events. For the purpose of this tutorial, the scale event data is simply interpreted and printed to the console.

// ScaleSink.cpp
HRESULT ScaleSink::StatusUpdateEvent(long Data)
{
if (Data == SCAL_SUE_STABLE_WEIGHT)
std::cout << WeightFormat(scale.ScaleLiveWeight) << std::endl;
else if (Data == SCAL_SUE_WEIGHT_UNSTABLE)
std::cout << "Scale weight unstable" << std::endl;
else if (Data == SCAL_SUE_WEIGHT_ZERO)
std::cout << WeightFormat(scale.ScaleLiveWeight) << std::endl;
else if (Data == SCAL_SUE_WEIGHT_OVERWEIGHT)
std::cout << "Weight limit exceeded." << std::endl;
else if (Data == SCAL_SUE_NOT_READY)
std::cout << "Scale not ready." << std::endl;
else if (Data == SCAL_SUE_WEIGHT_UNDER_ZERO)
std::cout << "Scale under zero weight." << std::endl;
else
std::cout << "Unknown status [" << Data << "]" << std::endl;

return S_OK;
}

std::string ScaleSink::WeightFormat(int weight)
{
std::string weightStr;

std::string units = UnitAbbreviation(scale.WeightUnits);
if (units.empty())
{
weightStr = "Unknown weight unit";
}
else
{
std::stringstream ss;
ss << std::fixed << std::setw(6) << std::setprecision(3) << (0.001 * (double)weight) << " " << units;
weightStr = ss.str();
}

return weightStr;
}

std::string ScaleSink::UnitAbbreviation(int units)
{
std::string unitStr;

switch (units)
{
case SCAL_WU_GRAM: unitStr = "gr."; break;
case SCAL_WU_KILOGRAM: unitStr = "kg."; break;
case SCAL_WU_OUNCE: unitStr = "oz."; break;
case SCAL_WU_POUND: unitStr = "lb."; break;
}

return unitStr;
}

Connecting the scale and the sink

Now we must inform the scale of the sink. To do this query an IConnectionPointContainer interface by calling QueryInterface() from the scale. On the connection point container, get the interface of the _IOPOSScaleEvents connection point by calling FindConnectionPoint(). Now a connection can be established between the scale and the sink by calling Advise() on the connection point.

The following code can be seen in context in the segment Tying it all together.

IConnectionPointContainer *cpc;
scale->QueryInterface(IID_IConnectionPointContainer, (void **) &cpc);

IConnectionPoint *cp;
cpc->FindConnectionPoint(__uuidof(OposScale_CCO::_IOPOSScaleEvents), &cp);
cpc->Release();

ScaleSink *sink = new ScaleSink(*scale);
DWORD cookie;
cp->Advise(sink, &cookie);

Tying it all together

As any experienced developer knows, a huge part of writing code is defensive programming, guarding against potential errors. So be aware, for the sake of brevity, this tutorial has avoided addressing the myriad issues you may encounter.

That said, a more complete example can be found in the Datalogic OPOS Examples.

Refactoring a little, we can move shared #include and #import statements to a common location (ScaleSink.h in this tutorial).

// ScaleSink.h
#include <windows.h>
#include <string>
#include <iostream>

// For access to scale constants (e.g. SCAL_SN_ENABLED)
#include "OposScal.h"

// This import statement causes generation of OPOSScale.tlh and OPOSScale.tli
#import "progid:OPOS.Scale"

// This import statement informs the IDE of the COM interface and everything in the
// namespace OposScale_CCO, but is not required for compilation.
#import "libid:ccb90170-b81e-11d2-ab74-0040054c3719"

Doing that simplifies the main file a little. Finally, we can add the code connecting the scale to the sink.

#include "ScaleSink.h"
#include <processthreadsapi.h>

static DWORD threadID;
static BOOL handler(DWORD event);

int main()
{
// Setup the console program to exit gracefully.
threadID = GetCurrentThreadId();
SetConsoleCtrlHandler((PHANDLER_ROUTINE)(handler), TRUE);

// Load and initialize the COM library.
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

// Create a COM object and point to it.
OposScale_CCO::IOPOSScalePtr scale;
scale.CreateInstance("OPOS.Scale");

// Open a context with the scale (e.g. "USBScale").
std::string profileName("<your selected profile>");
scale->Open(profileName.c_str());

// Claim control of the scale using a 1000 millisecond timeout.
scale->ClaimDevice(1000L);
if (scale->Claimed)
{
// The scale has been opened and claimed.

// Enable the device.
scale->DeviceEnabled = true;
// Tell the scale we intend to perform "live" weighing.
scale->StatusNotify = SCAL_SN_ENABLED;
// Enable transmission of event data.
scale->DataEventEnabled = true;

// Determine whether scale is connectable
IConnectionPointContainer* cpc;
bool isConnectable = (scale->QueryInterface(IID_IConnectionPointContainer, (void**)&cpc) == S_OK);

if (isConnectable)
{
// Determine whether _IOPOSScaleEvents connection point is supported.
IConnectionPoint* cp;
bool haveConnectionPoint = (cpc->FindConnectionPoint(__uuidof(OposScale_CCO::_IOPOSScaleEvents), &cp) == S_OK);
cpc->Release();

if (haveConnectionPoint)
{
ScaleSink* sink = new ScaleSink(*scale);

// Connect cp with sink (subscribe to the sink).
// cookie is a token representing the connection,
// used later when deleting the connection.
DWORD cookie;
cp->Advise(sink, &cookie);

std::cout << "Press \'Ctrl + C\' to quit." << std::endl;

// The scale message loop. Events will be handled by the methods of the sink.
static MSG msg = { 0 };
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

// Delete the connection (unsubscribe from the sink).
cp->Unadvise(cookie);
cp->Release();
}
}

// Disable, release and close the scale.
scale->DeviceEnabled = false;
scale->ReleaseDevice();
scale->Close();
}

// Release the COM object
scale.Release();

// Unload libraries on this thread.
CoUninitialize();
return 0;
}

BOOL handler(DWORD event)
{
PostThreadMessage(threadID, WM_QUIT, 0, 0);
return TRUE;
}