/* ISR-based reader for Neptune E-Coder water meter data. * - ISR notifies mainline of new message by setting * volatile int msgDone non-zero. * - For Uno/Duemilanove * - Expects data once/hr (as polled by R900 radio) * - Shortly before expected poll time, it reports any * noise interrupts the ISR saw. * - If notification is missed, reports counters from ISR. * - Uses approach from Bob Prust's 11 bit ascii Ecoder decoder. * - Expects inverters on both (clock, data) lines coming in. * (inverters like this: http://jimlaurwilliams.org/wordpress/wp-content/uploads/2012/09/MeterConnections.png) * - Usual color coding seems to be Green=gnd, Red=data, Black=clock * Added parity, stopbit, final ETB checks 10/31/19 * Cleaned up 2/3/20 jw */ //#include // just some short serial print aliases/macros #define spl Serial.println #define sp Serial.print #define sps Serial.print(" ") // DATAPIN can be whatever you choose #define DATAPIN (A0) // CLOCKPIN must be 2 or 3 for external interrupt #define CLOCKPIN (3) #define MAXCHARS (40) #define MSGOK (1) #define STOPERR (2) #define NOETB (3) #define PARITYERR (4) const char *retvals[]={"NONE","MSGOK","BADSTOP","NOETB","PARITYERR"}; // yeah, globals :( volatile char charBuf[MAXCHARS]; volatile int msgDone=0; //return flag from ISR volatile bool resetIsr=0; volatile int charNum=0,badEtbCnt=0,badStartCnt=0,intCnt=0; //========================================================= // This ISR is called on clock falling (after inverting buffer). // It manages the whole message from the water meter, and sets a // global flag when a message is available or on error. // Expects to be on an external hardware interrupt pin INT0/pin2 or INT1/pin3 // MSGLEN is the only way we know if we're done with a message #define MSGLEN 31 #define STX (2) #define ETX (3) #define ETB (23) // timing stuff based on pretty accurate once/hr poll from R900 #define ALMOSTTIME 3500000L #define PASTSLOP 20000L #define PASTTIME (3600000L + PASTSLOP) // number of 'noise' interrupts to ignore (mostly at end of last msg) #define IGNOREINT 15 //------- NEPTUNE READER ISR ---------- // Expects inverted data and clock // Sets msgDone >0 after getting MSGLEN chars // charBuf[] contains received chars incl parity bit void NeptuneISR(){ byte thischar; static int bitpos=0,wip=0,retval; // bitpos=which bit? Work In Progress word static unsigned long int lastclock=0; unsigned long int currmillis; bool mybit,badstart; intCnt++; // only used by mainline to see what's going on msgDone=0; //just in case // Complete reset (except intCnt badStartCnt, badEtbCnt if(resetIsr){ bitpos=0; wip=0; charNum=0; resetIsr=0; retval=0; // any errs get stored here }//end resetIsr // read the data bit mybit=!digitalRead(DATAPIN); // uninvert // check for start bit; only go on if start bit hi // bitpos==0 means looking for start bit // valid start bit on wire is low; after hw inverter and mybit=! above, still low badstart=((bitpos==0) && (mybit==1)); if(badstart){ // if bad start bit, just reset resetIsr=1; badStartCnt++; }else{ // all the normal bit processing here bitWrite(wip,bitpos,mybit); // nonfatal check for valid (first) stop bit if(bitpos==9 && mybit==0 && retval==0)retval=STOPERR; // bad stop bit // done with 11 bit word? if(++bitpos >10){ // mask off stops; shift right to dump start bit thischar=(byte)((wip&0x1ff)>>1); // check (even) parity here // parity trumps STOP, ETB // if mainline gets PARITYERR, I'd skip the message! if(__builtin_parity(thischar)){ // who knew this GCC builtin existed? retval=PARITYERR; // trumps STOPERR }// end if parity err // confirm 1st char STX or reset if(charNum==0 && ((thischar &0x7f) != STX)){ resetIsr=1; badStartCnt++;} else { // is OK char charBuf[charNum++]=thischar; // store the char with parity wip=0; //reset for next char bitpos=0; // last checks and flag done if(charNum>=MSGLEN){ if((thischar&0x7f)!= ETB && (retval==0)){ badEtbCnt++; retval=NOETB; } // end not ETB // return good unless anybody has found error msgDone=retval?retval:MSGOK; } // end got enough chars }//end else is OK char }//end if have 11 bits }// end else not bad start bit }//end ISR void setup() { Serial.begin(115200); spl("hello - waiting..."); pinMode(DATAPIN,INPUT_PULLUP); // enable internal pullups pinMode(CLOCKPIN,INPUT_PULLUP); // register our ISR attachInterrupt(digitalPinToInterrupt(CLOCKPIN),NeptuneISR,FALLING); // rising onwire; inverted }// end setup //========== MAIN LOOP ========== void loop() { byte bitnum,val; char outputStr[10]; int i=0,j,hours, minutes, seconds,nc; unsigned long int currmillis,lastnotif,starttime; bool didnoise=0; starttime=millis(); lastnotif=starttime; spl("in loop()"); while(1){ currmillis=millis(); //------- CHECK FOR NOISE ON INT LINE ---- if(!didnoise && millis()-lastnotif > ALMOSTTIME){ if(intCnt > IGNOREINT){ sp("Noise interrupts: "); spl(intCnt); didnoise=1; // so we only announce once } }// end if almost time //------ CHECK FOR MISSED NOTIFICATION------ // Doesn't deal gracefully with multiple missed notifications. if(millis()-lastnotif > PASTTIME){ int ic,bsc,bec; noInterrupts(); // correct, but almost certainly overkill ic=intCnt; bsc=badStartCnt; bec=badEtbCnt; interrupts(); sp("Missed notification: Ints:");sp(ic); sp(" Bad STX: ");sp(bsc); sp(" Bad ETB:");spl(bec); lastnotif=currmillis-PASTSLOP; // fake to expected last time clearCounters(); }// end if past time //----- HANDLE ISR NOTIFICATION ------ if(msgDone){ sp(retvals[msgDone]);spl(" returned by ISR"); nc=charNum; // it changes! Should just be MSGLEN based //----- CREATE USAGE STRING FROM E-CODER BUFFER --------- //200SW1234561234567890W78XYZ // data meter# data // Meter reading split into dumb parts. Above value=1234567.8 gal // 200SW is constant (S/W ver?) W,X,Y,Z not understood, not part of reading // Assumes initial is byte 0 in buffer. // Parity (even) handled in ISR. for(int i=7;i<13;i++)outputStr[i-7]=charBuf[i]&0x7f; // hi usage digits outputStr[6]=charBuf[27]&0x7f; //last digit of usage outputStr[7]='.'; outputStr[8]=charBuf[28]&0x7f; // decimal part of usage outputStr[9]='\0'; // string termination // Announce timing info sp((currmillis-lastnotif)/1000);spl(" sec since last poll"); hms((currmillis-starttime),&hours,&minutes,&seconds); sp(hours);sp(':');sp(minutes);sp(':');sp(seconds); spl(" since first started"); // Payload sp("Water usage: "); sp(outputStr);spl(" gal"); // some debugish prints #if 1 sp("charBuf:"); for(i=0;i