Tuesday, March 31, 2015

Writing an OPOS Service Object using ATL

What is OPOS?
An architecture that allows Point-of-Sale (POS) devices to be integrated to Microsoft Windows family of operating systems. The devices are divided into categories called device classes such as PIN Pads, Cash Drawers etc.

OPOS model
OPOS Control is software abstraction layer for a device class and it consists of a Control Object and a Service Object. A device is characterized by its Properties, Methods and Events. A POS application accesses a device through the OPOS Control APIs that expose these properties, methods and events. The layers are shown in the figure below taken from the UnifiedPOS specification document[1].




Control Object (CO)
 
  • exposes a set of properties, methods and events
  • is an ActiveX
  • can be downloaded from the web
  
Service Object (SO)
  • called by the CO
  • implements OPOS functionality for a specific device class
  • usually provided by the device manufacturer or third parties
  
How to use an OPOS Control?

The application usually calls the following Control Object methods:
  • add event handlers to get event data
  • call Open to open the device
  • call ClaimDevice to gain exclusive access to the device
  • set DeviceEnabled property to TRUE
  • call any other methods to perform required operations
  • call ReleaseDevice to release the device from exclusive access mode
  • call Close to release the device and associated resources when the application is finished using the device

Relationship between CO and SO methods

The table below shows which Service  Object method is called for the given Control Object method. For example, when the application calls CO's Open method, the CO calls the SO's OpenService method.

Category    Control Object                  Service Object
method      Open                            OpenService
            Close                           CloseService
            other method                    corresponding other method
            
property    get String property             GetPropertyString
            set String property             SetPropertyString
            get Numeric property            GetPropertyNumber
            set Numeric property            SetPropertyNumber
            get/set other property type     corresponding GetProperty/SetProperty

event                                       SO calls a CO event request method:
                                            SOData, SODirectIO, SOError, 
                                            SOOutputComplete and SOStatusUpdate

Control Object Open method
In order to write a Service Object, it's important to know how the Control Object's Open method works. If the Service Object does not satisfy certain requirements, the CO Open method will fail and the application won't be able to use the OPOS control. CO Open method as shown below takes a DeviceName as its argument.

long Open(BSTR DeviceName);

The CO Open method will
  • query the registry to find the device class and device name
  • load the service object for the device
  • check if at least the methods defined in the initial OPOS version for the device class are supported by the SO
  • call SO's OpenService method
  • call SO's GetPropertyNumber(PIDX_ServiceObjectVersion) to get the service object version, then check if the major version number matches that of the CO's
  • check if all methods that must be supported by the particular SO version are available

Service Object version
Three version levels are specified for SO version.
Major: The millions place
Minor: The thousands place
Build: The units place provided by the SO developer specifying a build number

For example, the version number 1002038 is interpreted as 1.2.38.
An SO will work with a CO as long as their major version numbers match.


Service Object for a simple virtual MSR
Now that we know the basics, we'll create a minimal Service Object for a virtual MSR (Magnetic Stripe Reader) supporting following functionality:
open, claim, enable, enable data events, get a data event notification, release and close.


The figure below shows a list of MSR CO methods and the OPOS versions they were first introduced.

Not all MSR CO methods are shown in the above list. For our minimal virtual MSR SO, we are only interested in version 1.0 (i.e. initial version) equivalents.
 

First we should download the latest OPOS Control Objects and header files from here. Download the CCO Runtime zip file, then register the MSR CO. Then follow the steps given below to create the SO.

Steps
1. Launch Visual Studio and create an ATL Project. Name it MyVirtualMsr. Select DLL as the Application Type and check Allow merging proxy/stub code in the ATL Project Wizard.

Create MyVirtualMsr ATL project
Selecting Application type and merging proxy/stub code
2. In the class view, right click the project, then select Add Class to add a Simple ATL Object. Enter MyVirtualMsrSO under Short name and set the ProgID value to VIRTUAL.OPOS.MSR.so. Set the Options as shown and click finish. This will create a CMyVirtualMsrSO class and a IMyVirtualMsrSO interface.
Setting ProgID

Options

3. Add the following methods to the IMyVirtualMsrSO interface (Right click the IMyVirtualMsrSO interface in class view and select add method from the menu. Make sure you set the parameter attributes out and retval for pRC).

HRESULT OpenService(BSTR DeviceClass, BSTR DeviceName, IDispatch* pDispatch, [out, retval] long* pRC);
HRESULT CheckHealth(long Level, [out, retval] long* pRC);
HRESULT ClaimDevice(long ClaimTimeout, [out, retval] long* pRC);
HRESULT ClearInput([out, retval] long* pRC);
HRESULT CloseService([out, retval] long* pRC);
HRESULT COFreezeEvents(VARIANT_BOOL Freeze, [out, retval] long* pRC);
HRESULT DirectIO(long Command, [in, out] long* pData, [in, out] BSTR* pString, [out, retval] long* pRC);
HRESULT ReleaseDevice([out, retval] long* pRC);

HRESULT GetPropertyNumber(long PropIndex, [out, retval] long* pNumber);
HRESULT GetPropertyString(long PropIndex, [out, retval] BSTR* pString);
HRESULT SetPropertyNumber(long PropIndex, long Number);
HRESULT SetPropertyString(long PropIndex, BSTR PropString);

4. Add the following variable to CMyVirtualMsrSO class. We are using this instance for event handling.

CComDispatchDriver m_pDriver;

5. Include OposMsr.hi from the Include directory of the CCO Runtime that you downloaded earlier. This includes the definitions of various OPOS constants such as ResultCodes and property IDs.

#include "OposMsr.hi"

6. Modify the OpenService methos as shown below:

STDMETHODIMP CMyVirtualMsrSO::OpenService(BSTR DeviceClass, BSTR DeviceName, IDispatch* pDispatch, LONG* pRC)
{
    m_pDriver = pDispatch;
    *pRC = OPOS_SUCCESS;
    return S_OK;
}

7. Set return code value pRC of the methods to OPOS_SUCCESS

*pRC = OPOS_SUCCESS;

8. Modify the GetPropertyNumber method as shown below to return a valid version number

STDMETHODIMP CMyVirtualMsrSO::GetPropertyNumber(LONG PropIndex, LONG* pNumber)
{
    if (PIDX_ServiceObjectVersion == PropIndex)
    {
        *pNumber = 1000111;
    }
    return S_OK;
}

9. Create a registry key under OLEforRetail for our device. We'll call our virtual MSR 'MyVirtualDevice' and this is the name that we'll be passing to CO Open method. The default value of the device name key MyVirtualDevice must be the value we entered for the ProgID while we were creating the ATL object, which in our case is the value VIRTUAL.OPOS.MSR.so.

If you are on a 64bit machine:
[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\OLEforRetail\ServiceOPOS\MSR\MyVirtualDevice]
@="VIRTUAL.OPOS.MSR.so"

If you are on a 32bit machine:
[HKEY_LOCAL_MACHINE\SOFTWARE\OLEforRetail\ServiceOPOS\MSR\MyVirtualDevice]
@="VIRTUAL.OPOS.MSR.so"

10. Now build the project. By default, it is set to register the dll after build. If you didn't launch Visual Studio with admin privileges, it'll give you an error. You can either launch Visual Studio with admin privileges, or disable registering by setting Register Output option in the Linker settings to No, then manually register the dll. To manually register,

if you are on a 64bit machine:
c:\windows\syswow64\regsvr32 MyVirtualMsr.dll

if you are on a 32bit machine:
c:\windows\system32\regsvr32 MyVirtualMsr.dll

Now we have satisfied all conditions for a minimal valid MSR Service Object by adding registry entries and methods, then returning a valid version number. This means Control Object Open(DeviceName) method will return success. Now, let's add some code to fire a simple data event to the Control Object.

11. Add a FireDataEvent method to CMyVirtualMsrSO.

void CMyVirtualMsrSO::FireDataEvent(void)
{
    VARIANT v; v.vt = VT_I4; v.lVal = 10;
    m_pDriver.Invoke1(L"SOData", &v);
}

12. We'll let a data event fire 500ms after the application enables Data Events. 
Please note that the code below is just for demo purpose of data events. If you call CO Close method before this 500ms, it'll destroy the SO, and this in turn will crash the application because the pVirtualMsr is no longer a valid object.

DWORD WINAPI MyThreadFunction( LPVOID lpParam ) 
{
    CMyVirtualMsrSO* pVirtualMsr = (CMyVirtualMsrSO*)lpParam;
    Sleep(500);
    if (NULL != pVirtualMsr)
    {
        pVirtualMsr->FireDataEvent();
    }
    return 0;
}

STDMETHODIMP CMyVirtualMsrSO::SetPropertyNumber(LONG PropIndex, LONG Number)
{
    switch (PropIndex)
    {
    case PIDX_DeviceEnabled:
        break;
    case PIDX_DataEventEnabled:
        CreateThread( 
                NULL,                   // default security attributes
                0,                      // use default stack size  
                MyThreadFunction,       // thread function name
                this,                   // argument to thread function 
                0,                      // use default creation flags 
                NULL);
        break;
    default:
        break;
    }
    return S_OK;
}

Rebulid the project and register the dll. Now we can test our SO easily with an MFC or a VB test application.

References
[1] UnifiedPOS 1.12 documentation (link)

31 comments:

  1. Hello!
    Would you please tell me how to install SO?

    ReplyDelete
  2. Hello!
    Would you please tell me how to install SO?

    ReplyDelete
    Replies
    1. Hi,

      Sorry for the late response. Installing the SO involves the following:

      1. adding the registry entries
      2. registering the COM dll

      If you follow the steps (9) and (10) in the post, you should be able to install the SO.

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Hi,
    Thanks for your tutorial essay.
    I tried to implement a cash drawer service object and simple application to do "CashDrawer.Open("MyCashDrawer");".
    But the return code will be 104 (OPOS_E_NOSERVICE).
    I'm no idea how to enable the debug log of CashDrawerImpl.cpp of 1.14.001 to know what's going on.
    1) How to make sure the service object is ok?
    2) How to enable debug log of OPOS_CCOs_1.14.001?
    Thanks for your help.

    ReplyDelete
    Replies
    1. To see the logs, download the CCO Debug Runtime for the version from the OPOS downloads page and install it. Also, the GetOpenResult method of the CO should give you a more descriptive error code.

      Delete
  5. Hi,
    I am trying to create a Service Object for OPOS Printer. But didn't succeed.
    I dont know C++ and am a C# programmer.
    How to create the SO for OPOS printer in C#.

    Any help will be appreciated.

    ReplyDelete
    Replies
    1. Hi Ravi,
      I don't know if what you are trying to do is possible with OPOS. There you are writing a managed dll that will be used by an ActiveX (the Control Object).

      If you are into .NET, I suggest you take a look at POS for .NET.

      Delete
  6. Hi,
    Thanks for the response.
    My task is to capture the data sent to the OPOS printer from any application and save it as a PDF format. I have tried with many tools but does not work for OPOS. When I googled I found that we can replace Printer Vendor's SO with our own SO.
    But I dont know where to start and how to achieve.
    Your article is helpful to understand how the OPOS works.

    ReplyDelete
    Replies
    1. Solution to your problem depends on several factors:

      (a) How the POS application is written : Sometimes they directly load the SO without ever using a CO. In this case, if possible, your SO will have to load the OEM SO and act like a filter.

      (b) How much you know about printer communication internals : Replacing the OEM SO with your own SO isn't a good idea unless you know the printer communication inside out. Also, with this approach you may have to rewrite the SO for a different printer vendor.

      (c) If the printer user (say a retail store) agrees to use a custom CO for PosPrinter, you can modify the CO source to add your requirements. I think this is the easiest thing to do.

      (d) If nothing above holds :
      1. rename the OEM PosPrinter regristy location to something different
      2. write an SO that calls relevant CO methods, and opens the relocated OEM printer - this wouldn't be easy :)
      3. put information related to your SO in the OEM registry location
      Then, whenever the POS application talks to the printer, it'll use your SO instead of the OEM SO.
      But you still need to talk to the OEM SO to get things done. This is where step (2) comes in. You can do it as either an in-process or an out-of-process thing.

      Delete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Hi!
    Would you please tell me how to write the SO's proprety?

    ReplyDelete
    Replies
    1. Hi,
      The question isn't very clear. Can you please explain it a bit?

      Delete
  9. Hi,

    Thanks for the Detailed explanation.

    Of the given options I have chosen option D.
    Based on your suggestion I have started writing my own SO, but I am stuck up at implementing the interface methods and property. Can you guide me how to implement the below in C#. so that I can do other implementations based on this.

    OpenService, DirectIO, CheckHealth, GetPropertyNumber, SetPropertyNumber, GetPropertyString, SetPropertyString,ClaimDevice, PrintNormal etc....

    Also how can I register my SO created in C#.
    Expecting your valuable reply.

    ReplyDelete
    Replies
    1. As I stated in my earliest reply to your question, I don't know if writing an SO in C# is possible, and I don't have much experience in .NET.
      You may have arrived at option (d) because nothing else is applicable in your case. From experience I know option (d) is not very easy, but certainly doable. But with C#, I have no idea.

      Delete
  10. Hi,
    thank you once again for your support.
    Is it possible to provide sample coding for the above methods and properties in my prev mail. in C++. so that i can understand what to give and how it works for Printer.

    ReplyDelete
  11. Hi,
    Ok.
    Anyway thanks for your guidance.

    ReplyDelete
  12. Hello,
    Great Job. I'm trying to build my own SO, adding code which accesses the scale. But my problem is that I don't know where do I have to put my owm code. Any help?

    Thanks a lot

    ReplyDelete
    Replies
    1. I think you are asking about the code that talks to the device hardware using a device I/O API such as the CreateFile, ReadFile, WriteFile, DeviceIoControl, etc. or some other API, to get things done from the device.
      In that case, you can invoke appropriate calls inside the corresponding OPOS SO methods.
      For example, in a very simple implementation, in the ClaimDevice method, you can try to open the relevant port using CreateFile and save the file handle, then may be in a DirectIO method, call an appropriate IOCTL using DeviceIoControl, finally, in ReleaseDevice method, close the file handle with CloseHandle.

      Delete
  13. Thanks so much for your quick response. I really apreciate your help.
    Basically what I need is to create a Scale SO that connects to my scale. And my main doubt is where do I have to writw my code.

    I'm implementing the "http://www.monroecs.com" OPOS zScale code.
    Using the ClaimDevice example, the DoInvoke call, calls the method itself.
    How can I call my own methods?

    STDMETHODIMP COPOSScale::ClaimDevice( long Timeout, long *pRC )
    {
    SetRC();

    // If not opened, set return code.
    if ( ! _bOpened )
    {
    *pRC = OPOS_E_CLOSED;
    DOTRACEV( ( _T("*ClaimDevice [Function] -- Closed") ) );
    return S_OK;
    }

    // Initialize so that events are allowed.
    EventClaim();

    // Call down into the Service Object to execute this method.
    OposVariant Var;
    Var.SetLONG( Timeout );
    return DoInvoke( DEBUGPARAM("ClaimDevice") S_OK, &Var, 1, nDIClaimDevice, pRC, false );
    }

    ReplyDelete
    Replies
    1. Your question is not clear. At first I thought you were writing a Service Object(SO). http://www.monroecs.com provide the Control Object(CO) sources. So, I assume you are modifying the CO, which is not covered in this blog.

      Delete
  14. Yes, I'm writing a SO.
    I think that I'm a little lost.
    I thought I was modiffying the SO, but I see that the zScale folder is about the CO, isn't it?
    If so, what is what I have to do? Where do I have to write my code for the SO?
    the code return DoInvoke( DEBUGPARAM("ClaimDevice") S_OK, &Var, 1, nDIClaimDevice, pRC, false ); is supposed to be calling my SO?

    Sorry about all thsese questions. I thought I was in the right way but I guess I'm totally lost.
    I'll appreciate much your help.

    ReplyDelete
    Replies
    1. You don't have to modify the CO sources. As you have already seen, the CO invokes the appropriate SO method. So, follow the steps in this post to create your own SO for the Scale.

      Delete
    2. Thanks very much. I think I'm in the correct way now.

      Delete
  15. Hi all,
    I finally managed to make it work. It's great. You saved my life!!

    Now I'm trying to make it better and trying to deal with exceptions. Am I in the right way thinking that I have to manage them throw resultCodes??

    ReplyDelete
    Replies
    1. Yes, you can do the error reporting with resultCodes and SOError events.

      Delete
  16. Hi, Could you please help me to handle events fired from OPOSImageScanner.ocx in non-windows application using IDispatch. I am able to open, claim and enable the device successfully.

    ReplyDelete
    Replies
    1. Long time back I did this, but now I don't remember the exact details. As I can remember, there were three ways of doing this, but deriving a class from IDispEventImpl or IDispEventSimpleImpl were the easier ways. I don't have a working code sample to share, but I think the article
      "AtlEvnt.exe sample shows how to creates ATL sinks by using the ATL IDispEventImpl and IDispEventSimpleImpl classes"(https://support.microsoft.com/en-us/help/194179/atlevnt-exe-sample-shows-how-to-creates-atl-sinks-by-using-the-atl-idi) should help.

      Delete
  17. I had created SO and registered the dll as mentioned above. Now I had created a MFC dialog test project and unable to call MyVirtualMsrso dll in MFC app.
    Can you please help me how should I call so dll in MFC app.

    Thanks in advance.

    ReplyDelete
  18. Can you give me an example GetPropertyString please?
    BSTR length is 0 in coDataEvent

    ReplyDelete