/* Interface for Fakespace fingertip contact gloves
* Based on AVRCDC by Osamu Tamura
* The Fakespace gloves have conductive tissue contacts at each fingertip
* (so there are 10 contacts in up to 5 groups), and a serial EEPROM
* inside the SubD9♂ plug, mainly to detect a correctly connected glove.
* This software auto-detects swapped-connected gloves!
* Furthermore, this software can work with self-made gloves without EEPROM.
* In that case, swap detection is done via a simple bridge
* An extra feature is reporting ground connected fingers,
* activate it by issuing "G1"
* This device draws very low standby current (220 µA, mainly for USB pull-up)
-070222 handling "missed" SOFs in main loop (incorrect time reported)
+070225 Saving G and T state in EEPROM
*070225 Ground connections will be reported too, but without extra bits,
in G0 state
*
* Source file properties: tabsize = 8, encoding = utf-8
*/
#include <string.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
#include <avr/eeprom.h>
#include "usbdrv.h"
#define DEBOUNCE 10 // ms of debouncing time
/*******
* LED *
*******/
static uchar Led_T; // flashing time in ms, 0 = LED is steady
static uchar Led_F; // flashing frequency in ms (half period)
static uchar Led_C; // flash-counter (via SOF pulses)
// start LED flashing, called from USB framework
void Led_Start(uchar t) {
if (!Led_T) {
Led_C=t;
PORTC|=0x20; // switch LED OFF
}
Led_T=Led_F=t;
}
// actualize LED state every 1 ms
static void Led_On1ms(void) {
uchar led_t=Led_T, led_c; // help compiler optimizing
if (!led_t) return;
led_c=Led_C; // copy to register
if (!--led_c) {
PORTC^=0x20; // toggle LED
led_c=Led_F; // reload time
}
if (!--led_t) PORTC&=~0x20; // switch LED ON
Led_T=led_t; // save registers back
Led_C=led_c;
}
/*******************************
* Detecting connection groups *
*******************************/
// wait in units of 0.3 µs
static void wait_us(uchar w) {
do __asm("nop"); while(--w);
}
// Drive bits (glove fingertip contact) low where <Drive> bit is one,
// and set bits to internal-pullup where <Drive> bit is zero
static void DriveBits(unsigned Drive) {
uchar x1,x2,x2b; // SubD9 connector X1 vs. X2 (see schematic)
x1 =Drive; // normally: left glove
x2 =Drive>>8; // normally: right glove
x1&=0x1F;
x2b=(x2>>4)&1; // PortB bit
x2<<=4; // PortD bits
DDRC|=0x1F; // drive all fingerstips (catch capacitive coupling)
DDRD|=0xF0;
DDRB|=0x01;
PORTC=(PORTC|0x1F)&~x1; // drive the no-more-used output to HIGH (shortly)
PORTD=(PORTD|0xF0)&~x2;
PORTB=(PORTB|0x01)&~x2b;
// wait_us(10); // wait a bit
DDRC =(DDRC &0xE0)| x1; // then drive the new output to LOW
DDRD =(DDRD &0x0F)| x2;
DDRB =(DDRB &0xFE)| x2b;
}
// Read bits. For fingertips driven to LOW, a high bit is returned
// Bits 5,6,7,13,14,15 are always zero.
// This way, two bytes suitable for serial response are returned.
static unsigned ReadBits(void) {
usbWord_t ret;
ret.bytes[0]=PINC|0xE0;
ret.bytes[1]=(PIND>>4) | ((PINB|0xFE)<<4);
return ~ret.word;
}
typedef struct { // The connection detection structure
uchar N; // number of connections
unsigned C[5]; // connection list (maximum 5 pairs)
}conn_t;
static conn_t Conn; // Detected connections (in order of bits)
register uchar RepGnd asm("r5"); // switch: report ground connection(s)
// count bits in a 16-bit word
static uchar CountBits(unsigned w) {
uchar i,r=0;
for (i=0; i<16; i++,w>>=1) if (w&1) r++;
return r;
}
// Find connection groups (unsorted)
static void FindConnGroups(void) {
unsigned detected=0xE0E0; // let auto-skip non-available fingers
unsigned Drive,Cur;
Conn.N=0; // clear list
Cur=ReadBits(); // find ground connections first
if (Cur && RepGnd) {
if (Cur&0x00FF) Cur|=0x0020;
if (Cur&0xFF00) Cur|=0x2000; // report with extra bits ("extra fingers") set
}
if (CountBits(Cur)>1) { // NEW: always report if more than 1 bit set
Conn.C[0]=Cur; // add to list (Conn.N is always zero here)
Conn.N++;
}
detected|=Cur; // skip these fingers later (no more an error)
for (Drive=1; Drive; Drive<<=1) {
if (Drive&detected) continue; // skip previously found connections
DriveBits(Drive);
wait_us(100); // wait 33 µs to stabilize capacitive conducted lines
Cur=ReadBits()&~detected;
if (!(Cur&Drive)) {
Conn.N=0;
return; // error: line is not driven to LOW
}
if (Cur&~Drive) { // there is a connection
detected|=Cur; // add connection for skipping
Conn.C[Conn.N++]=Cur; // add connection to list
}
}
DriveBits(0); // switch off all drivers (only weak pull-ups)
}
static conn_t Checked; // Unsorted conngroups, constant after DEBOUNCE
register uchar DebounceCounter asm("r4");
// Checks whether a new stable <Conn> is available. If so, 1 is returned.
static uchar StableConnGroups(void) {
if (memcmp(&Conn,&Checked,1+(Conn.N<<1))) { // changing
Checked=Conn; // copy structure
DebounceCounter=DEBOUNCE;
return 0;
}
if (!DebounceCounter) return 0; // not new
if (--DebounceCounter) return 0; // not zero
return 1;
}
static conn_t Sorted;
// Sort the current connection group (new connections first) into <Sorted>
// Returns TRUE when a change occurs, FALSE otherwise
static uchar SortConnGroups(void) {
static conn_t New; // <static>: don't allocate onto stack
uchar i,j,ret=0;
New.N=0;
for (i=0; i<Conn.N; i++) {
unsigned conn=Conn.C[i]; // fetch a new connection
for (j=0; j<Sorted.N; j++) {
if (conn==Sorted.C[j]) goto brk1; // skip known connections
}
New.C[New.N++]=conn; // Add a new connection at queue head
ret=1; // report change
brk1:;
}
for (j=0; j<Sorted.N; j++) {
unsigned conn=Sorted.C[j]; // fetch a previous connection
for (i=0; i<Conn.N; i++) {
if (conn==Conn.C[i]) {
New.C[New.N++]=conn; // Add a known connection (keeping order)
goto brk2;
}
}
ret=1; // No more found: a change!
brk2:;
}
if (ret) Sorted=New; // copy structure
return ret;
}
/**************************
* Talking to I²C EEPROMs *
**************************/
register uchar CurI2c asm("r3"); // switch: talking to X1 or X2 EEPROM?
// drive SCL low
static void SCLL(void) {
if (CurI2c) {
PORTB&=~0x04;
DDRB |= 0x04;
}else{
PORTD&=~0x01;
DDRD |= 0x01;
}
}
// drive SCL high, then put the line in high-impedance with internal pull-up
static void SCLH(void) {
if (CurI2c) {
PORTB|=0x04;
DDRB&=~0x04;
}else{
PORTD|=0x01;
DDRD&=~0x01;
}
}
// read-in SCL state (wait if LOW)
static uchar SCLR(void) {
if (CurI2c) return PINB&0x04;
return PIND&0x01;
}
// drive SDA low
static void SDAL(void) {
if (CurI2c) {
PORTB&=~0x02;
DDRB |= 0x02;
}else{
PORTD&=~0x02;
DDRD |= 0x02;
}
}
// drive SDA high, then put the line in high-impedance with internal pull-up
static void SDAH(void) {
if (CurI2c) {
PORTB|=0x02;
DDRB&=~0x02;
}else{
PORTD|=0x02;
DDRD&=~0x02;
}
}
// read-in SDA state (data bit)
static uchar SDAR(void) {
if (CurI2c) return PINB&0x02;
return PIND&0x02;
}
// pulse SCL (HIGH, then wait, then read SDA, then LOW)
// Include wait cycles here if necessary
static uchar SCLP(void) {
uchar ret=0;
// wait_us(3); // wait 1 µs
SCLH();
wait_us(6); // wait another 2 µs - so clock speed is 400 kHz
// do if (SCLR()) break; while(--ret); // additional wait (I²C spec.)
ret=SDAR();
SCLL();
// wait_us(3);
return ret;
}
// Write a byte onto I²C, returns the status bit
// SCL must be in LOW state, SDA in HIGH state (the state on exiting)
static uchar I2cWriteByte(uchar b) {
uchar i=8;
do{
if (b&0x80) SDAH(); else SDAL(); // I²C transfers MSB first
SCLP();
b<<=1;
}while (--i);
SDAH();
return SCLP();
}
// Read a byte from I²C and send the status bit <q>
// SCL must be in LOW state, SDA in HIGH state (the state on exiting)
static uchar I2cReadByte(uchar q) {
uchar i=8,b=0;
do{
b<<=1;
if (SCLP()) b|=1; // read MSB first
}while (--i);
if (!q) SDAL();
SCLP();
SDAH(); // release SDA
return b;
}
// assumes SDA in HIGH state and SCL in any state
// releases I²C with SDA = HIGH and SCL = LOW
static void I2cStart(void) {
wait_us(3); // wait 1 µs
SCLH(); // necessary if I²C bus is in arbited state
wait_us(3); // wait 5 µs
SDAL(); // do SDA H->L transition while SCL is high
wait_us(3); // wait 5 µs
SCLL(); // initialize SCL
wait_us(3); // wait 5 µs
SDAH(); // initialize SDA
}
// assumes I²C with SDA = HIGH and SCL = LOW
// releases I²C with SDA = SCL = HIGH
static void I2cStop(void) {
// char w=0;
SDAL();
wait_us(3); // wait 5 µs
SCLH();
wait_us(3); // wait 5 µs
// do if (SCLR()) break; while(--w);
SDAH(); // do SDA L->H transition while SCL is high
// do if (SDAR()) break; while(--w);
}
/**********************************************************
* Access glove's internal I²C EEPROM 24LC02B (or bridge) *
**********************************************************/
// Read serial EEPROM from start (max. 255 Bytes)
// Returns number of successfully read bytes (always 0 or <buflen>)
static uchar ReadEeprom(uchar addr, uchar*buf, uchar buflen) {
uchar ret=0;
I2cStart();
if (!I2cWriteByte(0xA0) // EEPROM I²C write address
&& !I2cWriteByte(addr)) { // EEPROM address
I2cStart();
if (!I2cWriteByte(0xA1)) { // EEPROM I²C read address
while(++ret<=buflen) {
*buf++=I2cReadByte(buflen==ret);
}
}
}
I2cStop();
return ret;
}
register uchar sig_x1 asm("r6");
register uchar sig_x2 asm("r7"); // signature bytes for the connectors
register uchar SwapLR asm("r2"); // gloves swapped
// retrieve left-glove ('L') or right-glove ('R') signature
// Any other value means no or unknown signature ("NO GLOVE" report)
uchar GetSig(void) {
uchar ret=0;
if (!SDAR()) ret='L'; // SubD bridge 6-9 (self-made left glove detect)
else if (!SCLR()) ret='R'; // SubD bridge 6-8 (self-made right glove detect)
else ReadEeprom(0,&ret,1); // read first byte of signature EEPROM
return ret;
}
// read in signature bytes from the connected gloves (first byte of I²C EEPROM)
// To be called repeatedly to detect connecting/disconnecting a glove
static void ReadSig(void) {
SwapLR=CurI2c=0; // assume no signature
sig_x1=GetSig();
CurI2c++;
sig_x2=GetSig();
if ((sig_x1=='R' && sig_x2!='R')
|| (sig_x2=='L' && sig_x1!='L')) SwapLR++; // gloves swapped
}
/*********************
* USB and CDC stuff *
*********************/
enum {
SEND_ENCAPSULATED_COMMAND = 0,
GET_ENCAPSULATED_RESPONSE,
SET_COMM_FEATURE,
GET_COMM_FEATURE,
CLEAR_COMM_FEATURE,
SET_LINE_CODING = 0x20,
GET_LINE_CODING,
SET_CONTROL_LINE_STATE,
SEND_BREAK};
PROGMEM char usbDescriptorConfiguration[] = { /* USB configuration descriptor */
9, /* sizeof(usbDescrConfig): length of descriptor in bytes */
USBDESCR_CONFIG, /* descriptor type */
67,0, /* total length of data returned (including inlined descriptors) */
2, /* number of interfaces in this configuration */
1, /* index of this configuration */
0, /* configuration name string index */
USBATTR_BUSPOWER, /* attributes */
20/2, /* max USB current in 2mA units */
/* interface descriptor */
9, /* sizeof(usbDescrInterface): length of descriptor in bytes */
USBDESCR_INTERFACE, /* descriptor type */
0, /* index of this interface */
0, /* alternate setting for this interface */
1, /* endpoints excl 0: number of endpoint descriptors to follow */
2, /* control interface class: CDC */
2, /* control interface subclass: Abstract (Modem) */
1, /* control interface protocol: AT commands */
0, /* string index for interface */
/* CDC Class-Specific descriptor */
5, /* sizeof(usbDescrCDC_HeaderFn): length of descriptor in bytes */
0x24, /* descriptor type */
0, /* header functional descriptor */
0x10,0x01,
4, /* sizeof(usbDescrCDC_AcmFn): length of descriptor in bytes */
0x24, /* descriptor type */
2, /* abstract control management functional descriptor */
0x02, /* SET_LINE_CODING, GET_LINE_CODING, SET_CONTROL_LINE_STATE */
5, /* sizeof(usbDescrCDC_UnionFn): length of descriptor in bytes */
0x24, /* descriptor type */
6, /* union functional descriptor */
0, /* CDC_COMM_INTF_ID */
1, /* CDC_DATA_INTF_ID */
5, /* sizeof(usbDescrCDC_CallMgtFn): length of descriptor in bytes */
0x24, /* descriptor type */
1, /* call management functional descriptor */
3, /* allow management on data interface, handles call management by itself */
1, /* CDC_DATA_INTF_ID */
/* Endpoint Descriptor */
7, /* sizeof(usbDescrEndpoint) */
USBDESCR_ENDPOINT, /* descriptor type = endpoint */
0x83, /* IN endpoint number 3 */
0x03, /* attrib: Interrupt endpoint */
8,0, /* maximum packet size */
2, /* polling interval in ms */
/* Second interface Descriptor */
9, /* sizeof(usbDescrInterface): length of descriptor in bytes */
USBDESCR_INTERFACE, /* descriptor type */
1, /* index of this interface */
0, /* alternate setting for this interface */
2, /* endpoints excl 0: number of endpoint descriptors to follow */
0x0A, /* Data Interface Class */
0, /* Data Interface Subclass */
0, /* Data Interface Class Protocol Codes */
0, /* string index for interface */
/* Endpoint Descriptor */
7, /* sizeof(usbDescrEndpoint) */
USBDESCR_ENDPOINT, /* descriptor type = endpoint */
0x01, /* OUT endpoint number 1 */
0x02, /* attrib: Bulk endpoint */
8,0, /* maximum packet size 8->6 */
0,
/* Endpoint Descriptor */
7, /* sizeof(usbDescrEndpoint) */
USBDESCR_ENDPOINT, /* descriptor type = endpoint */
0x81, /* IN endpoint number 1 */
0x02, /* attrib: Bulk endpoint */
8,0, /* maximum packet size */
0,
};
static uchar requestType;
static uchar modeBuffer[7];
static uchar intr3Status; /* used to control interrupt endpoint transmissions */
static uchar lastLen; // if 8, send an empty BULK packet
/***********************
* SETUP data received *
***********************/
uchar usbFunctionSetup(uchar data[8]) {
usbRequest_t *rq = (void*)data;
if ((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_CLASS) { /* class request type */
if (rq->bRequest==GET_LINE_CODING || rq->bRequest==SET_LINE_CODING) {
requestType = rq->bRequest;
return 0xff;
/* GET_LINE_CODING -> usbFunctionRead() */
/* SET_LINE_CODING -> usbFunctionWrite() */
}
if (rq->bRequest == SET_CONTROL_LINE_STATE) {
/* Report serial state (carrier detect). On several Unix platforms,
* tty devices can only be opened when carrier detect is set.
*/
if (!intr3Status) intr3Status=2;
}
/* Prepare bulk-in endpoint to respond to early termination */
if ((rq->bmRequestType & USBRQ_DIR_MASK) == USBRQ_DIR_HOST_TO_DEVICE)
lastLen=8; // send an empty packet
}
return 0;
}
/***********************
* IN transfer for EP0 *
***********************/
uchar usbFunctionRead(uchar *data, uchar len) {
if (requestType == GET_LINE_CODING) {
memcpy(data,modeBuffer,7);
return 7;
}
return 0; /* error -> terminate transfer */
}
/************************
* OUT transfer for EP0 *
************************/
uchar usbFunctionWrite(uchar *data, uchar len) {
if (requestType == SET_LINE_CODING) {
memcpy(modeBuffer,data,7);
return 1;
}
return 1; /* error -> accept everything until end */
}
/*****************************************
* Simple ring buffer for EP1IN transfer *
*****************************************/
typedef struct {
uchar rp,wp;
uchar buf[256];
}ringbuf_t;
ringbuf_t Ringbuf;
// Put datagram (message) into <Ringbuf>, returns !=0 when _not_ OK
uchar Ringbuf_Put(const uchar*data,uchar len) {
if (!len) return len;
if (len>(uchar)(Ringbuf.rp-Ringbuf.wp-1)) return len; // message does not fit
do{
Ringbuf.buf[Ringbuf.wp++]=*data++;
}while (--len);
return len; // message does fit
}
// Retrieve contiguous data from <Ringbuf>
// Returns number of bytes read
uchar Ringbuf_Get(uchar*data, uchar len) {
uchar ret=0;
for (;Ringbuf.rp!=Ringbuf.wp && len; len--) {
*data++=Ringbuf.buf[Ringbuf.rp++];
ret++;
}
return ret;
}
/************************************
* OUT transfer for EP1 (Bulk data) *
************************************/
static uchar rbuf; // memory for command byte
static uchar tbuf[60]; // multi-function buffer
static uchar DefPrefixByte; // 0x80 or 0x81
void usbFunctionWriteOut(uchar *data, uchar len) {
uchar c=rbuf,d;
if (!len) return;
Led_Start(150); // fast flashing (3 Hz)
do{
d=*data++;
if (!c) { // no command given
if (' '<d && d<0x7F) c=d; // ignore invisible characters as command
}else{ // command available
uchar *p=tbuf;
*p++=0x82;
if (c=='C') {
if (d=='L' || d=='R') {
uchar l;
ReadSig(); // Check for gloves now
if (d==sig_x1) CurI2c=0;
else if (d==sig_x2) CurI2c=1;
else{ // no EEPROM
*p++=d;
strcpy_P((char*)p,PSTR(" NO GLOVE"));
p+=9;
goto Out;
}
l=ReadEeprom(0,p,30/*sizeof(tbuf)-2*/);
if (l) do{ // EEPROM data available
uchar c=*p++;
if (c<0x20 || c>=0x7F) { // scan data until invalid character occurs
p--;
break;
}
}while(--l); else{ // no EEPROM, bridge only
*p++=d;
strcpy_P((char*)p,PSTR(" GLOVE"));
p+=6;
}
goto Out;
}
if (d=='P') {
strcpy_P((char*)p,
PSTR("V3.0B PINCH by Fakespace Inc. 1995 - haftmann#software"));
p+=54;
goto Out;
}
goto OutError;
}
if (c=='G' && (d=='0' || d=='1')) { // Report ground connection on/off
RepGnd=d-'0';
*p++=d;
goto Out;
}
if (c=='T' && (d=='0' || d=='1')) { // Report timestamp on/off
DefPrefixByte=d-'0'+0x80;
*p++=d;
goto Out;
}
OutError:
*p++='?';
Out:
*p++=0x8F;
Ringbuf_Put(tbuf,p-tbuf);
c=0; // delete pending command
}
}while(--len);
rbuf=c; // save valid command byte, expecting second byte
}
/*************************************************
* Making a connect/disconnect message for EP1IN *
*************************************************/
static unsigned Timestamp; // 14bit non-rollover counter
static void SendConnectMessage(void) {
uchar cnt=Sorted.N; // number of WORDs
uchar pre=DefPrefixByte; // Message introducer
uchar *p=tbuf; // pointer to universal buffer
*p++=pre;
if (cnt) {
if (SwapLR) { // copy while byte swapping
uchar *s=(uchar*)&Sorted.C+2;
do{
*p++=*--s; // AVR optimized
*p++=*--s;
s+=4;
}while(--cnt);
}else{
cnt<<=1;
memcpy(p,&Sorted.C,cnt); // copy directly
p+=cnt;
}
}
if (pre==0x81) {
*p++=Timestamp&0x7F;
*p++=Timestamp>>7;
}
*p++=0x8F; // Message terminator
if (!Ringbuf_Put(tbuf,p-tbuf)) Timestamp=0;
Led_Start(100); // Faster flashing (5 Hz)
}
/********************************
* Initialization and main loop *
********************************/
EEMEM uchar eeTstate, eeGstate; // EEPROM byte locations
static void hardwareInit(void) {
PORTB=PORTC=0x3F; // weak pullups everywhere (except USB data lines)
PORTD=0xF3;
DDRC=0x20; // LED as OUTPUT
DefPrefixByte=0x80; // answer without time stamp
ReadSig();
if (eeprom_read_byte(&eeTstate)==0x01) DefPrefixByte=0x81;
if (eeprom_read_byte(&eeGstate)==0x01) RepGnd=0x01;
}
// Updates an ATmega-internal EEPROM location when different
static void ActualizeEepromByte(uchar *ee_addr, uchar b) {
if (eeprom_read_byte(ee_addr)==b) return;
EEDR=b;
cli();
EECR|=1<<EEMWE;
EECR|=1<<EEWE; // don't wait here!
sei();
}
// Updates all internal EEPROM locations (here: "T" and "G" states)
// To be called repeatedly in mainloop - does not block
static void ActualizeEeprom(void) {
if (!eeprom_is_ready()) return;
ActualizeEepromByte(&eeTstate,DefPrefixByte-0x80);
if (!eeprom_is_ready()) return;
ActualizeEepromByte(&eeGstate,RepGnd);
}
int __attribute__((noreturn)) main(void) {
uchar SofCmp=0;
uchar mcucsr=MCUCSR;
MCUCSR=0;
ACSR|=0x80; // disable analog comparator and save 70µA
wdt_disable(); // watchdog survives RESET: disable it
if (mcucsr & (1 << WDRF)) {
if (USBIN & USBMASK) { // no SE0 state?
GICR=0x80;
MCUCR=0xA0; // SLEEP_MODE_PWR_DOWN and level interrupt for INT1
sei(); // must be enabled for wakeup
sleep_cpu(); // stop crystal and let INT1 do the next wakeup
cli();
}
}
MCUCR=0x80; // SLEEP_MODE_STANDBY
wdt_enable(WDTO_15MS); // no watchdog fuse here due to boot loader
usbInit();
sei();
hardwareInit();
for(;;){ // main event loop (infinite, possibly broken by watchdog)
uchar t,d;
if (USBIN&USBMASK) sleep_cpu(); // sleep, except at SE0, until SOF
t=usbSofCount; // atomic access to volatile variable
d=t-SofCmp; // time difference [ms]
if (d || (USBIN&USBMASK)) {
wdt_reset();
}
usbPoll();
if (d) { // After 1 ms (or maybe more)
SofCmp=t;
Timestamp+=d;
if (Timestamp&0x4000) Timestamp=0x3FFF; // increment without 14bit rollover
do Led_On1ms(); while(--d);
FindConnGroups();
if (StableConnGroups() && SortConnGroups()) {
if (Timestamp>=10000) ReadSig(); // read signature after long inactivity
SendConnectMessage();
}
}
/* device -> host */
if (usbInterruptIsReady()) {
uchar len;
len=Ringbuf_Get(tbuf,8);
if (len || lastLen&8 ) {
usbSetInterrupt(tbuf,len);
/* let send an empty block after last data block to indicate transfer end */
lastLen=len;
}
}
/* We need to report rx and tx carrier after open attempt */
if( usbInterruptIsReady3() && intr3Status ) {
static uchar serialStateNotification[10] = {
0xa1, 0x20, 0, 0, 0, 0, 2, 0, 3, 0};
if (intr3Status==2) usbSetInterrupt3(serialStateNotification, 8);
else usbSetInterrupt3(serialStateNotification+8, 2);
intr3Status--;
}
ActualizeEeprom();
}
}
/* Future features:
* Remote Wakeup: Contacting any (or specific) finger
will wake-up the connected PC/Laptop. Requires Watchdog use or ATmega88
* Touching a fixed TTL contact area: decoding info (as such as
"You have touched the knob 42 with the right hand middle finger.")
* HID device (to be more conform to USB standard
that forbids BULK for low-speed - less complicated Linux support)
*/
Detected encoding: UTF-8 | 0
|