Source file: /~heha/hs/UNI-T/dmm.zip/src/dmm.cpp

#define _WIN32_WINNT 0x0500
#if _MSC_VER > 1400
#define _INC_SWPRINTF_INL_
extern "C" int _declspec(dllimport) _cdecl swprintf(wchar_t*,const wchar_t*,...);
#endif
#include <windows.h>
#include <windowsx.h>
#include <shlwapi.h>
#include <commctrl.h>
#include <setupapi.h>
#include <devguid.h>
#include "he2325u.h"
#include <stdio.h>
#include <math.h>
#include <tchar.h>
#include <limits.h>
#include <float.h>
#include "dmm.h"

enum EVIA {VIACOM=1,VIAUSB};

HINSTANCE ghInst;
HWND ghMainWnd;
HICON ghIcons[2];
TCHAR ConfName[CONFMAX];	// e.g. "UT61B@USB1", not case sensitive
TCHAR HelpName[MAX_PATH];

static const COLORREF TraceColor[4]={0x008000,0x800000,0x800080,0x808000};

struct GDI{		// holds GDI resources needed for painting the client area
 HFONT fntBig,fntEx,fntUniT;	// four fonts
 HFONT fntRo;			// for extra (2,3,4) readout
 HPEN penGrid, penGraph[4];	// pens used in scope mode and for backtrace
 HPEN penWide[4];		// for the right-side vertical äbargraphô
 HBRUSH brFill[4];		// for backtrace graph (fillig to zero)
 HBRUSH brBar[4];		// for bargraph (less white)
 HPEN penDiv;			// visible field divider
 HPEN penTrig;
 HPEN penNull;			// for border-less polygon
 HBITMAP bmDblBuf;		// bitmap and DC, only used for double buffering
 HFONT fntAxes[2];		// two fonts for axes (backtrace)
 HDC dcDblBuf;
 void Make(int,int);		// (Re)Creates fonts and doublebuffer bitmap for given client pixel size
 void Make();			// Same but fetches the client size beforehand
 void Delete(bool FontToo=true);// Releases all GDI resources, for minimized display
 enum EUNITFONT{NA,INSTALLED,LOADED} unitfont;	// availability of Uni-Tech's symbol and 7segment font
private:
 void UnitfontLoad();
 void UnitfontUnload();
 static int CALLBACK EnumFontFamProc(const LOGFONT*,const TEXTMETRIC*,DWORD,LPARAM);
 static HFONT MakeFont(int,int,int,LPCTSTR);
 static void DeleteObj(HGDIOBJ&);
}gGdi;

TCHAR StdMBoxTitle[64];
// Assumption: The decimal character is never an MBCS character! Is this true?
WCHAR cDecimalW;	// The decimal delimiter
char cDecimalA;		// ANSI version
DWORD gDdeInst;		// DDE instance
HSZ hszDmm;		// String handle for "DMM"
HSZ hszTopic;		// String handle for e.g. "UT71B@COM2"
UINT CF_XlTable;	// Clipboard Format
EXTERN_C int _fltused;	// MSVC6 needs this
int _fltused;
#ifdef UNICODE
#define cDecimal cDecimalW
#else
#define cDecimal cDecimalA
#endif

#define MAXDEPTH 400	// History length of backtrace, in samples

/* This program internally uses multiple character sets:
 * ANSI - the current ANSI code page, for CP1252, cannot display Ohm but ░
 * UNICODE - more exactly: UTF-16 BMP0, all characters except symbols like Diode, Beeper etc.
 * UTF-8, same characters as UNICODE
 * ASCII, only 7-bit characters
 * OEM, the DOS code page, maybe
 * UNI-T, according to the seven-segment display true type font, all displayable characters and symbols
 */

#ifdef UNICODE
#define MyLoadStringW(id,buf,len) LoadString(ghInst,id,buf,len)
#else
int MyLoadStringW(UINT id, LPWSTR buf, int len) {
 HRSRC r=FindResource(ghInst,MAKEINTRESOURCE((id>>4)+1),RT_STRING);
 if (!r) return 0;
 PCWSTR p=(PCWSTR)LoadResource(ghInst,r);
 for (id&=15; id; id--) {
  p+=*p+1;
 }
 len--;
 if (len<0) return *p+1;	// size, in characters, needed for allocation
 if (len>*p) len=*p;
 p++;
 memcpy(buf,p,len*2);
 buf[len]=0;
 return len;
}
#endif

const __int64 _sNaN = 0x7FF0000000000001;	// signaling NaN with payload 1 (not used)
const __int64 _qNaN = 0xFFFFFFFFFFFFFFFF;	// quiet NaN (used for missing samples and values)
const __int64 _pInf = 0x7FF0000000000000;	// positive infinity (used for positive overflow)
const __int64 _nInf = 0xFFF0000000000000;	// negative infinity (used for negative overflow)

#ifdef _M_IX86
EXTERN_C void _cdecl _ftol2_sse() {
 void _cdecl _ftol();	// avoid parameter stacking with void
 _ftol();	// Anyone an idea how to say the compiler to emit _ftol instead of _ftol2_sse?
}		// (Same applies to __p__iob and __iob_func, see "sumatrapdf" source)
#endif

struct RECTS{
 short left,top,right,bottom;
 inline void operator=(const RECT&);	// set
 inline void operator>>(RECT&) const;	// get
};

void RECTS::operator=(const RECT&r) {
 left=short(r.left);
 top=short(r.top);
 right=short(r.right);
 bottom=short(r.bottom);
}

void RECTS::operator>>(RECT&r) const {
 r.left=left;
 r.top=top;
 r.right=right;
 r.bottom=bottom;
}

// The data structure saved into registry (fixed part)
struct TConfig{
 RECTS winpos;		// "restored" window position
 char showCmd;		// SW_SHOWxxx
 union{
  BYTE checkBits;	// checkbox group bits (currently 5)
  struct{
   BYTE doubleBuf:1;	// double buffering for drawing operation
   BYTE flashIcon:1;	// flashing icon
   BYTE sevenSeg:1;	// sevensegment display
   BYTE glassFx:1;	// Vista Glass effect (TODO)
   BYTE ddePoint:1;	// Use decimal point instead of Windows setting
   BYTE surviveReboot:1;// Auto-resume on reboot
  };
 };
 WORD serialConfig;	// some future flags for serial interface
 DWORD baudRate;	// baudrate or other settings used for interface, if not fixed by DMM construction
}Config;

int _cdecl MBox(HWND Wnd, UINT id, UINT Type, ...) {
 TCHAR buf[256],fmt[256];
 LoadString(ghInst,id,fmt,elemof(fmt));
 _sntprintf(buf,elemof(buf),fmt,(va_list)(&Type+1));
 return MessageBox(Wnd,buf,StdMBoxTitle,Type);
}

/********************************************************************************
 * Loading plugin DLLs and enumerating all available multimeters in linked list *
 ********************************************************************************/

ENUMINFO *el;

// Enumerate one DLL entry point - or one built-in list
static bool EnumOne(bool(_stdcall*p)(int,ENUMINFO*)) {
 bool ret=false;
 for (int i=0;;i++) {
  ENUMINFO*e=new ENUMINFO;	// allocate one element
  if (p(i,e)) {
   ret=true;			// at least one index is valid
   e->next=el;
   el=e;
  }else{
   delete e;
   break;
  }
 }
 return ret;
}

// Enumerate all built-in lists (currently one) and all available "*.dmm" plugin DLLs
static void EnumAll() {
 EnumOne(EnumUT);
 TCHAR sname[MAX_PATH];
 GetModuleFileName(0,sname,elemof(sname));
 lstrcpy(PathFindFileName(sname),T("*.dmm"));
 WIN32_FIND_DATA fd;
 HANDLE fh;
 fh=FindFirstFile(sname,&fd);
 if (fh!=INVALID_HANDLE_VALUE) {
  do{
   HINSTANCE hLib=LoadLibrary(fd.cFileName);
   if (hLib) {
    FARPROC p=GetProcAddress(hLib,"EnumAll");
    if (p && EnumOne((bool(_stdcall*)(int,ENUMINFO*))p));
    else FreeLibrary(hLib);
   }
  }while (FindNextFile(fh,&fd));
  FindClose(fh);
 }
}

int DoubleToString(double v, int nk, LPTSTR buf, UINT buflen) {
 if (!_finite(v)) return _sntprintf(buf,buflen,T("NaN"));
 int i=_sntprintf(buf,buflen,T("%.*f"),nk,v);
 LPTSTR p=_tcschr(buf,'.');
 if (p) *p=cDecimal;
 return i;
}

struct FEATURE{
 enum EFLAGS{NONE=0,PREPENDSPACE=1};
 DWORD f;		// When no readout is available, this (resource) string is displayed
 LPWSTR GenStringW(LPWSTR,EFLAGS f=NONE) const;
 bool Set(DWORD);
 int Out(HDC dc, bool measure=false) const;
 inline int Measure(HDC dc) const {return Out(dc,true);};
private:
 static LPWSTR _addstringW(LPWSTR,DWORD,EFLAGS);
}Features;		// A bitfield for various features reported by connected multimeter

struct BT{		// One backtrace item, a time+value item
 DWORD tick;		// time value, in ms
 float data;		// data value, in <unit>
};

int NumReadouts;	// The number of readouts that the multimeter reports (mostly 1, up to 4)
struct RO:READOUT{	// expand structure by routines to interpret the data in various ways
// Backtrace members
 BT*bt;			// back-trace data, as time+value pairs
 int btLen,btIdx;	// length and pointer-to back-trace data
 RECT btR;		// area for back-trace
// struct RECTF{
//  float left,top,right,bottom;
// }btFull,btZoom;
 void BtOut(HDC dc,int left, int top, int right, int bottom);	// show back-trace data
 inline void BtClear() {if (bt) LocalFree(bt); bt=NULL; btLen=btIdx=0;};
 void BtAdd(float v);
 bool BtHandleMouse(UINT,UINT,int,int);
// number display members
 enum EFLAGS{NONE=0,UNITSPACE=1,UNITPREFIX=2,WITHUNIT=4,LEADSPACE=8,CONVDECIMAL=16,UNITFONT=32,
   NORMAL=16+4+2+1,SEVENSEG=1+2+32};
 LPWSTR GenUnitW  (LPWSTR,EFLAGS f=NONE) const;
 LPSTR  GenUnitA  (LPSTR, EFLAGS f=NONE) const;
 LPWSTR GenStringW(LPWSTR,EFLAGS f=NORMAL) const;
 LPSTR  GenStringA(LPSTR, EFLAGS f=NORMAL) const;
#ifdef UNICODE
 inline LPTSTR GenUnit  (LPTSTR d,EFLAGS f=NONE)   const {	return GenUnitW(d,f);};
 inline LPTSTR GenString(LPTSTR d,EFLAGS f=NORMAL) const {	return GenStringW(d,f);};
#else
 inline LPTSTR GenUnit  (LPTSTR d,EFLAGS f=NONE)   const {	return GenUnitA(d,f);};
 inline LPTSTR GenString(LPTSTR d,EFLAGS f=NORMAL) const {	return GenStringA(d,f);};
#endif
 inline LPTSTR GenIconTitle(LPTSTR d) const {			return GenString(d);};
 DWORD Set(const READOUT*);
 inline DWORD Clear();
 int Out7Seg(HDC,int x=0,int y=0,bool measure=false) const;
 inline int Measure7Seg(HDC dc) const {				return Out7Seg(dc,0,0,true);};
private:
 char _unitprefix(EFLAGS f) const;
 char _unitcode(EFLAGS f) const;
}Readout[4];

SCOPEDATA Scope;
struct TD:TRACEDATA{
 static int datalen(const SCOPEDATA&sd=Scope);
 int operator[](int) const;
 void Set(const TRACEDATA&, int len);
 void ReAlloc(int oldlen, int newlen);
// LPCWSTR GetUnitW() const;
 static void OutGrid(HDC, RECT*);
 void OutTrig(HDC, const RECT*) const;		// draw trigger cross
 void OutTrace(HDC, const RECT*, HPEN) const;
 void OutZMark(HDC, const RECT*, HPEN) const;	// draw triangle left outside grid
 static void OutBase(HDC, char, char, char, COLORREF);
 void OutBase(HDC dc,COLORREF c) const{	OutBase(dc,devicode,unitcode,autorange,c);};
private:
 void CalcPoint(const RECT*, POINT*, int,int) const;
 void CalcPointList(const RECT*, POINT*) const;
}Trace[4];

// get length of trace data in bytes
int TD::datalen(const SCOPEDATA&sd) {
 return (sd.datasize&0x0F)*sd.tracelen;
}

int TD::operator[](int i) const {
 union{
  const void*v;
  const char*c;
  const short*s;
//const int*i;
  const BYTE*b;
  const WORD*w;
//const DWORD*d;
 }p;
 p.v=data;
 switch (Scope.datasize) {
  case 1: return p.c[i];
  case 2: return p.s[i];
//case 4: return p.i[i];
  case 0x81: return p.b[i]-0x80;
  case 0x82: return p.w[i]-0x8000;
//case 0x84: return p.d[i]-0x80000000L;
 }
 return 0;
}

// allocate or free space for copy of raw trace data
void TD::ReAlloc(int oldlen, int newlen) {
 if (!data) oldlen=0;
 if (oldlen==newlen) return;	// do nothing
 if (data) LocalFree((void*)data); data=NULL;
 if (newlen) data=LocalAlloc(LMEM_FIXED,newlen);
}

// copies data, buffer must be previously allocated
void TD::Set(const TRACEDATA&td, int len) {
 *(__int64*)this=*(__int64*)&td;	// copy 8 bytes header info
 RtlCopyMemory((void*)data,td.data,len);
}

DWORD SetScope(const SCOPEDATA*sd=NULL) {
 DWORD ret=0;
 int oldlen=TD::datalen();
 int newlen=sd ? TD::datalen(*sd) : 0;
 int newtraces=sd ? sd->n : 0;
 int i;
 for (i=0; i<elemof(Trace); i++) {
  if (i==Scope.n) oldlen=0;
  if (sd && i==sd->n) newlen=0;
  Trace[i].ReAlloc(oldlen,newlen);	// make or free space
 }
 if (sd) {
  if (Scope.n!=sd->n) ret|=1<<19;	// change GDI objects too
  Scope=*sd;				// copy SCOPEDATA structure
  const TRACEDATA *td=(const TRACEDATA*)(sd+1);	// TRACEDATA array follows
  newlen=TD::datalen(*sd);
  for (i=0; i<sd->n; i++) {		// copy traces (must be 1,2,3 or 4 on call of SetScope!)
   Trace[i].Set(*td++,newlen);
  }
  ret|=1<<18;				// Assume that at least something has changed
 }else{
  if (Scope.n) {
   Scope.n=0;				// no traces = no scope
   ret|=3<<18;				// change GDI objects and scope display
  }
 }
 return ret;
}

void TD::CalcPoint(const RECT*r, POINT*p, int x, int y) const {
 int xx=r->right-r->left;
 int yy=r->bottom-r->top;
 int ym=(r->top+r->bottom)>>1;
 p->x=r->left+MulDiv(xx,x,Scope.tracelen-1);
 p->y=ym-MulDiv(yy,y+ofs,Scope.fullscale);
}

void TD::CalcPointList(const RECT*r, POINT*pl) const {
 for (int i=0; i<Scope.tracelen; i++) {
  CalcPoint(r,pl,i,(*this)[i]);
  pl++;
 }
}

void SetWindowTitle(void) {
 if (IsIconic(ghMainWnd) && *Readout[0].digits) {
  TCHAR buf[32];
  Readout[0].GenIconTitle(buf);
  SetWindowText(ghMainWnd,buf);
 }else SetWindowText(ghMainWnd,ConfName);
}

// calculates base * 10^expo
double e10(char neg, int base, char expo) {
 if (neg) base=-base;
 return base*pow(10.0L,expo);
}

char comma() {
 return Config.ddePoint?'.':cDecimalA;
}

void ptc(char*s) {
 if (Config.ddePoint) return;
 s=strchr(s,'.');
 if (s) *s=cDecimalA;
}


// Check whether this DMM name is the current DMM (e.g. "UT60A")
bool IsCurrentDmm(LPTSTR s) {
 LPTSTR p=_tcschr(ConfName,'@');
 if (p) return StrCmpNI(s,ConfName,(int)(p-ConfName))==0;
 return lstrcmpi(s,ConfName)==0;
}

// Check whether this port is the current port (e.g. "USB1")
bool IsCurrentPort(LPTSTR s) {
 LPTSTR p=_tcschr(ConfName,'@');
 if (p) return lstrcmpi(s,p+1)==0;
 return false;			// no port
}

// Check whether a string matches a DDE item
static int GetItemIdx(PCTSTR s) {
 static const TCHAR Names[]=T("value\0unit\0trace\0min\0max\0\features\0");
 int l=lstrlen(s);
 if (!l) return 0;
 PCTSTR p=Names;
 for (int i=1; *p; i++, p+=lstrlen(p)+1) {
  if (!StrCmpNI(p,s,l)) return i;
 }
 return -1;	// no match
}

HDDEDATA CALLBACK DdeCallback(UINT uType,UINT uFmt,HCONV hConv,HSZ hsz1,HSZ hsz2,
  HDDEDATA hData,ULONG_PTR,ULONG_PTR) {
 HDDEDATA ret=DDE_FNOTPROCESSED;
#ifdef _DEBUG
 char s[64];
 _snprintf(s,64,"DdeCallback: uType=0x%X\n",uFmt);
 OutputDebugStringA(s);
#endif
 switch (uType) {
/* SERVER */
  case XTYP_ADVSTART: if (uFmt==CF_TEXT || uFmt==CF_UNICODETEXT || uFmt==CF_XlTable) ret=(HDDEDATA)TRUE; break;

  case XTYP_CONNECT: if (!DdeCmpStringHandles(hsz1,hszTopic)) ret=(HDDEDATA)TRUE; break;

  case XTYP_WILDCONNECT: {
   HSZ list[3]={hszDmm,hszTopic};
   ret=DdeCreateDataHandle(gDdeInst,(LPBYTE)list,sizeof list,0,0,0,0);
  }break;

  case XTYP_ADVREQ:
  case XTYP_REQUEST: {
   RO::EFLAGS f=RO::EFLAGS(RO::UNITSPACE|RO::WITHUNIT|~Config.checkBits&RO::CONVDECIMAL);
   TCHAR item[32],c;
   int i,j,k;
   DdeQueryString(gDdeInst,hsz2,item,elemof(item),CP_WINNEUTRAL);
   for (i=0; c=item[i]; i++) {
    if (!IsCharAlpha(c)) break;
   }
   j=i; if (c && !IsCharAlphaNumeric(c)) j++;	// skip " ", "[", "(" or similar non-numeric delimiter
   k=_ttoi(item+j);			// get the index
   if (k>=NumReadouts) return NULL;	// wrong index attached
   item[i]=0;				// strip index
   if (uFmt==CF_UNICODETEXT) {
    WCHAR buf[32], *p=buf;
    switch (GetItemIdx(item)) {
     case 0:
     case 1: p=Readout[k].GenStringW(p,f); break;
     case 2: p=Readout[k].GenUnitW(p); break;
    }
    if (p!=buf) ret=DdeCreateDataHandle(gDdeInst,(LPBYTE)buf,DWORD((p-buf+1)<<1),0,hsz2,uFmt,0);
   }else if (uFmt==CF_TEXT) {
    char buf[32], *p=buf;
    switch (GetItemIdx(item)) {
     case 0:
     case 1: p=Readout[k].GenStringA(p,f); break;
     case 2: p=Readout[k].GenUnitA(p); break;
    }
    if (p!=buf) ret=DdeCreateDataHandle(gDdeInst,(LPBYTE)buf,DWORD(p-buf+1),0,hsz2,uFmt,0);
   }else if (uFmt==CF_XlTable) {
#pragma pack(1)
    struct XLTABLE{
     WORD tdtTable,dimensions,rows,cols;
     WORD tdtType,tdtSize;
     union{
      short tdtShort[4];
      double tdtDouble;
      char tdtChar[8];
     };
    }XlTable={
     0x0010,	// Header
     4,		// 4 Bytes folgen
     1,		// 1 Spalte
     1,		// 1 Zeile
     4,		// Error
     sizeof(short),
     {42}};	// "#N/A"
#pragma pack()

    switch (GetItemIdx(item)) {
     case 0:
     case 1: if (!_isnan(Readout[k].value)) {
      XlTable.tdtType=1;	// Double
      XlTable.tdtSize=sizeof(double);
      XlTable.tdtDouble=Readout[k].value;
     }break;
     case 2: {
      XlTable.tdtType=2;	// String (ANSI)
      XlTable.tdtSize=(XlTable.tdtChar[0]=(char)(Readout[k].GenUnitA(XlTable.tdtChar+1)-(XlTable.tdtChar+1)))+1;
     }break;
     case 3: if (Scope.n) {
      ret=DdeCreateDataHandle(gDdeInst,NULL,12+Scope.tracelen*sizeof(double),0,hsz2,uFmt,0);
      XLTABLE*p=(XLTABLE*)DdeAccessData(ret,NULL);
      p->tdtTable=16;
      p->dimensions=4;
      p->rows=(WORD)Scope.tracelen;
      p->cols=1;
      p->tdtType=1;	// Double values
      p->tdtSize=(WORD)(Scope.tracelen*sizeof(double));
      double *dp=&p->tdtDouble;
      div_t d=div(Trace[0].devicode+81,3); d.quot-=27;
      double factor=e10(false,"\1\2\5"[d.rem],(char)d.quot)/64;
      for (int i=0; i<Scope.tracelen; i++) *dp++=Trace[0][i]*factor;
      DdeUnaccessData(ret);
      return ret;	// But how to transfer the time base? Extra topic?
     }
     case 4: if (!_isnan(Readout[k].min)) {
      XlTable.tdtType=1;	// Double
      XlTable.tdtSize=sizeof(double);
      XlTable.tdtDouble=Readout[k].min;
     }break;
     case 5: if (!_isnan(Readout[k].max)) {
      XlTable.tdtType=1;	// Double
      XlTable.tdtSize=sizeof(double);
      XlTable.tdtDouble=Readout[k].max;
     }break;
    }
    ret=DdeCreateDataHandle(gDdeInst,(LPBYTE)&XlTable,12+XlTable.tdtSize,0,hsz2,uFmt,0);
   }
  }break;
/* CLIENT */
  default: ret=gLog.DdeCallback(uType,uFmt,hConv,hsz1,hsz2,hData); break;
 }
 return ret;
}

struct ENUMDATA{
 LPTSTR tag;
 bool nomatch;
 bool alwaysnamed;
};

static BOOL CALLBACK EnumWindowsProc(HWND w, LPARAM tag) {
 ENUMDATA*ed=(ENUMDATA*)tag;
 if (w!=ghMainWnd) {			// only foreign windows
  TCHAR buf[32];
  GetClassName(w,buf,elemof(buf));
  if (!lstrcmpi(buf,T("DMM"))) {	// with own class name
   DWORD pid;
   GetWindowThreadProcessId(w,&pid);
   HANDLE h=OpenProcess(PROCESS_VM_READ,FALSE,pid);
   if (h) {
    TCHAR ForeignConfName[elemof(ConfName)];
    ReadProcessMemory(h,ConfName,ForeignConfName,sizeof(ConfName),NULL);
    CloseHandle(h);
// If the other process is still unnamed, it looks like a deadlock situation!
    if (!*ForeignConfName) ed->alwaysnamed=false;
    if (!lstrcmpi(ed->tag,ForeignConfName)) ed->nomatch=false;
   }
  }
 }
 return ed->nomatch;
}

void SetConfName(LPCTSTR NewName) {
 if (ConfName!=NewName) lstrcpyn(ConfName,NewName,elemof(ConfName));
 if (hszTopic) DdeFreeStringHandle(gDdeInst,hszTopic);
 hszTopic=DdeCreateStringHandle(gDdeInst,NewName,CP_WINNEUTRAL);
}

static bool IsConfigUnused(LPTSTR Config) {
 ENUMDATA ed;
 ed.tag=Config;
 ed.nomatch=ed.alwaysnamed=true;
 EnumWindows(EnumWindowsProc,(DWORD)&ed);
 if (!ed.alwaysnamed) OutputDebugStringA("Unconfigured instance detected!\r\n");
 return ed.nomatch;
}

// Check whether the enumerated registry value tag name matches the ConfigName from command-line
static bool TagMatch(LPTSTR tag) {
// If no command-line argument given, the first entry that is not occupied by another instance will match
 if (!*ConfName) return IsConfigUnused(tag);

 LPTSTR p=_tcschr(tag,'@');
// If port name is given, compare port portion
 if (*ConfName=='@') {
  if (p && !lstrcmpi(p,ConfName)) return true;
 }else{
// Then check for equal multimeter name
  if (p) *p=0;		// cut the string
  if (!lstrcmpi(tag,ConfName)) {
   if(p) *p='@';	// sew the string
   return true;
  }
  if (p) *p='@';
 }
 return false;
}

static bool LoadConfig() {
 bool found=false;
 HKEY k;
 if (!RegOpenKey(HKEY_CURRENT_USER,T("Software\\h#s\\DMM"),&k)) {	// nothing saved ever
// Retrieve key that matches ConfigName (Windows will make a case-insensitive comparison)
  DWORD len=sizeof(Config);
  if (*ConfName && !RegQueryValueEx(k,ConfName,NULL,NULL,(LPBYTE)&Config,&len)) {
   SetConfName(ConfName);	// for DDE
   found=true;
  }else{
// If not successful, enum the available values until match.
// (This will never match if @ character is in the middle of ConfigName)
   for (DWORD i=0;;i++) {
    TCHAR tag[CONFMAX];
    DWORD taglen=elemof(tag);
    len=sizeof(Config);
    LONG r=RegEnumValue(k,i,tag,&taglen,NULL,NULL,(LPBYTE)&Config,&len);
    if (r==ERROR_NO_MORE_ITEMS) break;
    if (!r && *tag && TagMatch(tag)) {
     SetConfName(tag);
     found=true;
     break;
    }
   }
  }
 }
 RegCloseKey(k);
 if (found) {
// Reposition window as saved, but ShowWindow() is called later (don't flicker)
  WINDOWPLACEMENT wp;
  wp.length=sizeof(wp);
  GetWindowPlacement(ghMainWnd,&wp);
  Config.winpos>>wp.rcNormalPosition;
  wp.showCmd=SW_HIDE;
  SetWindowPlacement(ghMainWnd,&wp);
 }else Config.showCmd=SW_SHOWDEFAULT;	// later, show window as in STARTUPINFO
 return found;
}

static void SaveConfig() {
// Store information for human registry hackers
 if (!*ConfName) return;	// Store nothing without multimeter+port selection

 RegSetValue(HKEY_CURRENT_USER,T("Software\\h#s"),REG_SZ,T("haftmann#software"),17*sizeof(TCHAR));

 HKEY k;
 if (RegCreateKey(HKEY_CURRENT_USER,T("Software\\h#s\\DMM"),&k)) return;
// Store information for human registry hackers
 TCHAR s[64];
 int l=LoadString(ghInst,1,s,elemof(s));
 RegSetValue(k,NULL,REG_SZ,s,l*sizeof(TCHAR));
// Retrieve Window placement info
 WINDOWPLACEMENT wp;
 wp.length=sizeof(wp);
 GetWindowPlacement(ghMainWnd,&wp);
 Config.winpos=wp.rcNormalPosition;
 Config.showCmd=(char)wp.showCmd;
// Store configuration information
 RegSetValueEx(k,ConfName,0,REG_BINARY,(LPBYTE)&Config,sizeof(Config));
 RegCloseKey(k);
}

static void SetRunOnce() {
 if (!*ConfName) return;
 HKEY k;
 if (RegCreateKey(HKEY_CURRENT_USER,T("Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce"),&k)) return;
 TCHAR buf[MAX_PATH+16];
 buf[0]='"';			// prepend double quote (path may contain spaces)
 int l1=GetModuleFileName(0,buf+1,elemof(buf)-1);
 int l2=1+l1+2+lstrlen(ConfName)+1; // space needed, including '\0' terminator
 if (l2<=elemof(buf)) {		// fail SetRunOnce() silently when it doesn't fit
  lstrcpy(buf+1+l1,T("\" "));	// append double quote and a space
  lstrcpy(buf+l1+3,ConfName);	// append configuration name as command line argument
  RegSetValueEx(k,ConfName,0,REG_SZ,(PBYTE)buf,l2*sizeof(TCHAR));
 }
 RegCloseKey(k);
}

// Opens a numbered COM port (1-based, similar to HeOpen())
// and sets protocol to "<baud>,8,n,1" if <baud> was given
// Returns 0 (not INVALID_HANDLE_VALUE) on failure
static HANDLE ComOpen(int n, int baud=0) {
 TCHAR ComName[12];
 _sntprintf(ComName,elemof(ComName),T("\\\\.\\COM%u"),n);
 HANDLE h=CreateFile(ComName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL);
 if ((int)h<0) h=0;
 if (h && baud) {
  DCB dcb;
  RtlZeroMemory(&dcb,sizeof(dcb));
  dcb.DCBlength=sizeof(dcb);
  dcb.BaudRate=baud,
  dcb.fBinary=TRUE;
  dcb.fDtrControl=DTR_CONTROL_ENABLE;	// DTR must be +12V, RTS must be -12V
  dcb.ByteSize=8;	// All other fields remain 0 (NOPARITY,ONESTOPBIT,NOHANDSHAKE)
  SetCommState(h,&dcb);
 }
 return h;
}

// Almost universal port-enumeration procedure
// For Wine 1.4.1, registry keys must be set somehow:
// HKLM/System/CCS/Enum/ACPI/PNP0501/0/...
// ClassGUID = {4D36E978-E325-BFC1-08002BE10318}
// Device Parameters/PortName = COM1
static bool EnumPorts(bool(CALLBACK*cb)(void*,PTSTR),void*p) {
 HANDLE devs=SetupDiGetClassDevs(&GUID_DEVCLASS_PORTS,NULL,0,DIGCF_PRESENT);
 if (devs!=INVALID_HANDLE_VALUE) {
  SP_DEVINFO_DATA devInfo;
  devInfo.cbSize=sizeof devInfo;
  for (int i=0; SetupDiEnumDeviceInfo(devs,i,&devInfo); i++) {
   HKEY hKey;
   TCHAR ComName[8];
   DWORD slen=sizeof ComName;
   *ComName=0;
   if ((hKey=SetupDiOpenDevRegKey(devs,&devInfo,DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_READ))
     ==INVALID_HANDLE_VALUE) continue;
   RegQueryValueEx(hKey,T("PortName"),NULL,NULL,(LPBYTE)ComName,&slen);
   RegCloseKey(hKey);
   if (!cb(p,ComName)) {
    SetupDiDestroyDeviceInfoList(devs);
    return false;
   }
  }
  SetupDiDestroyDeviceInfoList(devs);
 }
 return true;
}


static bool CALLBACK AppendUniquePortCallback(void*p,LPTSTR ComName) {
 if (*ComName=='C') {
  int&nc=(int&)p;
  int i=_ttoi(ComName+3);
  HANDLE h=ComOpen(i);
  if (h) {
   CloseHandle(h);
   if (nc) return false;		// More than one COM port, cannot decide which to use
   nc=i;
  }
 }
 return true;
}

// Appends a port number if no port given and only one usable (COM or USB) port exist.
// This procedure only counts ports that are free (not used by another app)!
// As it's very probably that exactly one found USB port addresses a multimeter, ...
static void AppendUniquePort() {
 if (_tcschr(ConfName,'@')) return;	// A port number is given, don't search
 int nc=0,nu=0;
 char List[HE_NUM_MAX];
 HeEnum(List);				// Create the list of USB ports
 char *p=(char*)memchr(List,0,HE_NUM_MAX);	// scan for an available port
 if (p) {				// there is one, is there another?
  p++;
  nu=(int)(p-List);
  p=(char*)memchr(p,0,List+HE_NUM_MAX-p);
  if (p) return;			// More than one USB port, cannot automatically decide which to use
 }else{					// scan COM ports only if no USB port found
  if (!EnumPorts(AppendUniquePortCallback,&nc)) return;
 }
 if (nc+nu==0) return;			// Not any free port found!
 int l=lstrlen(ConfName);
 if (nc) _sntprintf(ConfName+l,elemof(ConfName)-l,T("@COM%u"),nc);
 else    _sntprintf(ConfName+l,elemof(ConfName)-l,T("@USB%u"),nu);
}

/***********************************
 * GDI resources and font handling *
 ***********************************/

#define FONTNAME T("Arial")	// Name of standard font used
#define SYMBNAME T("UniT-a2")	// Name of private font containing 7-segment numbers and symbols
#define RGBREF(x) ((BYTE*)&x)

// Calculates a color between <c1> and <c2> where <weight> (0..256) is the <c1> part of mixing
COLORREF colormix(COLORREF c1, COLORREF c2, int weight) {
 int nw=256-weight;
 RGBREF(c1)[0]=(BYTE)((RGBREF(c1)[0]*weight+RGBREF(c2)[0]*nw)>>8);
 RGBREF(c1)[1]=(BYTE)((RGBREF(c1)[1]*weight+RGBREF(c2)[1]*nw)>>8);
 RGBREF(c1)[2]=(BYTE)((RGBREF(c1)[2]*weight+RGBREF(c2)[2]*nw)>>8);
 return c1;
}

int CALLBACK GDI::EnumFontFamProc(const LOGFONT*,const TEXTMETRIC*,DWORD,LPARAM p) {
 ((GDI*)p)->unitfont=INSTALLED;
 return 0;	// stop enumerating
}

#ifndef UNICODE
#undef AddFontResourceEx
#define AddFontResourceEx(a,b,c) AddFontResource(a)
#undef RemoveFontResourceEx
#define RemoveFontResourceEx(a,b,c) RemoveFontResource(a)
#endif

void GDI::UnitfontLoad() {
 if (unitfont) return;
 HDC dc=GetDC(0);	// Get screen DC
 EnumFontFamilies(dc,SYMBNAME,EnumFontFamProc,(LPARAM)this);
 ReleaseDC(0,dc);
 if (unitfont) return;
// Try to load the font from current directory (CWD).
// As this program does not alter its directory, CWD ist most probably the program's .EXE directory.
 if (AddFontResourceEx(SYMBNAME T(".ttf"),FR_PRIVATE|FR_NOT_ENUM,0)) unitfont=LOADED;
}

void GDI::UnitfontUnload() {
 if (unitfont==LOADED && RemoveFontResourceEx(SYMBNAME T(".ttf"),FR_PRIVATE|FR_NOT_ENUM,0)) unitfont=NA;
}

void GDI::DeleteObj(HGDIOBJ&obj) {
 if (obj && DeleteObject(obj)) obj=0;
}

void GDI::Delete(bool FontToo) {
 if (dcDblBuf && DeleteDC(dcDblBuf)) dcDblBuf=0;	// kicks out selected pen, font, and bitmap
 HGDIOBJ *obj=(HGDIOBJ*)this;
 for (int i=0; i<17; i++) DeleteObj(obj[i]);
 if (FontToo) UnitfontUnload();
}

HFONT GDI::MakeFont(int x, int y, int weight, LPCTSTR name) {
 HFONT ret=CreateFont(y,x,0,0,weight,0,0,0,DEFAULT_CHARSET,0,0,0,0,name);
 return ret;
}

// Font re-initialization, called when
// * window size changes
// * measurement unit or prefix changes
// * "number of readout" change
// * appearance changes between scope and multimeter mode
void GDI::Make(int x, int y) {
 Delete(false);
 UnitfontLoad();
 if (Config.doubleBuf) {
  HDC dc=GetDC(0);
  bmDblBuf=CreateCompatibleBitmap(dc,x,y);
  dcDblBuf=CreateCompatibleDC(dc);
  ReleaseDC(0,dc);
  SelectBitmap(dcDblBuf,bmDblBuf);
 }
 SIZE exs;		// character cell size for explanation font
 LPCTSTR bigfontname=FONTNAME; int xx=x/10; int weight=FW_BOLD;
 if (Config.sevenSeg && unitfont) bigfontname=SYMBNAME, xx=x>>3, weight=0;
 penDiv=CreatePen(PS_DOT,0,RGB(196,196,196));
 if (Scope.n) {		// create some smaller fonts
  for (int i=0; i<Scope.n; i++) {
   penGraph[i]=CreatePen(PS_SOLID,x/Scope.tracelen,TraceColor[i]);	// green pen with useful pen width
  }
  penGrid=CreatePen(PS_SOLID,0,0xC0C0C0);		// lightgray pen for grid lines
  penTrig=CreatePen(PS_SOLID,x/Scope.tracelen,0x000000);	// black pen for trigger cross
  fntBig=MakeFont(x>>5,y>>3,weight,bigfontname);
  exs.cx=x>>5;
  exs.cy=y>>3;
 }else{			// create client-area-filling font
  for (int i=0; i<NumReadouts; i++) {
   penGraph[i]=CreatePen(PS_SOLID,0,TraceColor[i]);	// same colors as for TraceColors
   penWide [i]=CreatePen(PS_SOLID,3,TraceColor[i]);
   brFill[i]=CreateSolidBrush(colormix(TraceColor[i],GetSysColor(COLOR_WINDOW),0x10));
   brBar [i]=CreateSolidBrush(colormix(TraceColor[i],GetSysColor(COLOR_WINDOW),0x30));
  }
  penNull=CreatePen(PS_NULL,0,0);
  fntBig=MakeFont(xx,MulDiv(y,NumReadouts>2?9:14,16),0,bigfontname);
  exs.cy=MulDiv(y,27,100);
  exs.cx=x/20;
 }
 fntEx=MakeFont(exs.cx,exs.cy,0,FONTNAME);
 fntRo=MakeFont(exs.cx,exs.cy,0,FONTNAME);
 if (unitfont) fntUniT=MakeFont(exs.cx,exs.cy,0,SYMBNAME);
}

void GDI::Make() {
 RECT r;
 GetClientRect(ghMainWnd,&r);
 Make(r.right-r.left,r.bottom-r.top);
}

// Ensure visibility of a string
void DecreaseFontWidth(HDC dc, int ist, int xavail, HFONT &fnt) {
 if (ist<=xavail && ist>=xavail>>1) return;	// do nothing if string fits
 LOGFONT lf;
 GetObject(fnt,sizeof(lf),&lf);
 int w=MulDiv(lf.lfWidth,xavail,ist);
 if (w==lf.lfWidth) w--;	// ensure that width really changes
 int h=lf.lfHeight;
 if (h>w<<2) h=w<<2;	// limit height
 if (w>h) w=h;		// limit width
 if (w==lf.lfWidth && h==lf.lfHeight) return;	// do nothing if nothing changes
 SelectObject(dc,GetStockFont(DEFAULT_GUI_FONT));
 DeleteFont(fnt);
 lf.lfWidth=w;
 lf.lfHeight=h;
 fnt=CreateFontIndirect(&lf);	// re-create with changed width or height (can be time consuming!!)
 SelectObject(dc,fnt);
}

void MakeStringFitW(HDC dc, int xavail, HFONT &fnt, LPCWSTR s, int slen) {
 SIZE size;
 GetTextExtentPoint32W(dc,s,slen,&size);
 DecreaseFontWidth(dc,size.cx,xavail,fnt);
}

// Pick a zero-terminated string out of tightly concatenated strings
static const WCHAR* GetIndexStringW(const WCHAR*p, int i) {
 if (i) do{
  p+=lstrlenW(p)+1;
 }while(--i);
 return p;
}

static const char* GetIndexStringA(const char*p, int i) {
 if (i) do{
  p+=lstrlenA(p)+1;
 }while(--i);
 return p;
}

/**********************************************************************
 * Internal data structures for evaluation and common helper funtions *
 **********************************************************************/

// A one-letter list of 1000' Unit prefixes
static const WCHAR prelettersW[]=L"afpn╡m\0kMGTPE";
// The same as ANSI version, on systems without '╡', '╡' changes to 'u'
static char prelettersA[16];
// A list of units (Unicode)
static const WCHAR unitsW[]=
 L"\0V\0\x2126\0F\0Hz\0A\0░C\0░F\0W\0%\0S\0H\0K\0001/min\0s\0VA\0var\0Wh\0VAh\0varh\0░";
// A list of units (ANSI - i.e. out of current Windows character set)
static char unitsA[64];

static void BuildPrelettersA() {
 WideCharToMultiByte(CP_ACP,0,prelettersW,13,prelettersA,16,"u",NULL);
}

static void BuildUnitsA() {
 char *pa=unitsA;
 LPCWSTR pw=unitsW;
 for (int i=0; i<14; i++) {
  int lw=lstrlenW(pw)+1;
  BOOL fail;
  int la=WideCharToMultiByte(CP_ACP,0,pw,lw,pa,8,NULL,&fail);
  if (fail) {	// replace the unit by an ASCII version
   const char *r=NULL;
   switch (i) {
    case 2: r="Ohm"; break;
    case 6: r="degC"; break;
    case 7: r="degF"; break;
    case 20: r="deg"; break;
    default: OutputDebugStringA("#2\n");
   }
   lstrcpyA(pa,r);
   la=lstrlenA(r)+1;
  }
  pw+=lw;
  pa+=la;
 }
}

// Handle some cases for unitprefix
char RO::_unitprefix(EFLAGS f) const {
 char c=0;
 switch (unitprefix) {
  case -2: c='c'; break;		// centi
  case -1: c='d'; break;		// deci
//  case 1: *d++='d'; c='a'; break;	// deca, should be avoided
  case 2: c='h'; break;			// hecto
  case -6: if (f&UNITFONT) c=0x2B; break;
 }
 return c;
}

// Handle some cases for unitcode
char RO::_unitcode(EFLAGS f) const {
 char c=0;
 if (f&UNITFONT) switch (unitcode) {
  case 2: c=0x2A;			// Ohm
  case 6: c=0x23;			// ░C
  case 7: c=0x3F;			// ░F
 }
 return c;
}

LPWSTR RO::GenUnitW(LPWSTR d,EFLAGS f) const {
 if (unitprefix || unitcode || *unit) {
  if (f&UNITSPACE) *d++=' ';
  if (f&UNITPREFIX) {
   WCHAR c=_unitprefix(f);
   if (!c) c=prelettersW[(unitprefix+18)/3];
   if (c) *d++=c;
  }
  char c=_unitcode(f);
  if (c) {
   *d++=c;
   *d=0;
  }else{
   if (unitcode) {
    lstrcpyW(d,GetIndexStringW(unitsW,unitcode));
    d+=lstrlenW(d);
   }else{
    d+=MultiByteToWideChar(CP_UTF8,0,unit,-1,d,8)-1;
   }
  }
 }
 return d;
}

LPSTR RO::GenUnitA(LPSTR d,EFLAGS f) const {
 if (unitprefix || unitcode || *unit) {
  if (f&UNITSPACE) *d++=' ';
  if (f&UNITPREFIX) {
   char c=_unitprefix(f);
   if (!c) c=prelettersA[(unitprefix+18)/3];
   if (c) *d++=c;
  }
  if (unitcode) {
   char c=_unitcode(f);
   if (c) {
    *d++=c;
    *d=0;
   }else{
    lstrcpyA(d,GetIndexStringA(unitsA,unitcode));
    d+=lstrlenA(d);
   }
  }else{
   WCHAR buf[8];
   MultiByteToWideChar(CP_UTF8,0,unit,-1,buf,8);
   d+=WideCharToMultiByte(CP_ACP,0,buf,-1,d,8,NULL,NULL)-1;
  }
 }
 return d;
}

void GenLogUnit(LPSTR d, const READOUT*ro) {
 *(((RO*)ro)->GenUnitA(d))=0;
}

// generates string for large display, for DDE etc.
LPWSTR RO::GenStringW(LPWSTR d,EFLAGS f) const {
 const char *s=digits;
 if (*s) {				// do nothing if empty
  if (f&LEADSPACE && *s!='-' && min<0) *d++=' ';
  if (f&UNITPREFIX) while (WCHAR c=*s++) {
   if (f&UNITFONT) {
    if (c>='A') c=0x40;	// replace letters by seven-segment 'L'
    if (c=='+') c=' ';	// remove positive sign (the font has no '+')
   }
   if (f&CONVDECIMAL && c=='.') c=cDecimalW;
   *d++=c;
  }else{		// generate DDE version without prefix
   int l=swprintf(d,_isnan(value)?L"NaN":L"%G",value);
   if (f&CONVDECIMAL) {
    LPWSTR p=wcschr(d,'.');
    if (p) *p=cDecimalW;
   }
   d+=l;
  }
  if (f&WITHUNIT) d=GenUnitW(d,f);
  *d=0;
 }
 return d;
}

// generates string for mimimized window title (ANSI version), or not compiled (UNICODE version)
LPSTR RO::GenStringA(LPSTR d,EFLAGS f) const {
 const char *s=digits;
 if (*s) {				// do nothing if empty
  if (f&LEADSPACE && *s!='-' && min<0) *d++=' ';
  if (f&UNITPREFIX) while (char c=*s++) {
   if (f&UNITFONT) {
    if (c>='A') c=0x40;	// replace letters by seven-segment 'L'
    if (c=='+') c=' ';	// remove positive sign (the font has no '+')
   }
   if (f&CONVDECIMAL && c=='.') c=cDecimalA;
   *d++=c;
  }else{		// generate DDE version without prefix
   int l=sprintf(d,_isnan(value)?"NaN":"%G",value);
   if (f&CONVDECIMAL) {
    LPSTR p=strchr(d,'.');
    if (p) *p=cDecimalA;
   }
   d+=l;
  }
  if (f&WITHUNIT) d=GenUnitA(d,f);
  *d=0;
 }
 return d;
}

// sets the new ReadOut value und gives information what has been changed
DWORD RO::Set(const READOUT *ro) {
 DWORD ret=0;
 if (!RtlEqualMemory(&value,&ro->value,sizeof(double))) ret|=1;	// value changed, catches NaN!
 if (!RtlEqualMemory(&min,&ro->min,4*sizeof(double))) ret|=2;	// range or error changed numerically
 if (!RtlEqualMemory(&digits,&ro->digits,15)) ret|=4;		// visual presentation changed
 if (unitcode!=ro->unitcode || lstrcmpA(unit,ro->unit)) ret|=14;// unit and presentation changed
 RtlCopyMemory(this,ro,sizeof(READOUT));
 if (ret&8) {
  BtClear();
//  BtUnzoom();
 }
 if (_finite(value)) BtAdd((float)value);
 return ret;
};

DWORD RO::Clear() {
 static const READOUT emptyreadout={NaN,NaN,NaN};
 return Set(&emptyreadout);
}

// Calculates the decimal point position and the indexed unit prefix.
// The "decimal point position" counts the digits _before_ decimal point.
FUNC(int) dp(char scalebase, char scaleadd, char*unitprefix) {
 char dppos=4;
 char unit=5;
 if (scalebase) {
  char unitsallowed=(scalebase>>5)&3;	// extract special bits
  char specialrangestart=scalebase>>7;
  scalebase&=0x1F;
  char scale=scalebase+scaleadd;	// the scale that applies
  char unitbase=scalebase/3;		// the indexed unit prefix (1=p, 2=n .. 8=G) of range start
  unit=scale/3;				// the indexed unit prefix - maybe!
  dppos=scale%3+1;			// the position of decimal point: numbers before point
  if (specialrangestart && !scaleadd) {
   unit++;				// correct unit prefix
   dppos-=3;				// correct decimal point position (should be never <0!)
  }
  if (unitsallowed && unit==unitbase+unitsallowed) {	// forbidden unit prefix reached?
   unit--;				// correct unit prefix
   dppos+=3;				// correct decimal point position (should be never >5!)
  }
 }
 if (unitprefix) *unitprefix=(unit-5)*3;// zero-centered 10**x exponent value
 return dppos;
}

static const char featuresA[]=
 "LowBat\0Cont\0Diode\0Rel\0Sigma\0Beta\0Hold\0Auto\0hFE\0EF\0Man\0Min\0Max\0AC\0DC\0"
 "Ph1\0Ph2\0Ph3\0TSR\0RST\0cos phi\0phi";
static const WCHAR featuresW[]=
// LoBat   Beep         Diode         Delta    Sigma  Beta
 L"25EA\0\x25A1\x25C1\0\x25B6\x258E\0\x0394\0\x03B2\0\x03B2\0HOLD\0AUTO\0hFE\0EF\0MAN\0MIN\0MAX\0AC\0DC"
//		arrow?		phi	phi
 L"\0╪1\0╪2\0╪3\0TSR\0RST\0cos \x03C6\0\x03C6";
// character codes into UniT-a2.ttf of the first 8 symbols of EFEATURE:
static const WCHAR gUniT_Indices[]={0x24,0x26,0x29,0x21,0x25,0x28,0x22,0xC4};

// Collects bits, based by p, adresses in BitTable. NumBits=sizeof(BitTable)
// Does not invert bits while collecting.
// Skips bits marked with 0xFF (0377 octal) in BitTable.
FUNC(DWORD) CollectBits(const void*p, const BYTE*BitTable, BYTE NumBits) {
 DWORD ret=0, mask=1;
 do{
  BYTE bitnum=*BitTable++;
  if (bitnum!=0377
  && ((const DWORD*)p)[bitnum>>5]&(1<<(bitnum&0x1F))) ret|=mask;
  mask<<=1;
 }while (--NumBits);
 return ret;
}

// Adds a feature to ExtraW string
LPWSTR FEATURE::_addstringW(LPWSTR d, DWORD i, EFLAGS fl) {
 if (fl&PREPENDSPACE)
#ifdef UNICODE 
  *d++=0x2001;		// add a wide 'm' space
#else
  *d++=' ', *d++=' ';
#endif
 lstrcpyW(d,GetIndexStringW(featuresW,i));
 return d+lstrlenW(d);
}

// Build feature string out of bit mask
LPWSTR FEATURE::GenStringW(LPWSTR d, EFLAGS fl) const{
 DWORD features=f;
 for (int i=0; i<NUMFEATURES; i++) {
  if (features&1) d=_addstringW(d,i,fl), *(BYTE*)&fl|=PREPENDSPACE;
  features>>=1;
 }
 return d;
}

// if different, sets the new value and returns 0x10000, else 0
bool FEATURE::Set(DWORD n) {
 if (f!=n) {
  f=n;
  return true;
 }
 return false;
}

/*************************************
 * Multimeter Decoder data structure *
 *************************************/

static struct WORKER{	// Worker thread data
// bool threadBreak;	// terminates the thread's main loop that processes incoming data
// DWORD lastTick;	// GetTickCount() value of last incoming byte
// int timeOutCounter;	// Used by some OnTimer() handler
 HANDLE hCom;		// The currently open serial or USB device
 OVERLAPPED o;		// The hEvent forms multiple objects with hBreak
 HANDLE hBreak;		// A break event object to terminate the worker thread
 HANDLE hThread;	// The worker thread handle
 HANDLE hMutex;		// that protects the gathered data while painiting (interpreting)
// DWORD dataInput;	// Number of bytes received since last invocation of pOnTimer()
// The "binary" decoded config info
 struct CONF{
// LPTSTR dllname;	// TODO: currently, no plugin DLLs are supported
//  MMINFO mmi;		// A copy of MMINFO data, most important, the three procedure pointers
// union{
  struct PORT{
   int num;		// 1 .. 256
   EVIA via;		// 1 or 2
//   BYTE res;		// 0, reserved
//  };
//  LPARAM all;		// for use as combobox item lParam parameter
  }port;
  bool Make();
 }conf;
 INDATA mm;
// LPARAM lParam;
// DWORD timer;		// time, in ms, after the last byte received, OnTimer() gets called
			// BUG: Cannot show "wrong data"!
// int recvlen;		// bytes received (into recvbuf or more)
// int recvmax;
// BYTE recvbuf[400];	// can hold sample data of scope multimeters
 DWORD ThreadProc();
 static DWORD WINAPI ThreadProc(LPVOID);// the thread procedure
 void Start();		// starts the thread
 void Stop();		// stops and terminates the thread
 void MakeTimer();
}w;			// static worker thread data

#ifdef _DEBUG
void DebugPacket() {
 if (!w.mm.rlen) return;
 char buf[160],*p=buf;
 int i=0;
 p+=sprintf(p,"(%d) ",w.mm.rlen);
 do{
  p+=sprintf(p,"%02X ",w.mm.rbuf[i++]);
 }while (p<buf+elemof(buf)-10 && i<w.mm.rlen);
 if (i!=w.mm.rlen) p+=sprintf(p,"... ");
 strcpy(p-1,"\r\n");
 OutputDebugStringA(buf);
}
void _cdecl DebugOut(const char*t,...) {
 char s[160];
 va_list va;
 va_start(va,t);
 int l=_vsnprintf(s,elemof(s)-3,t,va);
 if (l<0) l=elemof(s)-3;	// return value for cropping: set to end of string
 strcpy(s+l,"\r\n");
 OutputDebugStringA(s);
}
# define DbgOut(x) DebugOut x
#else
# define DebugPacket()
# define DbgOut(x)
#endif

FUNC(bool) SetData(DWORD features, const READOUT*readout, int num, const SCOPEDATA* scope) {
 if (unsigned(num)>elemof(Readout)) return false;		// cannot process
 if (scope && unsigned(scope->n-1)>elemof(Trace)-1) return false;
	// cannot process, must be at least 1 trace
 DWORD change=0;
 if (num || features || scope) {	// do nothing in case "0-0-0" - only the icon should flash
  if (w.hMutex && WaitForSingleObject(w.hMutex,1000)) return false;
  change=Features.Set(features)<<16;
  if (!num && !scope) change<<=1;				// without readout, no features!
  if (NumReadouts!=num) NumReadouts=num, change|=1<<17;
  int i;
  for (i=0; i<num; i++) change|=Readout[i].Set(readout+i)<<(i<<4);
  for (; i<elemof(Readout); i++) change|=Readout[i].Clear()<<(i<<4);
  change|=SetScope(scope);
  ReleaseMutex(w.hMutex);
 }
 if (num || scope || !features) change|=1<<24;	// let icon flash
// if (change&0x49999L && !DdePostAdvise(gDdeInst,hszTopic,0)) {	// WRONG THREAD!
 PostMessage(ghMainWnd,WM_USER+22,0,change);
 if (num) gLog.Line(num,readout,features,change);
 return true;
}

// Send bytes to multimeter
FUNC(BOOL) SendBytes(const void*p, int len) {
 OutputDebugStringA("*");
 switch (w.conf.port.via) {
  case VIACOM: {
   DWORD bw;
   return (WriteFile(w.hCom,p,len,&bw,&w.o)
   || GetLastError()==ERROR_IO_PENDING
   && !WaitForMultipleObjects(2,&w.o.hEvent,FALSE,INFINITE)
   && GetOverlappedResult(w.hCom,&w.o,&bw,FALSE))
   && (int)bw==len;
  }
  case VIAUSB: return HeWrite(w.hCom,(const BYTE*)p,len,&w.o);
 }
 return 0;
}

// Calculates minimum and maximum values out of unit, flags etc.
// This function clears the free-form unit, so you must add this later if necessary.
FUNC(void) GenMinMax(READOUT *ro, DWORD features, int maxnumber, char expo) {
 ro->unit[0]=0;
 ro->min=0;
 ro->max=e10(false,maxnumber,expo);
 switch (ro->unitcode) {
  case 1: /*V*/ if (ro->max>1000) ro->max=1000;
		if (!(features&(1<<F_AC))) features|=1<<F_Delta; break;	// max. 1000 V
  case 5: /*A*/ if (ro->max>10) ro->max=10;
		if (!(features&(1<<F_AC))) features|=1<<F_Delta; break;	// max. 10 A
  case 6: /*░C*/ ro->min=-40; ro->max=1000; break;
  case 7: /*░F*/ ro->min=-80; ro->max=1832; break;
  case 9: /*%*/ ro->max=100; break;
  case 20: /*░*/ ro->max=360; break;
 }
 if (features&(1<<F_Delta)) {
  ro->min=-ro->max;	// make symmetric ranges
  if (ro->unitcode==6) ro->unitcode=12; // K for temperature difference
 }
}

/*****************
 * Worker thread *
 *****************/

DWORD WORKER::ThreadProc() {
 conf.Make();
 hCom=0;
 mm.rmax=400;	// enough space?
 mm.rlen=0;
 mm.icto=20;
 mm.trto=3000;
 mm.counts=1999;	// maximum for ICL7136 based multimeters
 mm.baud=9600;
 mm.msg=mm.init;
 (mm.*mm.pHandler)();
 mm.rbuf=new BYTE[mm.rmax+8];

 do{
  if (!hCom) {
   switch (conf.port.via) {
    case VIACOM: {
     hCom=ComOpen(conf.port.num,mm.baud);
     if (hCom) {
      COMMTIMEOUTS to={mm.icto,0,mm.trto,0,0};
      SetCommTimeouts(hCom,&to);
     }
    }break;

    case VIAUSB: {
     hCom=HeOpen(conf.port.num,mm.baud,FILE_FLAG_OVERLAPPED);
     if ((int)hCom<0) hCom=0;
    }break;
   }
   if (hCom) {
    o.hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
    SetData(19,NULL,0);	// "Awaiting data"
    mm.msg=mm.open;
    mm.ok=true;
    (mm.*mm.pHandler)();
    if (!mm.ok) {
     SetData(18,NULL,0);	// "Internal error"
    }
   }else{
    SetData(17,NULL,0);	// "No interface!"
   }
  }else{
   int br;
   SetThreadPriority(hThread,THREAD_PRIORITY_ABOVE_NORMAL);
   switch (conf.port.via) {
    case VIACOM: {
     if (ReadFile(hCom,mm.rbuf+mm.rlen,mm.rmax-mm.rlen+8,(DWORD*)&br,&o)
     || (GetLastError()==ERROR_IO_PENDING
     && !WaitForMultipleObjects(2,&o.hEvent,FALSE,INFINITE)
     && GetOverlappedResult(hCom,&o,(DWORD*)&br,FALSE))) break;
     br=-1;
    }break;
  
    case VIAUSB: {
     br=HeRead(hCom,mm.rbuf+mm.rlen,mm.rmax-mm.rlen+8,mm.icto*2,mm.trto,&o);
    }break;
   }
   SetThreadPriority(hThread,THREAD_PRIORITY_NORMAL);
   if (br<0) {		// problems with interface, close and let re-open
    mm.msg=mm.close;
    mm.ok=true;
    (mm.*mm.pHandler)();
    CloseHandle(o.hEvent);
    CloseHandle(hCom);
    hCom=0;
   }else if (br) {
    mm.rlen+=br;
    DebugPacket();
    mm.msg=mm.data;
    goto hand;
   }else{
    mm.msg=mm.tout;
hand:
    mm.ok=false;
    (mm.*mm.pHandler)();
    if (mm.ok) {
     memmove(mm.rbuf,mm.rbuf+mm.rmax,mm.rlen);
    }else{
     SetData(mm.rlen?21:22,NULL,0);
     DbgOut(("Fail, rlen=%d",mm.rlen));
     mm.rlen=0;
    }
   }
  }
 }while (WaitForSingleObject(hBreak,hCom?0:2000));
 mm.msg=mm.close;
 (mm.*mm.pHandler)();
 SetScope();
 delete[] mm.rbuf;
 return CloseHandle(hCom);
}

DWORD CALLBACK WORKER::ThreadProc(LPVOID param) {
 return ((WORKER*)param)->ThreadProc();
}

void WORKER::Stop() {
 if (!hThread) return;
 SetEvent(hBreak);
 WaitForSingleObject(hThread,INFINITE);
 CloseHandle(hBreak);
 CloseHandle(hThread);
 CloseHandle(hMutex);
 RtlZeroMemory(this,sizeof(*this));
}

void WORKER::Start() {
 Stop();
 hMutex=CreateMutex(NULL,FALSE,NULL);
 hBreak=CreateEvent(NULL,TRUE,FALSE,NULL);
 DWORD ThreadId;
 hThread=CreateThread(NULL,0,ThreadProc,this,0,&ThreadId);
}

/************************
 * Configuration dialog *
 ************************/

// The system image list that contains useful "port" and "usb" images
static struct CONFDLG{
 HIMAGELIST DmmImageList;
 SP_CLASSIMAGELIST_DATA ild;
 static void UpdateCombo(HWND);
 static LONG_PTR CALLBACK DlgProc(HWND,UINT,WPARAM,LPARAM);
 static bool RetrieveConfig(HWND, TCHAR[CONFMAX]);
}ConfDlg;

// A ComboBoxExItem structure with an attached enumeration procedure
class CBFILL:public COMBOBOXEXITEM{
 HWND hCombo;
 static bool CALLBACK cbenum(void*p,LPTSTR ComName)	{((CBFILL*)p)->cbenum(ComName); return true;}
 void cbenum(LPTSTR ComName);
public:
 void cbenum(HWND w)		{hCombo=w; EnumPorts(cbenum,this);}
};

void CBFILL::cbenum(LPTSTR ComName) {
 if (*ComName=='C') {		// don't scan LPT
  pszText=ComName;
  iOverlay=0;			// this will overlay nothing
  lParam=_ttoi(ComName+3);	// one-based
  bool current=IsCurrentPort(ComName);
  if (!current) {
   HANDLE h = ComOpen((int)lParam);
   if (h) CloseHandle(h); else iOverlay=2;	// this will overlay a red X: port not available
  }
  SendMessage(hCombo,CBEM_INSERTITEM,0,(LPARAM)this);
  if (current) ComboBox_SetCurSel(hCombo,iItem);
  iItem++;
 }
}

// The serial and USB ports, it's now (141021) fucking fast
void CONFDLG::UpdateCombo(HWND hCombo) {
 int i;
 CBFILL cbei;
 cbei.mask=CBEIF_TEXT|CBEIF_IMAGE|CBEIF_SELECTEDIMAGE|CBEIF_OVERLAY;
 cbei.iItem=0;
 SetupDiGetClassImageIndex(&ConfDlg.ild,(LPGUID)&GUID_DEVCLASS_PORTS,&cbei.iImage);
 cbei.iSelectedImage=cbei.iImage;
 ComboBox_ResetContent(hCombo);
 cbei.cbenum(hCombo);

 SetupDiGetClassImageIndex(&ConfDlg.ild,(LPGUID)&GUID_DEVCLASS_USB,&cbei.iImage);
 cbei.iSelectedImage=cbei.iImage;
 char List[HE_NUM_MAX];
 HeEnum(List);
 for (i=1; i<=HE_NUM_MAX; i++) {
  char p=List[i-1];
  if (p!=NOT_AVAILABLE && p!=REMOVED) {
   TCHAR UsbName[8];
   _sntprintf(UsbName,elemof(UsbName),T("USB%u"),i);
   cbei.pszText=UsbName;
   bool current=IsCurrentPort(UsbName);
   if (current && p==CANNOT_OPEN) p=AVAILABLE;
	// 1 = yellow "!", 2 = red "x", I'm looking for a gray-out overlay!!
   static const BYTE overlay[4]={0,2,2,1};
   cbei.iOverlay=overlay[-p];
   SendMessage(hCombo,CBEM_INSERTITEM,0,(LPARAM)&cbei);
   if (current) ComboBox_SetCurSel(hCombo,cbei.iItem);
   cbei.iItem++;
  }
 }
}

// Decodes the textual ConfName (of command line argument or registry key) and fills the ConfInfo structure
// TODO: Create a list of builtins and plugin-provided multimeters beforehand
bool WORKER::CONF::Make() {
 char buf[CONFMAX],*p;
#ifdef UNICODE
 WideCharToMultiByte(CP_ACP,0,ConfName,-1,buf,elemof(buf),NULL,NULL);
#else
 lstrcpyn(buf,ConfName,elemof(buf));
#endif
 p=strchr(buf,'@');
 if (!p) return false;
 if (p) *p=0;
 ENUMINFO*e;
 for (e=el;e;e=e->next) {
  if (!lstrcmpiA(buf,e->ModelName)) break;
 }
 if (!e) return false;
 w.mm.pHandler=e->pHandler;
 w.mm.lParam=e->lParam;
// mmi=Builtin[i];
 p++;
 switch ((TCHAR)CharUpper((LPTSTR)*p)) {
  case 'U': port.via=VIAUSB; break;
  case 'C': port.via=VIACOM; break;
  default: return false;
 }
 port.num=atoi(p+3);
 return port.num ? true : false;
}

// Delete(!) the current registry entry, and create a ConfName (save later)
static void ChangeConfName(LPCTSTR NewConfName) {
 HKEY k;
 if (!RegOpenKey(HKEY_CURRENT_USER,T("Software\\h#s\\DMM"),&k)) {
  RegDeleteValue(k,ConfName);
  RegCloseKey(k);
 }
 SetConfName(NewConfName);
 SetWindowTitle();
}

// Called when OK or "Start new instance" is pressed,
// builds the NewConfName string and checks whether:
// * the string is complete
// * the string is not used by another running instance of DMM.exe
// This routine may show MessageBoxes and return <false>.
bool CONFDLG::RetrieveConfig(HWND Wnd, TCHAR NewConfName[CONFMAX]) {
 TCHAR dmm[8], port[8];
 COMBOBOXEXITEM cbei;
 cbei.mask=CBEIF_TEXT;

 HWND w=GetDlgItem(Wnd,100);
 cbei.iItem=ComboBox_GetCurSel(w);
 if (cbei.iItem==-1) goto err;
 cbei.pszText=dmm;
 cbei.cchTextMax=elemof(dmm);
 SendMessage(w,CBEM_GETITEM,0,(LPARAM)&cbei);

 w=GetDlgItem(Wnd,101);
 if (IsWindowEnabled(w)) {	// for now: always TRUE
  cbei.iItem=ComboBox_GetCurSel(w);
  if (cbei.iItem==-1) goto err;
  cbei.pszText=port;
  cbei.cchTextMax=elemof(port);
  SendMessage(w,CBEM_GETITEM,0,(LPARAM)&cbei);
 }else *port=0;			// no port for test device - program logic cannot handle!!

 _sntprintf(NewConfName,CONFMAX,T("%s@%s"),dmm,port);
 if (!IsConfigUnused(NewConfName)) {
  MBox(Wnd,8,IDOK|MB_ICONEXCLAMATION,NewConfName);
  return false;
 }
 return true;
err:
 MBox(Wnd,2,IDOK|MB_ICONEXCLAMATION);
 return false;
}

int ShowDlgItem(HWND w, UINT id, int sh) {
 return ShowWindow(GetDlgItem(w,id),sh);
}

LONG_PTR CALLBACK CONFDLG::DlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_INITDIALOG: {
   ConfDlg.DmmImageList=ImageList_LoadImage(ghInst,MAKEINTRESOURCE(1),16,1,CLR_NONE,IMAGE_BITMAP,0);
   HWND w=GetDlgItem(Wnd,100);		// Multimeter selection
   SendMessage(w,CBEM_SETIMAGELIST,0,(LPARAM)ConfDlg.DmmImageList);
   COMBOBOXEXITEM cbei;
   cbei.mask=CBEIF_TEXT|CBEIF_IMAGE|CBEIF_SELECTEDIMAGE;
   cbei.iItem=0;
   ENUMINFO *e;
   for (e=el;e;e=e->next) {
    WCHAR uni[32];
    MultiByteToWideChar(CP_UTF8,0,e->ModelName,-1,uni,elemof(uni));
#ifdef UNICODE
    cbei.pszText=uni;
#else
    CHAR ans[32];
    WideCharToMultiByte(CP_ACP,0,uni,-1,ans,elemof(ans),NULL,NULL);
    cbei.pszText=ans;
#endif
    cbei.iImage=cbei.iSelectedImage=e->IconIndex;
    int j=(int)SendMessage(w,CBEM_INSERTITEM,0,(LPARAM)&cbei);	// insert in backward order
    if (IsCurrentDmm(cbei.pszText)) ComboBox_SetCurSel(w,j);
   }
   ConfDlg.ild.cbSize=sizeof(ConfDlg.ild);
   SetupDiGetClassImageList(&ConfDlg.ild);
   w=GetDlgItem(Wnd,101);
   SendMessage(w,CBEM_SETIMAGELIST,0,(LPARAM)ConfDlg.ild.ImageList);
   for (int i=0; i<8; i++) CheckDlgButton(Wnd,110+i,(Config.checkBits>>i)&1);
   if (!gGdi.unitfont) ShowDlgItem(Wnd,112,SW_HIDE);		// no 7segment font available
// TODO: Hide "glass style" option if no Vista detected
    ShowDlgItem(Wnd,113,SW_HIDE);
   if (cDecimalW=='.') ShowDlgItem(Wnd,114,SW_HIDE);		// If this box is irrelevant
   SetTimer(Wnd,2,100,NULL);
  }return TRUE;

  case WM_DEVICECHANGE: SetTimer(Wnd,2,1500,NULL); break;

  case WM_TIMER: switch (wParam) {
   case 2: {
    KillTimer(Wnd,wParam);
    HCURSOR curs=SetCursor(LoadCursor(0,IDC_WAIT));
    UpdateCombo(GetDlgItem(Wnd,101));
    SetCursor(curs);
   }break;
  }break;

  case WM_COMMAND: switch (LOWORD(wParam)) {
   case IDOK: {
    TCHAR NewConfName[CONFMAX];
    if (!RetrieveConfig(Wnd,NewConfName)) break;
    BYTE b=0, m=1;
    for (int i=0; i<8; i++) {
     if (IsDlgButtonChecked(Wnd,110+i)) b|=m;
     m<<=1;
    }
    Config.checkBits=b;
    gGdi.Make();
    ChangeConfName(NewConfName);
   }nobreak;

   case IDCANCEL: EndDialog(Wnd,wParam); break;

   case 10: {			// new window button
    TCHAR NewConfName[CONFMAX];
    if (!RetrieveConfig(Wnd,NewConfName)) break;
    TCHAR ExeName[MAX_PATH];
    GetModuleFileName(0,ExeName,elemof(ExeName));
    STARTUPINFO si;
    GetStartupInfo(&si);	// inherit STARTUPINFO
    PROCESS_INFORMATION pi;
    TCHAR CmdLine[500];
    _sntprintf(CmdLine,elemof(CmdLine),T("\"%s\" %s"),ExeName,NewConfName);
    CreateProcess(NULL,CmdLine,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
   }
  }break;

  case WM_DESTROY: {
   SetupDiDestroyClassImageList(&ConfDlg.ild);
   ImageList_Destroy(ConfDlg.DmmImageList);
  }break;
 }
 return FALSE;
}

// Set an initial screen appearance and window title
void SetInitScreen() {
 SetWindowText(ghMainWnd,ConfName);
}

// Puts out features and uses the GDI's current position to place text
int FEATURE::Out(HDC dc, bool measure) const {
 DWORD f=this->f;
 int x=0;
 for (int i=0; i<NUMFEATURES; i++) {
  const WCHAR *p;
  int l;
  int spc=2;
  if (f&1) {
   if (i<8 && gGdi.unitfont) {
    SelectFont(dc,gGdi.fntUniT);
    p=gUniT_Indices+i;
    l=1;
    if (i==7) spc=4;		// add more spaces (the font file has bugs!)
   }else{
    p=GetIndexStringW(featuresW,i);
    l=lstrlenW(p);
   }
   WCHAR buf[16];
   RtlCopyMemory(buf,p,l<<1);
   if (i==F_AC && f&2) buf[l++]='+';	// special case "AC+DC"
   else do buf[l++]=' '; while(--spc);	// add some spaces
   if (measure) {
    SIZE size;
    GetTextExtentPoint32W(dc,buf,l,&size);
    x+=size.cx;
   }else TextOutW(dc,0,0,buf,l);
   if (i<8 && gGdi.unitfont) {
    SelectFont(dc,gGdi.fntEx);
   }
  }
  f>>=1;
 }
 return x;
}

#ifdef _M_IX86
#if _MSC_VER > 1400
# pragma optimize("g",off)
#endif
static void _stdcall Line(HDC dc, int x1, volatile int y1, volatile int x2, volatile int y2) {
 Polyline(dc,(POINT*)&x1,2); // use the parameter list on stack as ready-made point array
}
#if _MSC_VER > 1400
# pragma optimize("g",on)
#endif
#else
static void Line(HDC dc, int x1, int y1, int x2, int y2) {
 POINT p[2]={{x1,y1},{x2,y2}};
 Polyline(dc,p,2);
}
#endif

// This function takes the client rect and adds some borders for the grid rect,
// returns <r> modified to reflect the grid size
void TD::OutGrid(HDC dc, RECT *r) {
 int xx=r->right-r->left;
 int yy=r->bottom-r->top;
 r->top+=yy>>3;
 InflateRect(r,-xx>>6,-yy>>6);
 xx=r->right-r->left;
 yy=r->bottom-r->top;
 int i;
 SelectPen(dc,gGdi.penGrid);
 for (i=0; i<=Scope.grid_x; i++) {
  int x=r->left+MulDiv(i,xx,Scope.grid_x);
  Line(dc,x,r->top,x,r->bottom);
 }
 for (i=0; i<=Scope.grid_y; i++) {
  int y=r->top+MulDiv(i,yy,Scope.grid_y);
  Line(dc,r->left,y,r->right,y);
 }
}

void TD::OutTrig(HDC dc, const RECT *r) const{
 POINT p;
 SelectPen(dc,gGdi.penTrig);
 CalcPoint(r,&p,Scope.tx,Scope.ty-ofs);
 int d=(r->right-r->left)/(Scope.grid_x<<2);
 Line(dc,p.x-d,p.y,p.x+d,p.y);
 d=(r->bottom-r->top)/(Scope.grid_y<<2);
 Line(dc,p.x,p.y-d,p.x,p.y+d);
}

// Takes <r> as grid size and places the trace with given pen
void TD::OutZMark(HDC dc, const RECT *r, HPEN pen) const {
 POINT p[3];
 SelectPen(dc,pen);
 CalcPoint(r,p,0,0);
 p[0].x--;
 p[1].x=p[2].x=1;
 p[1].y=p[0].y-p[0].x;
 p[2].y=p[0].y+p[0].x;
 Polygon(dc,p,3);
}

// Takes <r> as grid size and places the trace with given pen
void TD::OutTrace(HDC dc, const RECT *r, HPEN pen) const {
 POINT *p=(POINT*)LocalAlloc(LMEM_FIXED,Scope.tracelen*sizeof(POINT));
 if (!p) return;
 SelectPen(dc,pen);
 CalcPointList(r,p);
 Polyline(dc,p,Scope.tracelen);
 LocalFree(p);
}

// TextAlign must be TA_UPDATECP
void TD::OutBase(HDC dc, char devicode, char unitcode, char autorange, COLORREF cr) {
 TextOutW(dc,0,0,L"  ",2);	// make spacing
 devicode+=6*9;		// work-around division and modulus with negative values
 char prefix=devicode/9;
 devicode%=9;
 char numzeroes=devicode/3;
 devicode%=3;
 WCHAR s[32],*p=s;
 static const char n1[]="125";
 *p++=n1[devicode];
 for(;numzeroes;numzeroes--) *p++='0';
 *p++=' ';
 WCHAR c=prelettersW[prefix];
 if (c) *p++=c;
 lstrcpyW(p,GetIndexStringW(unitsW,unitcode));
 COLORREF oc;
 if (autorange) {
  SetBkMode(dc,OPAQUE);
  SetBkColor(dc,cr);
  oc=SetTextColor(dc,GetSysColor(COLOR_WINDOW));
 }else oc=SetTextColor(dc,cr);
 TextOutW(dc,0,0,s,lstrlenW(s));
 if (autorange) SetBkMode(dc,TRANSPARENT);
 SetTextColor(dc,oc);
}

void RO::BtOut(HDC dc, int left, int top, int right, int bottom) {
 if (!bt) return;
 SetRect(&btR,left,top,right,bottom);
 right--;
 bottom--;
// check or expand min/max
 float ya=(float)min,ye=(float)max;	// scale to at least including range or zero
 int i;
 for (i=0; i<btLen; i++) {
  float y=bt[i].data;
  if (ya>y) ya=y;
  if (ye<y) ye=y;
 }
 float yd=ye-ya;	// get the difference
 if (!yd) yd=1;		// don't divide by zero later!
 int yn=bottom+int((bottom-top)*ya/yd);	// position of x axis
// get min/max of time axis
 DWORD xa,xe;
 xe=bt[btIdx-1].tick+1000;	// reserve 1 second for a flat segment at end
 xa=bt[btIdx<btLen?btIdx:0].tick;
 if (xe-xa<10000) xa=xe-10000;	// scale to at least 10 seconds
// calculate point array for PolyLine() and Polygon()
 POINT*points=(POINT*)LocalAlloc(LPTR,(btLen+5)*sizeof(POINT));
 POINT*pt=points;
 i=btIdx; do{		// start with oldest value (i.e. the one that would be overwritten next time)
  if (i>=btLen) i=0;
  ++pt;			// start filling with index 1, zero index is polygon start
  pt->x=left+MulDiv(right-left,bt[i].tick-xa,xe-xa);
  pt->y=yn-int((bottom-top)*bt[i].data/yd);
  i++;
 }while (i!=btIdx);
 ++pt;
 pt->x=pt[-1].x;
 pt->y=yn;		// down to zero line
 ++pt;
 *pt=pt[-2];		// copy that point
 ++pt;
 pt->x=right;
 pt->y=pt[-1].y;
 ++pt;
 pt->x=right;
 pt->y=yn;		// down to zero line for last index
 points->x=points[1].x;	// left to where the first point was
 points->y=yn;
// output the graph, first the polygon, then the line
 SelectPen(dc,gGdi.penNull);
 SelectBrush(dc,gGdi.brFill[this-Readout]);
 Polygon(dc,points,btLen+2);
 SelectBrush(dc,gGdi.brBar [this-Readout]);
 Polygon(dc,pt-3,4);
 SelectPen(dc,gGdi.penGraph[this-Readout]);
 Polyline(dc,points+1,btLen);
 SelectPen(dc,gGdi.penWide [this-Readout]);
 Polyline(dc,pt-2,2);
 LocalFree(points);
}

void RO::BtAdd(float v) {
 if (!bt) {
  bt=(BT*)LocalAlloc(LPTR,MAXDEPTH*sizeof(BT));
  if (!bt) return;
 }
 if (btLen<MAXDEPTH) btLen++;
 if (btIdx>=btLen) btIdx=0;	// wrap around
 bt[btIdx].data=v;
 bt[btIdx].tick=GetTickCount();
 btIdx++;			// point to next array position
}

// The font is already selected in device context!
// This function does not emit the unit!
int RO::Out7Seg(HDC dc, int x, int y, bool measure) const {
 SIZE size;
// The font is badly designed, so place each digit to a given position
 int aDX[2];
 GetTextExtentExPoint(dc,T("0."),2,0,NULL,aDX,&size);	// size of each cell
 aDX[1]-=aDX[0];	// with of the dot
 TCHAR buf[12], *p;
 GenString(buf,SEVENSEG);
 int adx[12], *pdx, dx, sx;
 for (p=buf,pdx=adx,sx=0;;p++) {
  switch(*p) {
   case 0: goto breakloop;
   case '.': dx=aDX[1]; break;
   default: dx= p[1]=='.' ? aDX[0] : size.cx; 
  }
  sx+=dx;
  *pdx++=dx;
 }
breakloop:
 if (measure) return sx;
 return ExtTextOut(dc,x,y,ETO_NUMERICSLATIN,NULL,buf,int(p-buf),adx);
}

void OutScope(HDC dc, RECT *r) {
 TD::OutGrid(dc,r);
 Trace[Scope.tsource&0x0F].OutTrig(dc,r);
 int i;
 for (i=0; i<Scope.n; i++) {
  Trace[i].OutZMark(dc,r,gGdi.penGraph[i]);
  Trace[i].OutTrace(dc,r,gGdi.penGraph[i]);
 }
// Value (with unit) sent by multimeter
 SelectFont(dc,gGdi.fntBig);
 SetBkMode(dc,TRANSPARENT);
 if (Config.sevenSeg && gGdi.unitfont) {
  SetTextAlign(dc,TA_TOP|TA_LEFT|TA_UPDATECP);
  MoveToEx(dc,r->left+4,r->top,NULL);
  Readout[0].Out7Seg(dc);
  WCHAR buf[32];
  TextOutW(dc,0,0,buf,int(Readout[0].GenUnitW(buf,RO::EFLAGS(RO::UNITSPACE|RO::UNITPREFIX))-buf));
  if (NumReadouts>=2) {	// The secondary reading is placed to the top-right corner of window
   SIZE size;
   int len=int(Readout[1].GenUnitW(buf,RO::EFLAGS(RO::UNITSPACE|RO::UNITPREFIX))-buf);
   GetTextExtentPoint32W(dc,buf,len,&size);
   size.cx+=Readout[1].Measure7Seg(dc);
   MoveToEx(dc,r->right-4-size.cx,r->top,NULL);
   Readout[1].Out7Seg(dc);
   TextOutW(dc,0,0,buf,len);
  }
 }else{
  SetTextAlign(dc,TA_TOP|TA_LEFT);
  WCHAR buf[32], *e=Readout[0].GenStringW(buf);
  TextOutW(dc,r->left+4,r->top,buf,int(e-buf));
  if (NumReadouts>=2) {	// The secondary reading is placed to the top-right corner of window
   SetTextAlign(dc,TA_TOP|TA_RIGHT);
   e=Readout[1].GenStringW(buf);
   TextOutW(dc,r->right-4,r->top,buf,int(e-buf));
  }
 }
 SelectFont(dc,gGdi.fntEx);
 SetTextAlign(dc,TA_TOP|TA_LEFT|TA_UPDATECP);
 MoveToEx(dc,4,0,NULL);
 Features.Out(dc);
 SelectFont(dc,gGdi.fntEx);
 for (i=0; i<Scope.n; i++) Trace[i].OutBase(dc,TraceColor[i]);
 TD::OutBase(dc,Scope.timebase,0x0E,Scope.autorange,0x808080);
}

/*==============================================*
 * Readouts are placed in the following manner:	*
 * 0. No readout (error in transmission):	*
 * +--------------------+			*
 * |			|			*
 * |    error_message	|			*
 * |			|			*
 * +--------------------+			*
 * 1. One readout (most devices):		*
 * +--------------------+			*
 * |	    info	|			*
 * |    R E A D O U T	|			*
 * +--------------------+			*
 * 2. Two readouts (UT81):			*
 * +--------------------+			*
 * |info	readout2|			*
 * |   R E A D O U T 1	|			*
 * +--------------------+			*
 * 3. Three readouts (UT233):			*
 * +--------------------+			*
 * |	    info	|			*
 * |   R E A D O U T 1	|			*
 * |readout2	readout3|			*
 * +--------------------+			*
 * 4. Four readouts (not seen yet):		*
 * +--------------------+			*
 * |info	readout4|			*
 * |   R E A D O U T 1	|			*
 * |readout2	readout3|			*
 * +--------------------+			*
 * "R E A D O U T" denotes LARGE font,		*
 * "readout" denotes SMALL font.		*
 *						*
 * 5. Table readout (VIP System 3)		*
 * +--------------------+			*
 * |         rowhead	|			*
 * |colhead  data  ...	|			*
 * |...      ...   ...  |			*
 * +--------------------+			*
 * This readout especially for power meters	*
 * may need scroll bars?			*
 *==============================================*/

void HandlePaint(HDC dc,LPARAM options) {
 RECT r;
 GetClientRect(ghMainWnd,&r);
 if (options&PRF_ERASEBKGND) {
  HBRUSH brBack=GetSysColorBrush(COLOR_WINDOW);
  SelectBrush(dc,brBack);
  PatBlt(dc,0,0,r.right,r.bottom,PATCOPY);
 }
 if (w.hMutex && WaitForSingleObject(w.hMutex,200)) return;
// timeout - should never occur, SetData() is fast
 if (Scope.n) OutScope(dc,&r);
 else{
// Value (with unit) sent by multimeter
  int slen;
  SIZE size;
// readout dividers (at fixed position)
  int ro0top=NumReadouts>=1 ? r.bottom>>2 : 0;
  int ro0bottom=r.bottom - (NumReadouts>2 ? ro0top : 0);
  WCHAR buf[32];
  SetBkMode(dc,TRANSPARENT);
  if (NumReadouts) {
   int bu=(r.bottom*3)>>2;	// bottom for unit (if value with 7-segment font)
   if (NumReadouts>2) {
    bu=MulDiv(bu,14,16);
   }
   Readout[0].BtOut(dc,0,ro0top,r.right,ro0bottom);
   int fonty=ro0bottom-1+((ro0bottom-ro0top-2)>>4);
   if (Config.sevenSeg && gGdi.unitfont) {
    SetTextAlign(dc,TA_BOTTOM|TA_LEFT);
    slen=int(Readout[0].GenUnitW(buf,RO::UNITPREFIX)-buf);
    SelectFont(dc,gGdi.fntRo);
    GetTextExtentPointW(dc,buf,slen,&size);
    int x=r.right-size.cx-(r.right>>4);	// divider number - unit
    TextOutW(dc,x,bu,buf,slen);
    x-=r.right>>4;
    SelectFont(dc,gGdi.fntBig);
    DecreaseFontWidth(dc,Readout[0].Measure7Seg(dc),x,gGdi.fntBig);
    SetTextAlign(dc,TA_BOTTOM|TA_RIGHT);
    Readout[0].Out7Seg(dc,x,fonty);
   }else{
    SetTextAlign(dc,TA_BOTTOM|TA_CENTER);
    SelectFont(dc,gGdi.fntBig);	// "LEADSPACE" prevents annoying jumping of centered output on sign change
    slen=int(Readout[0].GenStringW(buf,RO::EFLAGS(RO::NORMAL|RO::LEADSPACE))-buf);
    MakeStringFitW(dc,r.right,gGdi.fntBig,buf,slen);
    TextOutW(dc,r.right/2,fonty,buf,slen);
   }
  }
// Additionals (like HOLD, MIN, MAX, DELTA, PEAK, Diode, hFE, Beta, Beep) sent by multimeter
  SelectFont(dc,gGdi.fntEx);
  int m=r.right;	// middle or right?
  int fonty=ro0top+((ro0top)>>4);
  switch (NumReadouts) {
   case 4:		// Place fourth/secondary readout to the top-right corner of window
   case 2: {		// Features are on top-left corner
    m>>=1;
    int x=Features.Measure(dc);
    SelectFont(dc,gGdi.fntRo);
    WCHAR buf[32];
    RO&ro=Readout[NumReadouts-1];
    int slen=int(ro.GenStringW(buf)-buf);
    GetTextExtentPoint32W(dc,buf,slen,&size);
    DecreaseFontWidth(dc,size.cx,m,gGdi.fntRo);
    ro.BtOut(dc,m,0,r.right,ro0top-1);
    SetTextAlign(dc,TA_BOTTOM|TA_CENTER);
    TextOutW(dc,m+(m>>1),fonty,buf,slen);
    SelectFont(dc,gGdi.fntEx);
   }nobreak;
   case 3:
   case 1: {		// Features are on top-center
    int x=Features.Measure(dc);
    DecreaseFontWidth(dc,x,m,gGdi.fntEx);
    MoveToEx(dc,(m-x)>>1,0,NULL);		// Zentrierung
    SetTextAlign(dc,TA_TOP|TA_LEFT|TA_UPDATECP);
    Features.Out(dc);
    SelectPen(dc,gGdi.penDiv);
    Line(dc,0,ro0top,r.right,ro0top);		// waagerechte Linie
    Line(dc,m,0,m,ro0top);			// senkrechte Linie (evtl. au▀erhalb)
   }break;
   case 0: {		// Message string on center-center of window
    SetTextAlign(dc,TA_BASELINE|TA_CENTER);
    slen=MyLoadStringW(Features.f,buf,elemof(buf));	// Show a message
    MakeStringFitW(dc,r.right,gGdi.fntEx,buf,slen);
    TextOutW(dc,r.right/2,r.bottom/2,buf,slen);
   }break;
  }
  if (NumReadouts>2) {	// place two extra readouts below main readout
   SelectFont(dc,gGdi.fntRo);
   m=r.right>>1;
   int fonty=r.bottom+((r.bottom-ro0bottom)>>4);
   for (int i=1,x=0; i<=2; i++,x=r.right-m) {
    WCHAR buf[32];
    int slen=int(Readout[i].GenStringW(buf)-buf);
    SIZE size;
    GetTextExtentPoint32W(dc,buf,slen,&size);
    DecreaseFontWidth(dc,size.cx,m,gGdi.fntRo);
    Readout[i].BtOut(dc,x,ro0bottom,x+m,r.bottom);
    SetTextAlign(dc,TA_BOTTOM|TA_CENTER);
    TextOutW(dc,x+(m>>1),fonty,buf,slen);
   }
   SelectPen(dc,gGdi.penDiv);
   --ro0bottom;
   Line(dc,0,ro0bottom,r.right,ro0bottom);	// waagerechte Linie (ⁿberhΣngende Diagrammelemente abschneiden)
   Line(dc,m,ro0bottom,m,r.bottom);		// senkrechte Linie
  }
 }
 if (w.hMutex) ReleaseMutex(w.hMutex);
}

/*************************
 * Main window procedure *
 *************************/

static LONG_PTR CALLBACK MainWndProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_CREATE: {
   ghMainWnd=Wnd;
// Valid command-line arguments are:
// device@port	- initializes this device at this port
// device	- locates first saved registry entry for this device (port may be random)
// @port	- locates first saved registry entry for this port (device may be random)
// #number	- locates n-th registry entry (one-based) TODO
// With this argument, LoadConfig places the window to the saved state ...
// If device is unknown, setup dialog appears.
// If port is unknown (given but doesn't exist), setup dialog appears.
// If port is not given, and multiple ports exist, setup dialog appears.
// Otherwise, the software starts operation with the only port found.
// The software never attempts to use a configuration name that is used by another instance.

   HMENU m=GetSystemMenu(Wnd,FALSE);
   AppendMenu(m,MF_SEPARATOR,0,NULL);
   TCHAR s[64];
   LoadString(ghInst,4,s,elemof(s));
   AppendMenu(m,MF_STRING,0x40,s);
   LoadString(ghInst,5,s,elemof(s));
   AppendMenu(m,MF_STRING,0x50,s);
   LoadString(ghInst,6,s,elemof(s));
   AppendMenu(m,MF_STRING,0x60,s);
   
   SendMessage(Wnd,WM_WININICHANGE,0,0);

   EnumAll();	// load all plugins

   lstrcpyn(ConfName,PathGetArgs(GetCommandLine()),elemof(ConfName));
   AppendUniquePort();
   LoadConfig();
   PostMessage(Wnd,WM_USER+100,0,0);
  }break;

  case WM_USER+100: {
   SetInitScreen();
   if (w.conf.Make()) w.Start();
   else PostMessage(Wnd,WM_COMMAND,0x40,0);
  }break;

  case WM_USER+22: {	// lParam = bits indicating what portion of information has changed
   if (lParam) {
    if (lParam&1<<24) DdePostAdvise(gDdeInst,0,0);
    if (IsIconic(Wnd)) {
     if (lParam&0x2000F) SetWindowTitle();	// check Readout[0] change, not else
    }else{
     if (lParam&0xA8888) gGdi.Make();
     if (gGdi.dcDblBuf) HandlePaint(gGdi.dcDblBuf,PRF_ERASEBKGND);
     InvalidateRect(Wnd,NULL,TRUE);
    }
   }
   if (lParam&(1<<24) && Config.flashIcon) {
    SendMessage(Wnd,WM_SETICON,ICON_SMALL,(LPARAM)ghIcons[1]);
    SetTimer(Wnd,2,100,NULL);
   }
  }break;

  case WM_TIMER: switch (wParam) {
   case 2: {
    KillTimer(Wnd,wParam);
    SendMessage(Wnd,WM_SETICON,ICON_SMALL,(LPARAM)ghIcons[0]);
   }break;
  }break;

  case WM_WININICHANGE: {
   TCHAR sDecimal[2];
   GetProfileString(T("intl"),T("sDecimal"),T("."),sDecimal,elemof(sDecimal));
#ifdef UNICODE
   cDecimalW=*sDecimal;
   WideCharToMultiByte(CP_ACP,0,sDecimal,1,&cDecimalA,1,".",NULL);
#else
   cDecimalA=*sDecimal;
   MultiByteToWideChar(CP_ACP,0,sDecimal,1,&cDecimalW,1);
#endif
  }return 0;

  case WM_SIZE: {
   if (wParam==SIZE_MINIMIZED) {
    SetWindowTitle();
    gGdi.Delete();	// no GDI resources are needed, so free all!
   }else{
    gGdi.Make(GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam));
    if (gGdi.dcDblBuf) HandlePaint(gGdi.dcDblBuf,PRF_ERASEBKGND);
   }
  }break;

  case WM_PAINT: {
   PAINTSTRUCT ps;
   BeginPaint(Wnd,&ps);
   SendMessage(Wnd,WM_PRINTCLIENT,(WPARAM)ps.hdc,PRF_ERASEBKGND);
   EndPaint(Wnd,&ps);
  }return 0;

  case WM_PRINTCLIENT: {
   if (gGdi.dcDblBuf) {
    RECT r;
    GetClientRect(Wnd,&r);
    BitBlt((HDC)wParam,0,0,r.right,r.bottom,gGdi.dcDblBuf,0,0,SRCCOPY);
   }else HandlePaint((HDC)wParam,lParam);
  }break;
  
  case WM_QUERYOPEN: {
   SetWindowText(Wnd,ConfName);
  }break;
  
  case WM_COPY: {
   HGLOBAL hMemW=GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE,32*sizeof(WCHAR));
   HGLOBAL hMemA=GlobalAlloc(GMEM_MOVEABLE|GMEM_SHARE,32);
   if (hMemW && hMemA && OpenClipboard(Wnd) && EmptyClipboard()) {
    Readout[0].GenStringW((LPWSTR)GlobalLock(hMemW));
    GlobalUnlock(hMemW);
    Readout[0].GenStringA((LPSTR)GlobalLock(hMemA));
    GlobalUnlock(hMemA);
    SetClipboardData(CF_UNICODETEXT,hMemW);
    SetClipboardData(CF_TEXT,hMemA);
    CloseClipboard();
   }else{
    GlobalFree(hMemA);
    GlobalFree(hMemW);
   }
  }break;
  
  case WM_COMMAND:	// from TranslateAccelerator
  case WM_SYSCOMMAND: switch (wParam&0xFFF0) {
   case 0x40: if (DialogBox(ghInst,MAKEINTRESOURCE(0x40),Wnd,(DLGPROC)CONFDLG::DlgProc)==IDOK) {
    w.Stop();
    SaveConfig();
    SetInitScreen();
    w.Start();
   }break;
   case 0x50: SendMessage(Wnd,WM_COPY,0,0); break;
   case 0x60: gLog.OnOff(Wnd,3); break;
  }break;

  case WM_CLOSE: {
   w.Stop();
   gLog.OnOff(Wnd,2);
   SaveConfig();
  }break;

  case WM_ENDSESSION: if (wParam) {
   SaveConfig();	// don't halt thread, instead, ensure fast Windows shutdown
   if (Config.surviveReboot) SetRunOnce();
  }break;

  case WM_DESTROY: {
   gGdi.Delete();
   PostQuitMessage(IDOK);
  }break;

 }
 return DefWindowProc(Wnd,Msg,wParam,lParam);
}

void CALLBACK WinMainCRTStartup() {
 ghInst=GetModuleHandle(NULL);
 INITCOMMONCONTROLSEX icc={sizeof(icc),ICC_USEREX_CLASSES|ICC_DATE_CLASSES};
 InitCommonControlsEx(&icc);

 ghIcons[0]=(HICON)LoadImage(ghInst,MAKEINTRESOURCE(1),IMAGE_ICON,16,16,0);
 ghIcons[1]=(HICON)LoadImage(ghInst,MAKEINTRESOURCE(2),IMAGE_ICON,16,16,0);

 WNDCLASSEX wc={
  sizeof(wc),
  CS_BYTEALIGNWINDOW|CS_DBLCLKS|CS_HREDRAW|CS_VREDRAW,
  MainWndProc,
  0,
  0,
  ghInst,
  LoadIcon(ghInst,MAKEINTRESOURCE(1)),
  LoadCursor(0,IDC_ARROW),
  0,
  NULL,
  T("DMM"),
  ghIcons[0]};

 RegisterClassEx(&wc);

 BuildPrelettersA();
 BuildUnitsA();
 LoadString(ghInst,1,StdMBoxTitle,elemof(StdMBoxTitle));
 GetModuleFileName(0,HelpName,elemof(HelpName));
 lstrcpy(PathFindExtension(HelpName),T(".chm"));

// Problem: DDE im Haupt-Thread benutzt die gleiche Warteschlange
// wie die Benutzereingaben (gΣhn).
// DDE im Worker-Tread erfordert das Beackern der Warteschlange
// (also alle WaitForSingleObject ersetzen durch MsgWaitForMultipleObjects)
// und/oder das Erzeugen eines versteckten Fensters
// DdePostAdvise() ist zu bl÷d, in die richtige Warteschlange zu posten,
// da muss eine Hilfskonstruktion (PostMessage) her.
 CF_XlTable=RegisterClipboardFormat(T("XlTable"));	// fast Excel format
 DdeInitialize(&gDdeInst,DdeCallback,APPCLASS_STANDARD
   |CBF_FAIL_EXECUTES|CBF_FAIL_POKES
   |CBF_SKIP_CONNECT_CONFIRMS|CBF_SKIP_REGISTRATIONS
   |CBF_SKIP_DISCONNECTS|CBF_SKIP_UNREGISTRATIONS,0);
 hszDmm=DdeCreateStringHandle(gDdeInst,T("DMM"),CP_WINNEUTRAL);
// HSZ hszt=DdeCreateStringHandle(gDdeInst,T("bla"),CP_WINNEUTRAL);
 DdeNameService(gDdeInst,hszDmm,0,DNS_REGISTER|DNS_FILTERON);
// DdeFreeStringHandle(gDdeInst,hsz);
// gHszList.Create(T("\0") T("Value\0") T("Value2\0") T("Unit\0") T("Unit2\0") T("Trace\0"));
// hszMinMax=DdeCreateStringHandle(gDdeInst,T("MinMax"),CP_WINNEUTRAL);
// hszFeatures=DdeCreateStringHandle(gDdeInst,T("Features"),CP_WINNEUTRAL);

 CreateWindowEx(WS_EX_OVERLAPPEDWINDOW,T("DMM"),ConfName,
   WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT,0,400,200,
   0,0,ghInst,NULL);

 ShowWindow(ghMainWnd,Config.showCmd);

 MSG Msg;
 HACCEL hAccel=LoadAccelerators(ghInst,MAKEINTRESOURCE(1));
 while (GetMessage(&Msg,0,0,0)) {
  if (TranslateAccelerator(ghMainWnd,hAccel,&Msg)) continue;
  TranslateMessage(&Msg);
  DispatchMessage(&Msg);
 }
// gHszList.Delete();
 DdeUninitialize(gDdeInst);
 ExitProcess((UINT)Msg.wParam);
}
Detected encoding: OEM (CP437)1
Wrong umlauts? - Assume file is ANSI (CP1252) encoded