Table Of Contents
- Introduction
- What Is An iButton
- How Is It Used Here
- iButton Code Encryption
- NIC Address Encryption
- License Time Encryption
- System Requirements
- Polling
- Notification
- Contention
- The Button Interface Class
- Driver Initialization
- Session Startup And Termination
- Obtaining A List Of 1-Wire Devices
- Initiating The Worker Thread
- Pausing And Resuming The Worker Thread
- Handling Notification Messages In The Application
- Device Selection
- NVRAM Read/Write Functions
- NVRAM I/O During Device Monitoring
- The Worker Thread
- CDialog Notification Handler
- Conclusion
First off, hopefully no one is offended with the content of this article! The intention of this article is to demonstrate the use of an interesting technology in an even more interesting, and legal (in most states), industry. In many ways, this industry is no more and no less corrupt than any other form of commerce (think of Enron). Because it is so closely monitored by the government and special interest groups, it may be even more accountable than most.
In the adult entertainment industry, tracking cash transactions is a dubious process. My client has resolved this problem by, in part, using an interesting little device known as an "iButton", manufactured by Dallas Semiconductor (the other half of the equation involves custom hardware distributed throughout the club utilizing a proprietary communication protocol over Ethernet). Using this device, a computer system can track dances, money deposited into bill acceptors, authorize door access, etc. Installed in dance clubs across the country, the system has, in many cases, doubled the revenue of the club because dance income is now accurately tracked. To see a system in action near you...
As mentioned, cash transaction monitoring is complicated. There are two cash transaction models that are supported--"debit account" and "credit account". The following diagrams the difference in these two models. Depending on the club requirements, one or the other or both methods are used.
Credit Account:
Debit Account:
In both cases, this method works only when a method exists for activating the booth, window, or other device that initiates a timed interaction between the entertainer and the customer. In cases where there is direct contact between the entertainer and the customer, a booth is used with an activation light. This still requires checkers, which are people that roam around the club all the time making sure that an entertainer is not in a booth with a customer without activation. There is also extensive video surveillance in most clubs that is constantly being monitored and taped. Interestingly, VHS tape is still used instead of fancy digital recorders. The reason, as explained to me, is that the police will take only the tape for evidence, whereas if you have a digital recorder, they'll take the whole box.
Additional features of the system include being able to charge different amounts based on the entertainer (as identified by the iButton) and the activation point (booth, type of booth, shower, etc). This is all possible because the of unique ID of the entertainer's iButton.
An iButton is like a snowflake. No two are alike. A unique 8 byte (64 bit) code is written into a Read Only Memory (ROM) of each device at the time of manufacture. Besides the ROM, there are several flavors of iButtons, many of which include non-volatile random-access-memory (NVRAM), timer functions, and real-time clocks. Interesting devices include a decoder ring, Java interpreter, and temperature sensor. The devices are addressed using a "1-wire" network, which is a network protocol designed by Dallas Semiconductor to support iButton interfacing.
For more information on iButtons and to download the iButton SDK, go here: www.ibutton.com
In this article, I will illustrate:
- reading the ROM code
- reading and writing NVRAM pages
- 1-wire polling via a worker thread
- custom dialog notifications
- iButton debouncing
This code is applicable for all iButtons supporting NVRAM, such as the DS1994.
In the software utilized by my client, an iButton with NVRAM is used as a copy protection mechanism and license renewal service. The encryption algorithm used is based on the public domain "Blowfish" algorithm. There is a CP article on the subject here. The code that validates the encryption is itself encrypted, and because it is written in a proprietary script language (ooh, opportunity to plug the AAL), it is difficult to determine the exact locations where the validation tests are made, since this is actually generic code used throughout the application. (Go ahead Bill, give it a try).
First, an encrypted version of the iButton code itself is written into NVRAM. This protects the system from someone with moderate ability from copying the authorization from one iButton to another. Because iButtons are unique, the decrypted iButton code of one button will not be equal to the code of the other button.
Secondly, the computer's NIC address (not the IP address) is encrypted and stored in the iButton. This prevents the software from operating on a computer for which it is not registered to operate. Besides preventing illegal copies, it also ensures that there is some control over the computer system that is operating the software. In almost all cases, a turnkey system is sold to the customer.
The software is typically licensed to a club for a certain number of hours of operation. This ensures a continuing source of revenue, and in some cases where the customer doesn't pay on time, it ensures that the system is shut down after a certain amount of time. Yes, in this industry people like to hold on to their cash, and methods like this need to be used to ensure that people pay for their systems. The licensed hours of operation is encrypted and stored in the iButton. During software operation, this value is decremented frequently. This approach was used instead of the iButton's timer features to ensure that time is decremented only while the software is actually in operation.
In addition to identifying transactions taking place in the club, the iButton is used to quickly select the entertainer from list boxes and combo boxes during the cash out and income statement reports (among other places). To support this capability, several challenges exist, some of which have nothing to do with iButtons.
The 1-wire network is a polled network. It does not alert the system when devices are added or removed from the network. Therefore, a thread is utilized to monitor the devices on the network. There are two complications that exist. One is that the 1-wire drivers consume a considerable amount of CPU resources. Polling a network with two devices every 100ms on a 1.6Ghz P4 results in a CPU utilization approaching 50%. This is unacceptable for continuous operation, and therefore requires a mechanism to activate the polling only when required.
Secondly, because a person actually connects the iButton to the network using a simple "reader" (seeing picture), the iButton can appear and disappear several times within a short (500ms or less) period of time. One of the artifacts of this is that the ordering of the devices on the network can change. This caused problems in the initial implementation of this system because I was expecting the licensing iButton (which is physically mounted in a holder that attaches to the parallel port--the holder has it's own unique address, by the way) to always appear first in the device list. Alas, this is not necessarily the case. To compensate for this problem, a simple debounce system is implemented. This mechanism provides an immediate notification when an iButton is added to the network, but holds off on the "iButton removed" from network notification for 500ms. This prevents multiple "new button" notifications from being issued to the user interface.
The next problem is how to get notification of an event to a dialog that contains the list box or combo box. In the example provided here, a custom message notification scheme is used and the PreTranslateMessage
method of CDialog
must be overridden. In the actual system in use, a message is placed in an internal queue which is processed during the application's OnIdle()
function. Because this function is not active when a modal dialog is present, the dialog implements a timer which issues a message and activates the same OnIdle
processing. For those interested, the reason for this implementation is that is also serves other purposes, such as enabling processing of other messages coming in from the club's network. Because Access is used as the database, all database transactions must occur in the application thread. There. That's the technical reason.
Contention between the polling routine worker thread and any application thread I/O is resolved using semaphores.
This class is a framework for adding additional functionality that the iButton family supports. It currently reads the ROM code, reads and writes NVRAM pages, and provides a polling mechanism for notification of device changes on the network. The class definition is:
class ButtonInterface
{
private:
static UINT ButtonInterface::MonitorThreadStartup(void* v);
public:
ButtonInterface(void);
virtual ~ButtonInterface(void);
bool StartSession(void);
void EndSession(void);
void StartMonitor(int msSleep=100, int debounceTime=500);
void EndMonitor(void) {endMonitor=true;};
void PauseMonitor(void) {pauseMonitor=true;};
void ResumeMonitor(void) {pauseMonitor=false;};
void BeginIO(void);
void EndIO(void);
bool SelectDevice(CString romCode);
bool SelectDeviceStrong(CString romCode);
bool ReadPage(int page, unsigned char* data, int len);
bool WritePage(int page, unsigned char* data, int len);
bool IsMonitorActive(void) {return monitorActive;};
UINT GetNotificationAdded(void) {return notificationAdded;};
UINT GetNotificationRemoved(void) {return notificationRemoved;};
int GetButtonList(CString* codes);
protected:
void Monitor(void);
void PostNotification(UINT msg, CString code);
struct ButtonInfo
{
ButtonInfo(void)
{
touched=false;
newButton=true;
}
LARGE_INTEGER currentSampleTime;
bool touched;
bool newButton;
};
protected:
std::map<CString, ButtonInfo> buttonInfo;
std::map<CString, CString*> msgData;
HINSTANCE hInst;
long hSess;
bool endMonitor;
bool pauseMonitor;
bool monitorActive;
int sleepTime;
int debounceTime;
CRITICAL_SECTION cs;
BYTE stateBuffer[15360];
UINT notificationAdded;
UINT notificationRemoved;
LARGE_INTEGER freq;
private:
short (far pascal* TMReadDefaultPort)
(short far* portNum, short far* portType);
long (far pascal* TMExtendedStartSession)
(short portNum, short portType, void far* enhSessOpt);
short (far pascal* TMSetup)(long hSess);
short (far pascal* TMEndSession)(long hSess);
short (far pascal* TMFirst)(long hSess, void far* stateBuff);
short (far pascal* TMNext)(long hSess, void far* stateBuff);
short (far pascal* TMRom)
(long hSess, void far* stateBuff, short far* ROM);
short (far pascal* TMAccess)
(long hSess, void far* stateBuff);
short (far pascal* TMStrongAccess)
(long hSess, void far* stateBuff);
short (far pascal* TMWritePacket)
(long hSess, void far* stateBuff, short page,
unsigned char far* data, short len);
short (far pascal* TMReadPacket)
(long hSess, void far* stateBuff, short page,
unsigned char far* data, short len);
short (far pascal* TMTouchByte)(long hSess, short byte);
short (far pascal* TMBlockStream)
(long hSess, unsigned char far* data, short len);
};
The driver can be initialized by including a LIB in the application, or by loading a DLL at runtime and determining the interface points programmatically. I have chosen the latter method, as it accommodates updates in the driver without having to recompile because of the dependent LIB. This simplifies updating the clubs with new iButton driver software--in many cases, they can do it themselves. The following code illustrates loading the DLL and acquiring the entry points to the functions required in this implementation of the ButtonInterface
object.
ButtonInterface::ButtonInterface(void) :
hSess(0), endMonitor(false), pauseMonitor(false), monitorActive(false)
{
InitializeCriticalSection(&cs);
notificationAdded=RegisterWindowMessage(
"iButtonNotification_ButtonAdded");
notificationRemoved=RegisterWindowMessage(
"iButtonNotification_ButtonRemoved");
QueryPerformanceFrequency(&freq);
hInst = LoadLibrary("IBFS32.DLL");
if (hInst != NULL)
{
TMExtendedStartSession=(long (far pascal *)
(short,short,void far *))
GetProcAddress(hInst, "TMExtendedStartSession");
TMReadDefaultPort=(short (far pascal *)
(short far*, short far*))
GetProcAddress(hInst, "TMReadDefaultPort");
TMSetup=(short (far pascal *)(long))
GetProcAddress(hInst, "TMSetup");
TMEndSession=(short (far pascal *)(long))
GetProcAddress(hInst, "TMEndSession");
TMFirst=(short (far pascal *)(long, void far*))
GetProcAddress(hInst, "TMFirst");
TMNext=(short (far pascal *)(long, void far*))
GetProcAddress(hInst, "TMNext");
TMRom=(short (far pascal *)(long, void far*, short far*))
GetProcAddress(hInst, "TMRom");
TMAccess=(short (far pascal *)(long, void far*))
GetProcAddress(hInst, "TMAccess");
TMStrongAccess=(short (far pascal *)(long, void far*))
GetProcAddress(hInst, "TMStrongAccess");
TMTouchByte=(short (far pascal *)(long, short))
GetProcAddress(hInst, "TMTouchByte");
TMBlockStream=(short (far pascal *)
(long, unsigned char far*, short))
GetProcAddress(hInst, "TMBlockStream");
TMWritePacket=(short (far pascal *)
(long, void far*, short, unsigned char far*, short))
GetProcAddress(hInst, "TMWritePacket");
TMReadPacket=(short (far pascal *)
(long, void far*, short, unsigned char far*, short))
GetProcAddress(hInst, "TMReadPacket");
}
}
ButtonInterface::~ButtonInterface(void)
{
endMonitor=true;
while (monitorActive) {};
FreeLibrary(hInst);
DeleteCriticalSection(&cs);
}
All interfacing to the 1-wire network must be performed within a session. The session mechanism locks out other applications from accessing the 1-wire network while the session is active. Dallas Semiconductor recommends that you keep the session open for as short a period as possible so that other applications are not locked out from accessing the network. Because this is a proprietary turnkey system, this is not an issue and the session is maintained for the lifetime of the ButtonInterface
object. The following code illustrates initiating and terminating a session, which also automatically terminates the worker thread.
bool ButtonInterface::StartSession(void)
{
if (hInst != NULL)
{
short portNum;
short portType;
TMReadDefaultPort(&portNum, &portType);
hSess=TMExtendedStartSession(portNum, portType, NULL);
if (hSess != 0)
{
TMSetup(hSess);
}
}
return hSess != 0;
}
void ButtonInterface::EndSession(void)
{
if (hSess > 0)
{
endMonitor=true;
while (monitorActive) {};
TMEndSession(hSess);
hSess=NULL;
}
}
A list of all 1-wire devices can be obtained in the application thread at any time by calling the GetButtonList
method. Note that this method does not affect the flags maintained internally by the ButtonInterface
object for determining additions and removals of 1-wire devices. The following code illustrates how the devices on the 1-wire are acquired. This object currently handles a maximum of 32 devices. The ROM codes for each device is returned in an array of CStrings, as opposed to hex values. Note that the ROM code is read out in reverse order from the way it is displayed on the iButton. This corrects the fact that the ROM code is returned from the driver in reverse order.
int ButtonInterface::GetButtonList(CString* codes)
{
short ret=TMFirst(hSess, stateBuffer);
int n=0;
while ( (ret==1) && (n<32) )
{
short ROM[8]={0, 0, 0, 0, 0, 0, 0, 0};
TMRom(hSess, stateBuffer, ROM);
char s[3]="\0\0";
CString romCode="";
for (int i=7; i>=0; i--)
{
sprintf(s, "%02X", ROM[i]);
romCode+=s;
}
codes[n]=romCode;
++n;
ret=TMNext(hSess, stateBuffer);
}
return n;
}
This code acquires the devices on the network through a straight forward "First" and "Next" iteration. Each byte is converted into an ASCII representation of the hexidecimal value of the byte.
The polling worker thread is initiated with a default 100ms polling interval and a 500ms debounce circuit. Termination sets a flag that causes the thread to exit the next time polling is initiated. Because the fairly inaccurate Sleep
function is used, these timing values are approximate only.
...
void StartMonitor(int msSleep=100, int debounceTime=500);
void EndMonitor(void) {endMonitor=true;};
...
void ButtonInterface::StartMonitor(int msSleep, int debounce)
{
if (hSess > 0)
{
sleepTime=msSleep;
debounceTime=debounce;
AfxBeginThread(MonitorThreadStartup, this);
}
}
UINT ButtonInterface::MonitorThreadStartup(void* v)
{
ButtonInterface* btn=(ButtonInterface*)v;
btn->Monitor();
return 0;
}
The worker thread utilizes the same method as the application thread network identification. In addition, it sets and clears various flags to identify new devices, devices that have been removed from the network, and handles device debounce.
When paused, the worker thread wakes up at the polling interval and checks only if the thread is to be terminated. It does not perform polling when paused. This is useful for when you simply want to stop polling for performance or other reasons, without going through the process of destroying and recreating the object which will result in reloading the DLL.
...
void PauseMonitor(void) {pauseMonitor=true;};
void ResumeMonitor(void) {pauseMonitor=false;};
...
The ButtonInterface
object establishes its own unique "button added" and "button removed" notifications. These can be determined using two methods defined in the class header file:
...
UINT GetNotificationAdded(void) {return notificationAdded;};
UINT GetNotificationRemoved(void) {return notificationRemoved;};
...
Before reading and writing to NVRAM, the specific device must be selected on the 1-wire. To ensure correct selection, the TMAccessStrong
driver function must be called with the ROM code of the desired device. If the TMAccess
is used, you will not be guaranteed that the desired device is actually on the network (this has been impirically verified). The ButtonInterface
class provides two methods for selecting a device. The "weaker" SelectDevice
method can be used once the device has been verified to exist on the network using the SelectDeviceStrong
method. For further information, read the TMAccess
and TMAccessStrong
documentation in the iButton SDK.
bool ButtonInterface::SelectDevice(CString romCode)
{
short ROM[9];
for (int i=7; i>=0; i--)
{
CString hex=romCode.Mid(i*2, 2);
sscanf(hex, "%02X", &ROM[7-i]);
}
TMRom(hSess, stateBuffer, ROM);
int n=TMAccess(hSess, stateBuffer);
return n==1;
}
bool ButtonInterface::SelectDeviceStrong(CString romCode)
{
short ROM[9];
for (int i=7; i>=0; i--)
{
CString hex=romCode.Mid(i*2, 2);
sscanf(hex, "%02X", &ROM[7-i]);
}
TMRom(hSess, stateBuffer, ROM);
int n=TMStrongAccess(hSess, stateBuffer);
return n==1;
}
The NVRAM page read/write functions are closely tied to the hardware implementation, reading and writing a maximum of 32 bytes per page. This implementation does not support intelligent cross-page boundary detection. While the drivers provide "TMReadPacket" and "TMWritePacket" functions, these functions only support NVRAM devices such as the DS1993. Of course, this information isn't mentioned in the "TMReadPacket" function, only the "TMWritePacket" function, and caused me to spend about 4 hours trying to figure out why I couldn't read or write to a DS1994 device.
The ReadPage
method issues a command over the 1-wire network requesting the specified page and then reads out the desired number of bytes, up to the page size, which is 32 bytes.
bool ButtonInterface::ReadPage(int page, unsigned char* data, int len)
{
if (len > 32) return false;
TMAccess(hSess, stateBuffer);
TMTouchByte(hSess, 0xF0);
TMTouchByte(hSess, (short)((page*32)&0xFF));
TMTouchByte(hSess, (short)((page*32)>>8));
Similarly, the WritePage sends up to one page size (32 bytes) to the specified page:
bool ButtonInterface::WritePage(int page, unsigned char* data, int len)
{
if (len > 32) return false;
TMAccess(hSess, stateBuffer);
TMTouchByte(hSess, 0x0F);
TMTouchByte(hSess, (short)((page*32)&0xFF));
TMTouchByte(hSess, (short)((page*32)>>8));
TMBlockStream(hSess, data, (short)len);
TMAccess(hSess, stateBuffer);
TMTouchByte(hSess, 0xAA);
unsigned char auth[3];
for (int i=0; i<3; i++)
{
auth[i]=(unsigned char)TMTouchByte(hSess, 0xFF);
}
TMAccess(hSess, stateBuffer);
TMTouchByte(hSess, 0x55);
for (int i=0; i<3; i++)
{
TMTouchByte(hSess, auth[i]);
}
return true;
}
Note that the 1-wire network always returns a byte when a byte is placed onto the network. In the case of reading a byte, an 0xFF is placed onto the network. In the case of writing a byte, a non-0xFF byte is placed on the network. Therefore, true binary cannot be stored in NVRAM. Also note that in the write process, the target beginning and ending address is read off of the network after data transfer and must be written back to the network in order to actually commit the data to memory. This can be used to ensure data integrity.
In order to avoid conflicts with the device monitoring thread, the application must initiate and terminate an I/O session using the following methods:
void ButtonInterface::BeginIO(void)
{
EnterCriticalSection(&cs);
}
void ButtonInterface::EndIO(void)
{
LeaveCriticalSection(&cs);
}
This is not necessary when the monitoring thread is not used.
This thread is fairly self explanitory. The debounce feature is the most interesting, and involves timing how long a device has remained "untouched" in the list. Once the device has been "untouched" for the specified time, a notification message is posted to the application thread and the device is removed from the list. This also terminates testing for removed devices, therefore if more than one device is simultaneously removed the notifications will be delayed by the polling rate.
void ButtonInterface::Monitor(void)
{
CString* codes=new CString[32];
monitorActive=true;
while (!endMonitor)
{
Sleep(sleepTime);
if (pauseMonitor)
{
continue;
}
EnterCriticalSection(&cs);
std::map<CString, ButtonInfo>::iterator iter=buttonInfo.begin();
while (iter != buttonInfo.end())
{
(*iter).second.touched=false;
++iter;
}
int numButtons=GetButtonList(codes);
for (int i=0; i<numButtons; i++)
{
CString romCode=codes[i];
iter=buttonInfo.find(romCode);
if (iter != buttonInfo.end())
{
(*iter).second.touched=true;
QueryPerformanceCounter(&(*iter).second.currentSampleTime);
}
else
{
buttonInfo[romCode]=ButtonInfo();
}
}
LARGE_INTEGER sampleTime;
QueryPerformanceCounter(&sampleTime);
iter=buttonInfo.begin();
while (iter != buttonInfo.end())
{
CString code=(*iter).first;
ButtonInfo& bi=(*iter).second;
if (bi.newButton)
{
PostNotification(notificationAdded, (*iter).first);
bi.newButton=false;
}
else
if (bi.touched==false)
{
if (sampleTime.QuadPart-bi.currentSampleTime.QuadPart >
freq.QuadPart*debounceTime/1000)
{
PostNotification(notificationRemoved, (*iter).first);
buttonInfo.erase(iter);
break;
}
}
++iter;
}
LeaveCriticalSection(&cs);
}
endMonitor=false;
delete[] codes;
monitorActive=false;
}
The following code illustrates how the monitor thread notifications are handled in a CDialog
derived class. In this example, the button code is either added to a list control list or removed. Note that the ROM code CString*
is deallocated by the message handler.
BOOL CButtonTestDlg::PreTranslateMessage(MSG* msg)
{
BOOL ret;
if (msg->message==btn->GetNotificationAdded())
{
CString* code=(CString*)msg->lParam;
buttonList.ButtonPresent(code, true);
delete code;
ret=true;
}
else
if (msg->message==btn->GetNotificationRemoved())
{
CString* code=(CString*)msg->lParam;
buttonList.ButtonPresent(code, false);
delete code;
ret=true;
}
else
{
ret=CDialog::PreTranslateMessage(msg);
}
return ret;
}
In conclusion, the iButton is an excellent solution for tracking monetary transactions. In the particular case of my client's system, all funds are maintained in the server that monitors club transactions. In other applications, the funds can be maintained directly in iButton NVRAM, providing autonomous support for authorization requests and funds crediting/debiting. There are numerous other interesting applications as well!