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.
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 |
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);
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.
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:
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
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);
}
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)
Hello!
ReplyDeleteWould you please tell me how to install SO?
Hello!
ReplyDeleteWould you please tell me how to install SO?
Hi,
DeleteSorry 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.
This comment has been removed by the author.
ReplyDeleteHi,
ReplyDeleteThanks 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.
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.
DeleteHi,
ReplyDeleteI 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.
Hi Ravi,
DeleteI 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.
Hi,
ReplyDeleteThanks 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.
Solution to your problem depends on several factors:
Delete(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.
This comment has been removed by the author.
ReplyDeleteHi!
ReplyDeleteWould you please tell me how to write the SO's proprety?
Hi,
DeleteThe question isn't very clear. Can you please explain it a bit?
Hi,
ReplyDeleteThanks 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.
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.
DeleteYou 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.
Hi,
ReplyDeletethank 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.
I'm sorry, that's not possible.
DeleteHi,
ReplyDeleteOk.
Anyway thanks for your guidance.
Hello,
ReplyDeleteGreat 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
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.
DeleteIn 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.
Thanks so much for your quick response. I really apreciate your help.
ReplyDeleteBasically 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 );
}
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.
DeleteYes, I'm writing a SO.
ReplyDeleteI 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.
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.
DeleteThanks very much. I think I'm in the correct way now.
DeleteHi all,
ReplyDeleteI 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??
Yes, you can do the error reporting with resultCodes and SOError events.
DeleteHi, 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.
ReplyDeleteLong 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
Delete"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.
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.
ReplyDeleteCan you please help me how should I call so dll in MFC app.
Thanks in advance.
Can you give me an example GetPropertyString please?
ReplyDeleteBSTR length is 0 in coDataEvent