Source file: /~heha/hs/Funkuhr.zip/src/Dcf77.c

/******************************************
 * DCF77-Funkuhr-Empfänger über serielles *
 * oder paralleles Port			  *
 * haftmann#software, August 2008	  *
 ******************************************/

#include "Funkuhr.h"

#include <winioctl.h>
#define IOCTL_USBPRINT_GET_LPT_STATUS CTL_CODE(FILE_DEVICE_UNKNOWN,12,METHOD_BUFFERED,FILE_ANY_ACCESS)                                                           

/**** Zeittelegramm ****
0	Minutenstartbit (immer 0)
1 - 14	Wettervorhersage (verschlüsselt), Katastrophenwarnung (siehe mtDekoder.c)
15	Rufbit (bis Mitte 2003: Reserveantenne) 
16	"1": Am Ende dieser Stunde wird MEZ/MESZ umgestellt 
17	"0": MEZ, "1": MESZ 
18	"0": MESZ, "1": MEZ 
19	"1": Am Ende dieser Stunde wird eine Schaltsekunde eingefügt 
20	Startbit (ist immer "1") 
21 - 27 Minute, BCD, beginnend mit niederwertigstem Bit
28	Parität Minute 
29 - 34 Stunde
35	Parität Stunde 
36 - 41	Tag des Monats (1. = 1) 
42 - 44	Wochentag (Mo. = 1, So. = 7) 
45 - 49	Monat (Januar = 1) 
50 - 57	Jahr (ohne Jahrhundert) 
58	Parität Datum, kann entfallen bei Schaltsekunde==1 && Minute==0
(59)	Füll-Bit (=0) NUR BEI Schaltsekunde==1 && Minute==0
Es wird stets die Zeit der _nächsten_ Minute gesendet.
Das vereinfachte nämlich früher den Aufbau von Digitaluhren.
Per Gesetz gibt's Schaltsekunden nur am 30. Juni 21:59:59 MESZ und am 31. Dezember 22:59:59 MEZ
 ***********************/
THid gHid;
static OVERLAPPED o;		// für asynchrone Kommunikation - und zum Thread-Beenden
static HANDLE hThread;
static HANDLE hLibInOut;
static UINT TimerId,TimerPeriod;
static UINT JoyCapP1;
static BYTE (_stdcall*inportb)(WORD);	// 2 Funktionszeiger
static void (_stdcall*outportb)(WORD,BYTE);
static DWORD StartTick;
#define BITTIME 20	// Millisekunden pro Schritt (Bit)
#define hCom gHid.hDev

const GUID GUID_DEVINTERFACE_USBPRINT = {
  0x28d78fad,0x5a12,0x11D1,0xae,0x5b,0x00,0x00,0xf8,0x03,0xa8,0xc2};

// Macht aus <data> BCD->Binär-Wandlung,
// liefert 0xFF bei Pseudotetraden sowie bei Bereichsüberlauf
BYTE GetBCD(BYTE data, BYTE min, BYTE max) {
 if ((data&0x0F)>=0x0A) return 0xFF;	// Pseudotetrade!
 if ((data&0xF0)>=0xA0) return 0xFF;	// Pseudotetrade!
 data-=(data>>4)*6;	// BCD->Binär-Korrektur
 if (data<min) return 0xFF;
 if (data>max) return 0xFF;
 return data;
}

// 1-Bits zählen, zur Paritätsprüfung
BYTE CountLsbBits(ULONG l, BYTE bits) {
 BYTE r=0;
 do{
  if (l&1) r++;
  l>>=1;
 }while (--bits);
 return r;
}

// Zeit-Telegramm einer Minute in SYSTEMTIME dekodieren
// Liefert <false> bei Fehler (dann kann »st« dennoch verändert sein!)
bool DecodeTime(ULONGLONG ll, MYSYSTEMTIME*st) {
 __stosd((PDWORD)st,0,sizeof(*st)/4);
 if ((UINT)ll&1) return false;
 ll>>=16;
 switch ((BYTE)ll&0x16) {
  case 0x12:		// MESZ und Startbit
  case 0x14: break;	// MEZ und Startbit
  default: return false;	// keine oder beide Bits gesetzt, kein Startbit
 }
 ll>>=5;
 if (Config.Where==0 && Config.SerialLineIn==4) st->wMilliseconds=10*BITTIME;
 // TODO (Feintuning): Geo-Position des Empfangsortes einrechnen (ergibt aber bloß 1 ms pro 300 km)
 if (CountLsbBits((ULONG)ll,8)&1) return false;	// falsche Parität Minute
 if ((st->bMinute=GetBCD((BYTE)(ll&0x7F),0,59))==0xFF) return false;
 ll>>=8;
 if (CountLsbBits((ULONG)ll,7)&1) return false;	// falsche Parität Stunde
 if ((st->bHour=GetBCD((BYTE)(ll&0x3F),0,23))==0xFF) return false;
 ll>>=7;
 if (CountLsbBits((ULONG)ll,23)&1) return false;	// falsche Parität Datum
 if ((st->bDay=GetBCD((BYTE)(ll&0x3F),1,31))==0xFF) return false;
 ll>>=6;
 if ((st->bDayOfWeek=GetBCD((BYTE)(ll&0x07),1,7))==0xFF) return false; // Windows prüft das nicht
 ll>>=3;
 if ((st->bMonth=GetBCD((BYTE)(ll&0x1F),1,12))==0xFF) return false;
 ll>>=5;
 if ((st->bYear=GetBCD((BYTE)ll,0,99))==0xFF) return false;
 st->st.wYear+=2000;	// es gibt in absehbarer Zukunft kein 21xx
 return true;
}

// History löschen
void ClearHisto(void) {
 __stosd((DWORD*)Empfang.Histo,0,sizeof(Empfang.Histo)/4);
}

// Ermittelte Impulslänge in Histogramm eintragen,
// ggf. Invertierung umschalten
// liefert Index, bei dem die Impulslänge eingetragen wurde
static BYTE AddHisto(DWORD ms) {
 DWORD sum;
 ms/=10;		// 10-ms-weise eintragen
 if (ms>49) ms=49;	// begrenzen
 if (Empfang.Histo[ms]!=MAXWORD) Empfang.Histo[ms]++;
 if (ms!=49) return (BYTE)ms;	// langer Puls - nur Empfangsproblem?
 if (Empfang.Histo[49]<10) return 49;
 sum=0; do{		// übrige Pulse gegenrechnen
  sum+=Empfang.Histo[--ms];
 }while (ms);
 if (sum>Empfang.Histo[49]) return 49;	// nicht invertieren
 Empfang.Invers=!Empfang.Invers;	// invertieren
 ClearHisto();		// Histogramm jetzt tünneff
 return 0xFF;		// "ungültig"
}

static BOOL CALLBACK EnumThreadWndProc(HWND Wnd, LPARAM lParam) {
 TCHAR n[8];
 GetClassName(Wnd,n,elemof(n));
 if (!lstrcmpi(n,KARTECLASSNAME))
   PostMessage(Wnd,WM_FUNKRECV,LOWORD(lParam),HIWORD(lParam));
 return TRUE;
}

// Postet WM_FUNKRECV an aktive Eigenschaftsseite
// sowie an alle geöffneten Kartendarstellungen
void InfoPropSheet(WPARAM wParam, LPARAM lParam) {
 if (PropWnd) {
  HWND PageWnd=PropSheet_GetCurrentPageHwnd(PropWnd);
  if (PageWnd) PostMessage(PageWnd,WM_FUNKRECV,wParam,lParam);
 }
 if (wParam&0x10) {	// nur „neue Wetterdaten“ und „Tageswechsel“
  EnumThreadWindows(GetCurrentThreadId(),EnumThreadWndProc,MAKELONG(wParam,lParam));
 }
}

#define CurData Empfang.Data[Empfang.Index]
#define CurOkay Empfang.Okay[Empfang.Index]
#define IncMod

static DWORD PulsEndTick;
static bool summieren;

// Auswertung der Gesamtpulslänge (aus Teilstücken mit Lücken)
void OnTimerPulsAuswertung(void) {
 DWORD Ticks=PulsEndTick-StartTick;
 ULONGLONG CurMask=(ULONGLONG)1<<Empfang.Sek;
 KillTimer(MainWnd,1);
 if (Empfang.Signal) {		// zu lang
  CurData|=CurMask;		// Fehler-Bit
 }else{
  if (Ticks<Config.ZuKurz) {	// zu lang, zu kurz
   CurData|=CurMask;		// Fehler-Bit
  }else if (Ticks<Config.Trenn){// 0-Bit (kurz)
   CurOkay|=CurMask;
  }else{			// 1-Bit (lang)
   CurData|=CurMask;
   CurOkay|=CurMask;
  }
  InfoPropSheet(1,AddHisto(Ticks));
 }
 summieren=false;
}

// Simulation einer 59. Sekunde
void OnTimerSek59(void) {
 KillTimer(MainWnd,2);
 Empfang.Sek++;		// sollte 59 werden (60 bei Schaltsekunde?)
 InfoPropSheet(2,0);	// 59. Sekunde anzeigen
}

// Modifiziert Empfang.ZeitNeu derart, dass das Datum nicht verändert wird
static void PatchDatum(void) {
 SYSTEMTIME st1,st2;
 (Config.Checkmarks&32?GetSystemTime:GetLocalTime)(&st1);
 FileTimeToSystemTime(&Empfang.ZeitNeu.ft,&st2);
 *(ULONGLONG*)&st2=*(ULONGLONG*)&st1;	// Tag kopieren
 SystemTimeToFileTime(&st2,&Empfang.ZeitNeu.ft);
}

void OnSignalChange(bool Signal, DWORD Timestamp) {
 DWORD Ticks;

 Ticks=Timestamp-StartTick;
 Empfang.Signal=Signal;
 if (Signal) {		// Impuls-Start
  if (900<Ticks && Ticks<=1100) {	// nichts
   summieren=true;
   SetTimer(MainWnd,1,Config.ZuLang<<1,NULL);
   Empfang.Sek++;
   if (Empfang.Luecke && Empfang.Sek==58) SetTimer(MainWnd,2,1000,NULL);
   if (Empfang.Sek>=60) Empfang.Luecke=false;
  }else if (1900<Ticks && Ticks<2100) {
   summieren=true;
   SetTimer(MainWnd,1,Config.ZuLang<<1,NULL);
   Empfang.Sek++;
   if (Empfang.Luecke) {
    if (Empfang.Sek==60) {	// Telegramm vollständig empfangen
     if (DecodeTime(CurData,&Empfang.Dek)) {
      U64 ft;
      SystemTimeToFileTime(&Empfang.Dek.st,&ft.ft);
      if (Config.Checkmarks&32) ft.ull-=HOUR2FILETIME(((DWORD)CurData>>17&1)+1);
      if (!Empfang.LastOK || ft.ull==Empfang.ZeitNeu.ull+600000000) Empfang.LastOK++;
      Empfang.ZeitNeu=ft;
      if (Empfang.LastOK==2) {	// 2 aufeinanderfolgende Telegramme
       if (!(Config.Checkmarks&8)) PatchDatum();
       if (Empfang.Ein&1) ComputerUhrStellen();	// Nicht bei Ein==2
       Empfang.LastOK=1;
      }
      SetTrayTip();
     }else{
      Empfang.LastOK=0;		// keine Zeiger zeigen!
      SetTrayTip();
     }
    }else{
     Empfang.Luecke=false;	// neue Lücke suchen
    }
   }
   Empfang.Index++;
   if (Empfang.Index==MINU) Empfang.Index=0;
   if (Empfang.Anzahl<MINU) Empfang.Anzahl++;
   if (!Empfang.Anzahl) Empfang.Anzahl++;	// mindestens 1
   CurData=CurOkay=0;
   Empfang.Sek=0;
   Empfang.Luecke=true;	// Lücke gefunden!
  }else if (summieren && Timestamp-PulsEndTick<100) {
   if (AnimYes()) AnimateTrayIcon();
   return;		// ignorieren
  }else{
   Empfang.Luecke=false;// Lücke neu suchen!
  }
  StartTick=Timestamp;
  InfoPropSheet(0,0);
  if (AnimYes()) {
   MakeIcons();
   AnimateTrayIcon();
  }
 }else{			// Impuls-Ende
  if (summieren) {
   PulsEndTick=Timestamp;
  }else{
/*
   BYTE hi;
   ULONGLONG CurMask=(ULONGLONG)1<<Empfang.Sek;
  if (Ticks<Config.ZuKurz) {		// zu kurz
   CurData|=CurMask;			// Fehler-Bit
  }else if (Ticks<Config.Trenn){	// 0-Bit (kurz)
//    ClearCommBreak(hCom);
   CurOkay|=CurMask;
  }else if (Ticks<Config.ZuLang*2U) {	// 1-Bit (lang)
   CurOkay|=CurMask;
   CurData|=CurMask;
  }else{				// zu lang
   CurData|=CurMask;
  }
*/
   InfoPropSheet(1,AddHisto(Ticks));
  }
  if (Decrypt && Empfang.Sek==15 && Empfang.Dek.bMinute%3==2) {
   BYTE cipher[10];
   long saveindex;	// LOWORD = Index (0..479), HIWORD = Monat/Tag
   if (BuildCipher(0,cipher,&saveindex)) {
    long result=Decrypt(cipher);
    DbgPrintf(("%s(%d) : Decrypt()=%X @ %d, MJD=%d\n",__FILE__,__LINE__,result,saveindex%480,saveindex/480));
    if (result>=0) {
     SetCache24Entry(saveindex,result);
     SaveConfig(CACHE24);
     InfoPropSheet(16,saveindex%480);
    }
   }
  }
  if (AnimYes()) AnimateTrayIcon();
 }
}

/*************************************
 *** Priorisierter Empfangs-Thread ***
 *************************************/

/*** A: Serieller Empfangs-Thread ***/
static void AsyncSignalChange(bool Signal,DWORD TimeMark) {
 Signal^=Empfang.Invers;
 if (Config.Piep && AnimYes() && !Empfang.DauerPiep && Config.Where!=2) {
  (Signal?StartBeep:StopBeep)();
 }
 PostMessage(MainWnd,WM_FUNKRECV,Signal,TimeMark);
}	// hier: wParam = 0 oder 1, lParam = Millisekunden-Zähler

// ordnet SetXXX-Code der Ausgabeleitung zu
static const BYTE CodeFromLineOut[3]={
 SETDTR,
 SETRTS,
 SETBREAK};
// ordnet Ereigniskode der Eingabeleitung zu
static const WORD EventFromLineIn[5]={
 EV_CTS,
 EV_DSR,
 EV_RING,
 EV_RLSD,
 EV_BREAK|EV_RXCHAR};
// ordnet Statuskode der Eingabeleitung zu
static const BYTE StateFromLineIn[5]={
 MS_CTS_ON,
 MS_DSR_ON,
 MS_RING_ON,
 MS_RLSD_ON,
 0};	// BREAK-Erkennung ist Sonderfall

// Alles unterdokumentierte von OVERLAPPED nullsetzen
void zeroOverlapped(OVERLAPPED*o) {
 __stosd((DWORD*)o,0,(sizeof(OVERLAPPED)-sizeof(HANDLE))/4);
}

// Signalerkennung auf serieller Leitung
static DWORD CALLBACK UhrThread(LPVOID p) {
 DWORD SerialEvent,SerialState,SerialError,deb;
 DWORD WantEvent=EventFromLineIn[Config.SerialLineIn];
 DWORD WantState=StateFromLineIn[Config.SerialLineIn];
 DWORD tick;

 if (!GetCommModemStatus(hCom,&SerialState)) {		// Ohne das geht's nicht! (W2k)
  PostMessage(MainWnd,WM_TryReOpen,0,0);
  return 0;
 }
 SerialError=(DWORD)-1;
 ClearCommError(hCom,&SerialError,NULL);
 for(;;){
  ResetEvent(o.hEvent);
  if (Config.SerialLineIn==4) PurgeComm(hCom,PURGE_RXABORT|PURGE_RXCLEAR);
  SerialEvent=WantEvent;
  if (WaitCommEvent(hCom,&SerialEvent,&o)) ;
  else /*if ((deb=GetLastError())==ERROR_IO_PENDING)*/ {
   switch (deb=WaitForSingleObject(o.hEvent,Empfang.IntrOK?2000:10)) {
    case 0: {
     Empfang.IntrOK=true;
    }break;

    case WAIT_TIMEOUT: {	// Ob sich hinter dem Rücken dennoch DSR änderte?
     DWORD dw;
     GetCommModemStatus(hCom,&dw);
     if ((dw^SerialState)&WantState) {	// Änderung der Signalleitung?
      Empfang.IntrOK=false;
      SerialEvent=WantEvent;
     }else continue;
    }break;

//    default: DebugPrintf(T("WaitForSingleObject: %u\n"),deb);
   }
/*  }else{
   DebugPrintf(T("WaitCommEvent: %u\n"),deb);
   continue;*/
  }

  if (!SerialEvent) break;	// abgebrochen durch Haupt-Thread

  tick=GetTickCount();
  if (Config.SerialLineIn==4) {	// RxD zum Empfang (Trägerabsenkung = +12V)
   DWORD t=0;
   if (SerialEvent&EV_RXCHAR) {	// "Zeichen" abholen und auswerten
    BYTE chr;
    DWORD nb;
    zeroOverlapped(&o);
    if (ReadFile(hCom,&chr,1,&nb,&o)) {	// sollte nicht versagen
     _BitScanForward(&t,chr|0xFFFFFF00u);
     t++;
//      t=bsf()+1;
    }else{
//     DebugPrintf("ReadFile: %u\n",GetLastError());
     SerialError=(DWORD)-1;
     ClearCommError(hCom,&SerialError,NULL);
     Sleep(10);		// Bug mit USBCDC abmildern (ohne Signal 100% Rechenlast)
    }
   }
   if (SerialEvent&EV_BREAK) t=10;	// langer Impuls (>190 ms)
   if (t) {
    t*=BITTIME;	    // in Millisekunden
    AsyncSignalChange(true,tick);
    Sleep(t);
    AsyncSignalChange(false,tick+t);	// tatsächliche Pulslänge mitteilen
   }
  }else if (SerialEvent&WantEvent) {
   DWORD dw=SerialState;	// alter Zustand
   GetCommModemStatus(hCom,&SerialState);	// Änderung?
   if ((dw^SerialState)&WantState) {
    bool Signal=(dw&WantState)!=0;
    AsyncSignalChange(Signal,tick);
   }else{
    SerialError=(DWORD)-1;
    ClearCommError(hCom,&SerialEvent,NULL);
    Sleep(10);		// Bug mit USBCDC abmildern (ohne Signal 100% Rechenlast)
   }
  }
 }
 return 0;
}

/*** B: Parallelport-Empfangs-Thread ***/
static WORD InAddr,OutAddr;
static BYTE InMask,OutMask,OutSave,InLast;
static bool InInv,OutInv;

// MMSYSTEM-Timer sind ebenfalls ein eigener Thread; wird im 10-ms-Takt gerufen
// Keine sinnvolle Möglichkeit für ein Resume-Problem
static void CALLBACK TimerProc(UINT TimerId, UINT uMsg,
  DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2) {
 BYTE b=inportb(InAddr);
 if ((b^InLast)&InMask) {
  bool Signal=(b&InMask)!=0;
  Signal^=InInv;
  AsyncSignalChange(Signal,timeGetTime());
 }
 InLast=b;
}

/*** C: Soundkarten-Thread ***/
typedef struct{
 double cos,sin;
}sincos_t;

typedef struct{
 short ch,sh,cf,sf;	// Werte in der Reihenfolge: Kosinus/Sinus aufsteigendes cos², absteigendes cos²-Fenster
}sintab_entry_t;	// im Wertebereich ±16K

typedef struct{
 double UsedAngle;	// Benutzter Gesamtwinkel für Tabelle, im Bogenmaß
 sincos_t UsedSC;	// davon Sinus und Kosinus für I/Q-Drehung
 sintab_entry_t e[];
}sintab_t;

struct{
 HWAVEIN hWaveIn;
 DWORD SamplesPerSec;
 double f,w;		// Auf Abtastrate umgerechnete Winkelgeschwindigkeit sowie Winkel des DDS-Generators
 __int64 hi,hq;		// „halbe“ Demodulationsergebnisse
 WAVEHDR wh[10];	// Puffergröße immer 2^n für FFT zur Frequenzsuche
//#ifdef _DEBUG
// LARGE_INTEGER qpf;
//#endif
 sintab_t *sintab;
}demod;

TDemodIQ DemodIQ;

#ifdef _M_IX86
__forceinline void fsincos(sincos_t*sc, double arg) {
 _asm	fld	arg
 _asm	mov	eax,sc
 _asm	fsincos
 _asm	fstp	qword ptr[eax+0]
 _asm	fstp	qword ptr[eax+8]
}

__forceinline __int64 imul64(int a, int b) {
 _asm	mov	eax,a
 _asm	imul	b
}

#else
extern void fsincos(sincos_t*,double);
#define imul64(a,b) ((__int64)(a)*(b))
#endif

#define LN32K	10.397207708399179641258481821873	// Bereich
// Config.iDemodTrig -> DemodIQ.at
// AM-Demodulator-Schaltschwelle von Hand einstellen
// iTrig == 0  -> at == 1
// iTrig == 63 -> at == 32768
void SetTrig() {
 DemodIQ.at=lrint(exp((Config.iDemodTrig>>2)*LN32K/63))<<IQSHIFT;
}

// result = ln(t)*max/LN32K
int Logarithmize(int t, int max) {
 if (t<0) t=0;
 if (t) t=lrint(log(t)*max/LN32K);
 if (t>max) t=max;	// sollte nie vorkommen
 return t;
}

// Hypotenuse berechnen, Integer-Version
int hypoti(int x, int y) {
 return lrint(sqrt(imul64(x,x)+imul64(y,y)));
}

// Faule Hypotenusenabschätzung
int hypot_lazy(int x, int y) {
 x=abs(x);
 y=abs(y);
 if (x>y) y>>=1; else x>>=1;	// den kleineren Wert halbieren
 return x+y;			// Summe liefern, fertig
}

// Config.iDemodTrig <- DemodIQ.at
void UpdateTrig() {
 int lnat=Logarithmize(DemodIQ.at>>IQSHIFT,63);
 if (lnat!=Config.iDemodTrig>>2)
   DbgPrintf(("UpdateTrig(%d->%d) AT<<%d=%d\n",Config.iDemodTrig>>2,lnat,IQSHIFT,DemodIQ.at));
 Config.iDemodTrig=Config.iDemodTrig&3|lnat<<2;
}

__forceinline void Discriminate() {
 int a=DemodIQ.a;
 int at=DemodIQ.at;
 static int LastDiff;
 int Diff;
 if (!DemodIQ.NoAGC) {
  if (a>at) at=(int)((imul64(at,1023)+a)>>10);		// 1/1024 drauf (wie ist dann Tau?)
  else at=(int)((imul64(at,255)+a)>>8);			// 1/256 weg
  DemodIQ.at=at;	// (asymmetrisch wegen kürzerer Trägerabsenkung; evtl. Median verwenden?)
 }
 Diff=a-at;
 if ((LastDiff^Diff)<0) {	// unterschiedliches Vorzeichen = Pegelwechsel
  // hier: nicht piepsen, sonst hängt sich Windows auf!
  int dt=MulDiv(LastDiff,1<<DemodIQ.FFT_LenShift,LastDiff-Diff);	// Geradeninterpolation
  dt+=DemodIQ.nBlocks<<DemodIQ.FFT_LenShift;
  if (dt<0) dt=DemodIQ.nBlocks=0;	// Überlaufsituation nur gelegentlich (TODO: Überläufe anders lösen!)
// Überlaufperiode bei 44100 Sa/s: rund 12 Stunden
  AsyncSignalChange(Diff>>31&1,MulDiv(dt,1000,demod.SamplesPerSec));
  LastDiff=Diff;
 }
 if (DemodIQ.hShowDemod) PostMessage(DemodIQ.hShowDemod,WM_RECV_IQ,DemodIQ.i,DemodIQ.q);
}

static void Demodulate(const short*data) {
 int i,j,q,k;	// auf 32 Bit (15+IQSHIFT=27 Bit) verkürzte Ergebnisse
 __int64 hi,hq,fi,fq,n;
 sincos_t sc;
//#ifdef _DEBUG
// LARGE_INTEGER tic,toc;
// QueryPerformanceCounter(&tic);
//#endif
 sintab_t *T;
 DemodIQ.lock|=2;
 T=demod.sintab;
 fi=demod.hi;		// vorhergehende halbe Demodulationsergebnisse laden
 fq=demod.hq;
 n=hi=hq=0;
 k=1<<DemodIQ.FFT_LenShift;
 for(i=0; i<k; i++) {	// Die innere Schleife kommt ohne Gleitkomma aus
  sintab_entry_t *e=T->e+i;
  int d=data[i];
  n+=d*d;
  hi+=d*e->ch;	// parallelisierbar mit SSE:
  hq+=d*e->sh;	// f=*e;
  fi+=d*e->cf;	// d=__m64i_mm_shuffle_pi16(data[i],0)
  fq+=d*e->sf;	// __m128i_mm_add_epi32(a,__m128i_mm_unpackhi_epi32(__m64i_mm_mulhi_pi16(d,f),__m64i_mm_mullo_pi16(d,f)))
 }
 demod.hi=llrint( hi*T->UsedSC.cos+hq*T->UsedSC.sin);
 demod.hq=llrint(-hi*T->UsedSC.sin+hq*T->UsedSC.cos);	// „zurückdrehen“
 demod.w=fmod(demod.w+T->UsedAngle,2*PI);	// nächstes DDS-Winkelargument normalisieren
 k=DemodIQ.FFT_LenShift;
 i=(int)(fi>>k);
 q=(int)(fq>>k);
 fsincos(&sc,demod.w);
 j=lrint(i*sc.cos-q*sc.sin);
 q=lrint(i*sc.sin+q*sc.cos);	// nachträgliche Korrektur (für feste Sinustabelle)
 DemodIQ.i=j;			// -32768..32768<<IQSHIFT (theoretisch)
 DemodIQ.q=q;
 DemodIQ.a=hypoti(j,q);		// Amplitude (0..32768<<IQSHIFT)
 DemodIQ.n=lrint(sqrt(n>>k));		// Rauschen (0..32768)
 DemodIQ.lock&=~2;
//#ifdef _DEBUG
// QueryPerformanceCounter(&toc);
// DbgPrintf(("Zeit: %d ms\n",lrint((double)(toc.QuadPart-tic.QuadPart)*1000/demod.qpf.QuadPart)));
//#endif
// DbgPrintf(("Demodulate: I=%d, Q=%d, A=%d, N=%d, W=%d°\n",hi,hq,DemodIQ.a,DemodIQ.n,lrint(demod.w*180/PI)));
 Discriminate();
}

static void CALLBACK waveInProc(HWAVEIN hwi, UINT msg, void*dwUser, WAVEHDR*dw1, void*dw2) {
 switch (msg) {
  case WIM_DATA: {
   if (dw1->dwBytesRecorded==dw1->dwBufferLength) {
    Demodulate((short*)dw1->lpData);
    if (!(DemodIQ.lock&1) && DemodIQ.FFT_Data) {
	// TODO: FFT
	// TODO: Resume-Behandlung
    }
    if (DemodIQ.DoFFT) DemodIQ.DoFFT--;
    DemodIQ.nBlocks++;
    waveInAddBuffer(hwi,dw1,sizeof(WAVEHDR));
   }
  }
 }
}

/*** D: Joystick-Thread ***/
static void CALLBACK JoyTimerProc(UINT TimerId, UINT uMsg,
  DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2) {
 static bool LastState;
 bool CurState;
 DWORD TimeAxis = Config.iJoyButton>>5;		// 0 = keine, sonst 1..7
 JOYINFOEX ji;
 InitStruct(&ji,sizeof(ji));
 ji.dwFlags=(TimeAxis?1<<TimeAxis:0)|JOY_RETURNBUTTONS;
 if (joyGetPosEx(Config.iJoystick,&ji)) {	// wine BUG: joyGetPosEx liefert konstante Position
  PostMessage(MainWnd,WM_TryReOpen,0,0);	// Linux BUG: /dev/input/js0 liefert auch bei …
  return;					// … abgeschalteter Kalibrierung falsche Zeitstempel
 }
 CurState=ji.dwButtons>>(Config.iJoyButton&0x1F)&1;// aus bis zu 32 Knöpfen einer (x86 bräuchte das & nicht:-)
 if (CurState!=LastState) {
  switch (TimeAxis) {
   case 0: TimeAxis=timeGetTime(); break;
   case 7: TimeAxis+=2;	nobreak;	// dwButtons und dwButtonNumber überspringen, zu dwPOV gehen
   default: {
    static DWORD LastTime;
    TimeAxis=(&ji.dwXpos)[TimeAxis-1];	// ANNAHME: liefert 16-Bit-Wert vom Mikrocontroller
    TimeAxis=MAKELONG(LOWORD(TimeAxis),HIWORD(LastTime));	// Letztes High-Word einsetzen
    if ((long)(TimeAxis-LastTime)<0) TimeAxis+=0x10000;		// bei Umlauf High-Word erhöhen
    LastTime=TimeAxis;
   }
  }
  AsyncSignalChange(CurState,TimeAxis);
  LastState=CurState;
 }
}

/*** E: USB-Paralleldrucker-Konverter-Thread ***/

static void CALLBACK UsbPrnTimerProc(UINT TimerId, UINT uMsg,
  DWORD_PTR dwUser, DWORD_PTR dw1, DWORD_PTR dw2) {
 BYTE b;
 DWORD dw,e;
 static BYTE LastState;
 if (!DeviceIoControl(hCom,IOCTL_USBPRINT_GET_LPT_STATUS,NULL,0,&b,1,&dw,&o)
 && ((e=GetLastError())!=ERROR_IO_PENDING || WaitForSingleObject(o.hEvent,10))) {
  CancelIo(hCom);
  if (e!=ERROR_IO_PENDING) PostMessage(MainWnd,WM_TryReOpen,0,0);
 }else{
  b>>=Config.iUsbInput-010;	// oktal!
  b&=1;
  if (LastState!=b) AsyncSignalChange(LastState=b,timeGetTime());
 }
}

/*** F: HID-Vorlaufempfänger-Thread ***/
int GetMaxReport(const HIDP_CAPS*caps) {
 int ret=caps->InputReportByteLength;
 if (ret<caps->OutputReportByteLength) ret=caps->OutputReportByteLength;
 if (ret<caps->FeatureReportByteLength) ret=caps->FeatureReportByteLength;
 return ret;
}

// ersetzt HidD_GetInputReport(), mit TimeOut bei FILE_FLAG_OVERLAPPED
bool GetInputReport(HANDLE hDev, void*buf, int len, OVERLAPPED*o) {
 DWORD br;
 zeroOverlapped(o);
 return ReadFile(hDev,buf,len,&br,o)
 || GetLastError()==ERROR_IO_PENDING
 && !WaitForSingleObject(o->hEvent,100)
 && GetOverlappedResult(hDev,o,&br,FALSE)
 && br;
}

static DWORD CALLBACK HidThread(LPVOID p) {
 PUCHAR report;		// HID-Report-Puffer
 int len=gHid.caps.InputReportByteLength;
 report=LocalAlloc(LPTR,len<<1);
 if (report) {
  for (;;) {
   HidP_InitializeReportForID(HidP_Input,4,gHid.pd,report,len);
   if (!GetInputReport(hCom,report,len,&o)) break;
   if (*(DWORD*)(report+len+2)!=*(DWORD*)(report+2)) {
    *(DWORD*)(report+len+2)=*(DWORD*)(report+2);
    if (PropWnd) {
     HWND w=PropSheet_IndexToHwnd(PropWnd,0);
     if (w==PropSheet_GetCurrentPageHwnd(PropWnd)) {
      //SendMessage(w,WM_FUNKRECV,14,(LPARAM)report);
      FillHidRep4(w,&gHid,report);
     }
    }
   }
  }
  CancelIo(hCom);
  LocalFree(report);
 }
 return 5;
}

/******************************
 * Ab hier wieder Main-Thread *
 ******************************/
static void ClearSerialLines(void) {
 EscapeCommFunction(hCom,CLRDTR);
 EscapeCommFunction(hCom,CLRRTS);
 EscapeCommFunction(hCom,CLRBREAK);
}

// ermittelt Invertierung einer Druckerport-Leitung
static bool GetInvFromLine(BYTE line) {
 switch (line) {
  case 017:			// BSY (oktal!)
  case 020:			// STB
  case 021:			// AF
  case 023: return true;	// SEL
 }
 return false;
}

// Öffnet GUID-basiertes Device-Interface mit FILE_FLAG_OVERLAPPED
// Für UsbPrn und HID-Vorlaufempfänger verwendet
static HANDLE OpenDevInterface(const GUID *guid, int idx, DWORD access, DWORD share) {
 HANDLE ret=0;
 HDEVINFO devs=SetupDiGetClassDevs(guid,0,0,DIGCF_PRESENT|DIGCF_DEVICEINTERFACE);
 if (devs!=INVALID_HANDLE_VALUE) {
  SP_DEVICE_INTERFACE_DATA devinterface;
  devinterface.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
  if (SetupDiEnumDeviceInterfaces(devs,NULL,guid,idx,&devinterface)) {
   struct{
    SP_DEVICE_INTERFACE_DETAIL_DATA id;
    TCHAR space[MAX_PATH];
   }id;
   id.id.cbSize=sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
   if (SetupDiGetDeviceInterfaceDetail(devs,&devinterface,&id.id,sizeof(id),NULL,NULL)) {
    ret=CreateFile(id.id.DevicePath,access,share,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,NULL);
    if (ret==INVALID_HANDLE_VALUE) ret=0;
   }
  }
  SetupDiDestroyDeviceInfoList(devs);
 }
 return ret;
}

void AllocFFTSpace() {
 if (DemodIQ.DoFFT) {
  if (!DemodIQ.FFT_Data) {
   DemodIQ.FFT_Data=LocalAlloc(LPTR,(sizeof(fft_t)*3)<<DemodIQ.FFT_LenShift);
   // Realteile, Imaginärteile (verschachtelt), Hanningfenster (dahinter)
   if (DemodIQ.FFT_Data) {
    fft_t *p=DemodIQ.FFT_Data+(2<<DemodIQ.FFT_LenShift);
    int k=1<<DemodIQ.FFT_LenShift;
    int i;
    for (i=0; i<k; i++,p++) {	// das optimiert der Compiler schon weg
     *p=lrint((1-cos(i*2*PI/k))*(1<<IQSHIFT));
    }	// Hanningfenster-Faktoren berechnen (im Bereich 0..2<<IQSHIFT, also 8K)
   }
  }
 }else{
  if (DemodIQ.FFT_Data) DemodIQ.FFT_Data=LocalFree(DemodIQ.FFT_Data);
 }
}

static sintab_t* GenSintab() {
 int len=1<<DemodIQ.FFT_LenShift;
 sintab_t*r=LocalAlloc(LMEM_FIXED,sizeof(sintab_t)+len*sizeof(sintab_entry_t));
 if (r) {
  int i;
  double w=0;
  sincos_t dds;
  for(i=0; i<len; i++) {
   sintab_entry_t *e=r->e+i;
   double wa=(i+0.5)*PI/len;	// Fensterungs-Argument        _
   double wf=0.5*(cos(wa)+1);	// Fenster: zweite Hälfte   _ ( \_)
   double wh=1-wf;		// Fenster: erste Hälfte (_/ )
   fsincos(&dds,w);		// Werte des Quadratur-DDS-Generators
   e->ch=(short)lrint(dds.cos*wh*(1<<IQSHIFT));
   e->sh=(short)lrint(dds.sin*wh*(1<<IQSHIFT));
   e->cf=(short)lrint(dds.cos*wf*(1<<IQSHIFT));
   e->sf=(short)lrint(dds.sin*wf*(1<<IQSHIFT));
   w+=demod.f;			// nächstes DDS-Winkelargument
  }
  r->UsedAngle=w;
  fsincos(&r->UsedSC,w);
  i=lrint(w*180/PI);
  DbgPrintf(("GenSinTab Perioden=%d, Restwinkel=%d°\n",i/360,i%360));
 }
 return r;
}

// Sinustabelle neu berechnen
void SetFiltFreq(int f) {
 if (DemodIQ.f!=f) {
  int even=(f+100)/200;
  int fein=f-even*200;
  Config.iFiltFreq=(WORD)even;
  Config.iAFC=(char)fein;
 }
 if ((DemodIQ.f!=f || !demod.f) && demod.SamplesPerSec) {
  demod.f=f*0.02*PI/demod.SamplesPerSec;	// Kreisfrequenz
  if (demod.sintab) {
   void*old=demod.sintab;
   demod.sintab=GenSintab();	// in atomarer Schreiboperation Zeiger ersetzen
   while (DemodIQ.lock&2) Sleep(0);
   LocalFree(old);
  }
 }
 DemodIQ.f=f;
}

// Liefert gerundete Filterfrequenz in ganzen (nicht nur geraden) Hertz
// <*frac> ist der Initialisierungswert für die Feinverstellung im Bereich -50..50
int FiltFreqHz(int*frac) {
 int sum=Config.iFiltFreq*200+Config.iAFC;
 int ret=(sum+50)/100;		// neigt zum Aufrunden
 int fra=sum-ret*100;		// verbleibende Differenz
 if (fra==-50 && ret&1) {fra+=100; ret--;}	// im Grenzfall gerade Zahlen bevorzugen
 if (frac) *frac=fra;
 return ret;
}

// Startet Empfangs-Thread (unabhängig von »Empfang.Ein«)
static bool StartEmpfangOnce(void) {
 switch (Config.Where) {
  case 0: {		// seriell AKA Win32
   DWORD ThreadId;
   TCHAR ComName[12];
   wnsprintf(ComName,elemof(ComName),T("\\\\.\\COM%u"),Config.SerialNo+1);
   hCom=CreateFile(ComName,GENERIC_READ|GENERIC_WRITE,
     0,NULL,OPEN_EXISTING,FILE_FLAG_OVERLAPPED,0);
   if (hCom==INVALID_HANDLE_VALUE) hCom=0;
   if (hCom) {
    ClearSerialLines();
    if (Config.SerialLineIn==4) {	// via RxD
     DCB dcb;
     InitStruct(&dcb,sizeof(dcb));
     dcb.BaudRate=1000/BITTIME;	// hier: 50 Baud
// Dies ist die Einstellung des Programms „WinClk“ zur „Expert mouse clock“.
// Damit kann man keine zu langen Impulse detektieren.
// Die darin enthaltene Hardware (PIC16F84, abgeschliffen, aber ich erkenne es trotzdem)
// macht sich tatsächlich den Umstand, die LED „nacheilend“ blinken zu lassen.
// Somit ist es synchron zum Piep dieser Software.
// Zu bedenken beim Empfang via RxD ist,
// dass erst beim Einlauf eines Bytes (200 ms) die Länge gemessen
// und die Uhr gestellt werden kann. Dies wird in DecodeTime() kompensiert.
     dcb.fBinary=1;
     dcb.ByteSize=8;	// alles andere bleibt Null
     SetCommState(hCom,&dcb);
    }
    	// positive Hilfsspannung
    EscapeCommFunction(hCom,CodeFromLineOut[Config.SerialLineOut]);
    SetCommMask(hCom,EventFromLineIn[Config.SerialLineIn]);
    o.hEvent=CreateEvent(NULL,TRUE,TRUE,NULL);
    hThread=CreateThread(NULL,0,UhrThread,NULL,0,&ThreadId);
	// für genaue Zeitnahme Priorität hochsetzen
    SetThreadPriority(hThread,THREAD_PRIORITY_HIGHEST);
    return true;
   }
  }break;

  case 1: {		// parallel AKA Direktzugriff
   hLibInOut=MyLoadLibrary(INPOUTDLL);
   if (hLibInOut) {	// "globale" Variablen vorbelegen
    BYTE b;
    (FARPROC)inportb=GetProcAddress(hLibInOut,"Inp32");
    (FARPROC)outportb=GetProcAddress(hLibInOut,"Out32");
    InAddr=Config.ParallelAddr+(Config.ParallelLineIn>>3);
    InMask=1<<(Config.ParallelLineIn&7);
    InInv=GetInvFromLine(Config.ParallelLineIn);
    OutAddr=Config.ParallelAddr+(Config.ParallelLineOut>>3);
    OutMask=1<<(Config.ParallelLineOut&7);
    OutInv=GetInvFromLine(Config.ParallelLineOut);
    OutSave=b=inportb(OutAddr);
    if (OutInv) b&=~OutMask; else b|=OutMask;
    outportb(OutAddr,b);	// Stromversorgung einschalten
    TimerPeriod=10;
    timeBeginPeriod(TimerPeriod);
    TimerId=timeSetEvent(TimerPeriod,TimerPeriod,TimerProc,0,TIME_PERIODIC);
    return true;
   }
  }break;

  case 2: {		// Soundkarte und I/Q-Demodulator
// Das Wirkprinzip ist, das eingehende Frequenzgemisch in Blöcken von 25..50 ms
// entsprechend 20..40 Hz aufzuteilen und in jedem eine Korrelation mit einer
// Sinus- und einer Kosinusfunktion der gegebenen Frequenz zu machen.
// Heraus kommen 2 Zahlen, die man als komplexe Zahl interpretieren kann.
// Dessen Betrag ist ein Maß für die Amplitude der gewünschten Frequenz,
// dessen Argument (aka Winkel) die Phasenlage.
// Die Phasenlage bezüglich Blockanfang kann man nachher korrigieren,
// um das stetige Neuberechnen der Sinuswerte zu vermeiden.
// Die Bandbreite dieser Filterung ist durch die Stückelung vorgegeben: 10..20 Hz.
   int i,size;
   WAVEFORMATEX wf;
   wf.wFormatTag=WAVE_FORMAT_PCM;
   wf.nChannels=1;
   wf.wBitsPerSample=16;
   wf.nBlockAlign=2;
   switch (Config.iSampleRate) {
    case 11:
    case 22:
    case 44: demod.SamplesPerSec=MulDiv(Config.iSampleRate,44100,44); break;
    default: demod.SamplesPerSec=Config.iSampleRate*1000;
   }
   wf.nSamplesPerSec=demod.SamplesPerSec;
   wf.nAvgBytesPerSec=wf.nSamplesPerSec*wf.nBlockAlign;
   if (i=waveInOpen(&demod.hWaveIn,Config.iSoundCard,&wf,(DWORD_PTR)waveInProc,0,CALLBACK_FUNCTION)) {
    DbgPrintf(("waveInOpen Fehler %d\n",i));
    return false;
   }
   _BitScanReverse(&size,wf.nSamplesPerSec/20);
   DemodIQ.FFT_LenShift=(BYTE)size;
   size=wf.nBlockAlign<<size;	// sollte eine Zeit zwischen 50 und 100 ms ergeben!
   DbgPrintf(("WaveIn-Blockgröße: %d Samples = %d ms\n",size>>1,MulDiv(size,500,wf.nSamplesPerSec)));
   // minimal 512, maximal 16K, typisch 4K Samples pro Block (@ 8, 200, 44,1 kSa/s)
   for (i=0; i<elemof(demod.wh); i++) {
    WAVEHDR *wh=demod.wh+i;
    if (!(wh->lpData=LocalAlloc(LMEM_FIXED,size))) return false;
    wh->dwBufferLength=size;
    if (waveInPrepareHeader(demod.hWaveIn,wh,sizeof(WAVEHDR))) return false;
    if (waveInAddBuffer(demod.hWaveIn,wh,sizeof(WAVEHDR))) return false;
   }
   SetFiltFreq(Config.iFiltFreq*200+Config.iAFC);
   demod.sintab=GenSintab();
   demod.w=DemodIQ.nBlocks=0;
   SetTrig();
//#ifdef _DEBUG
//   QueryPerformanceFrequency(&demod.qpf);
//#endif
   if (waveInStart(demod.hWaveIn)) return false;
   return true;
  }break;

  case 3: {		// Joystick-Feuerknopf
// wine BUG: Workaround: uPeriod==0 nicht erlaubt, daher == 10
   switch (joySetCapture(MainWnd,Config.iJoystick,10,FALSE)) {
    case 0: joyReleaseCapture(Config.iJoystick); goto joystart;
// Dieser Fehlerkode kommt regulär unter Win98SE!
// Auch die „Spieloptionen“ in der Systemsteuerung hat damit ein Problem.
// Offenbar erwartet Win98 zwingend einen Hebel für die Funktion.
// joyGetPosEx() funktioniert aber trotzdem.
    case JOYERR_UNPLUGGED: if (LOBYTE(WinVer)<5) goto joystart; else break;
    case JOYERR_NOCANDO: {	// Win7-64: Kommt vor, trotzdem alles OK
joystart:
     TimerPeriod=Config.iJoyButton&0xE0?20:10;	// Zeitstempel vom Hebel: Nicht so straff abfragen
     timeBeginPeriod(TimerPeriod);
     TimerId=timeSetEvent(TimerPeriod,TimerPeriod,JoyTimerProc,0,TIME_PERIODIC);
     JoyCapP1=Config.iJoystick+1;	// merke den "gekaperten" Joystick
    }return true;
   }
  }break;
  
  case 4: {		// UsbPrn
   hCom=OpenDevInterface(&GUID_DEVINTERFACE_USBPRINT,Config.iUsbPrn,0,0);
   if (hCom) {
    o.hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
    TimerPeriod=10;
    timeBeginPeriod(TimerPeriod);
    TimerId=timeSetEvent(TimerPeriod,TimerPeriod,UsbPrnTimerProc,0,TIME_PERIODIC);
    return true;
   }
  }break;

  case 5: {		// Vorlaufempfänger
// Im Prinzip genauso wie der Joystick-Feuerknopf, nur dass Empfangsdaten
// oder die ermittelte Uhrzeit gepuffert und nachträglich abgeholt werden kann.
// Es wird nur genau 1 angestecktes Gerät unterstützt.
   if (HidOpen(&gHid)) {
    DWORD ThreadId;
    __stosd((DWORD*)&o,0,sizeof o/4);
    o.hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
    hThread=CreateThread(NULL,0,HidThread,NULL,0,&ThreadId);
	// für genaue Zeitnahme Priorität hochsetzen
    //SetThreadPriority(hThread,THREAD_PRIORITY_ABOVE_NORMAL);
    return true;
   }
   
  }break;
 }
 return false;
}

// wie StartEmpfangOnce, aber mit mehreren Versuchen nach Exe-Start oder Power-Event
bool StartEmpfang(void) {
 while (!StartEmpfangOnce()) {
  if (!TryReOpen) {
   DbgPrintf(("StartEmpfang() versagt!\n"));
   return false;
  }
  --TryReOpen;
  Sleep(50);
 }
 TryReOpen=0; // nunmehr erfolgreich; keine Restarts bei thread-innerem Versagen
 return true;
}

// Beendet Empfangs-Thread (unabhängig von »Empfang.Ein« und Config.Where)
void EndeEmpfang(void) {
 if (demod.hWaveIn) {
  int i;
  waveInReset(demod.hWaveIn);
  Sleep(1);		// Ob das hilft??
  for (i=0; i<elemof(demod.wh); i++) {
   WAVEHDR *wh=demod.wh+i;
   waveInUnprepareHeader(demod.hWaveIn,wh,sizeof(WAVEHDR));
   LocalFree(wh->lpData);
  }
  waveInClose(demod.hWaveIn);
  LocalFree(demod.sintab);
  __stosd((void*)&demod,0,sizeof(demod)/4);
  if (DemodIQ.FFT_Data) DemodIQ.FFT_Data=LocalFree(DemodIQ.FFT_Data);
 }
 if (hCom) SetCommMask(hCom,0); // WaitCommEvent() loseisen
 if (hThread) {		// seriell AKA Win32
  if (o.hEvent) SetEvent(o.hEvent);	// blockierende Abfrage beenden
  if (WaitForSingleObject(hThread,3000)==WAIT_TIMEOUT)
    TerminateThread(hThread,0);
  if (Config.Piep) StopBeep();
  CloseHandle(hThread);
  hThread=0;
  if (hCom) {
   if (o.hEvent) CloseHandle(o.hEvent),o.hEvent=0;	// USB-Modus?
   else ClearSerialLines();	// Stromversorgung ausschalten
   CloseHandle(hCom);
   hCom=0;
  }
 }
 if (hLibInOut) {	// parallel AKA Direktzugriff
  if (Config.Piep) StopBeep();
  outportb(OutAddr,OutSave);	// Stromversorgung ausschalten
 }
 if (TimerId) {		// parallel oder Joystick
  timeKillEvent(TimerId);
  timeEndPeriod(TimerPeriod);
  TimerId=0;
 }
 if (JoyCapP1) {	// Joystick
  joyReleaseCapture(JoyCapP1-1);
  JoyCapP1=0;
 }
 if (hLibInOut) {	// parallel AKA Direktzugriff
  FreeLibrary(hLibInOut);
  hLibInOut=0;
 }
 HidClose(&gHid);
}

// Thread-Neustart, NUR wenn »Empfang.Ein«!
bool RestartEmpfang(void) {
 if (!Empfang.Ein) return true;
 EndeEmpfang();
 return StartEmpfang();
}

#undef hCom

bool HidOpen(THid*Hid) {
 GUID hidGuid;
 HidD_GetHidGuid(&hidGuid);
 Hid->hDev=OpenDevInterface(&hidGuid,Config.iUsbHid,GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE);
 if (Hid->hDev) {
  if (HidD_GetPreparsedData(Hid->hDev,&Hid->pd)) {
   HidP_GetCaps(Hid->pd,&Hid->caps);
   return true;
  }
  CloseHandle(Hid->hDev);
  Hid->hDev=0;
 }
 return false;
}

void HidClose(THid*Hid) {
 if (Hid->pd) {
  HidD_FreePreparsedData(Hid->pd);
  Hid->pd=0;
 }
 if (Hid->hDev) {
  CloseHandle(Hid->hDev);
  Hid->hDev=0;
 }
}
Detected encoding: ANSI (CP1252)4
Wrong umlauts? -