/* Arduino code for Whole House Telephone Intercom */ /* Version 1.1, June 13, 2009 - Improve off hook detection immunity to disturbances on Telco line */ /* Copyright (c) 2009, Joseph E. Doll */ /* You may use this code and derivative works for personal, non-profit applications. */ /* You may distribute copies without charge to others, so long as this copyright notice remains intact and modifications are clearly identified. */ /* You may not distribute this code, or any part or embodiment of it, in association with any product or service for which you charge money */ /* Original distribution point: http://joes.com/intercom/ATI10.txt (ATI = "Arduino Telephone Intercom") */ /* Code verified on Arduino Duemilanove with ATmega328 */ /* This version does not support Intercom/Hold mode */ /* Revision History */ /* Version 1.0, June 5, 2009 */ #include const int serialReport = true; // Set to false if serial reporting/debugging messages not desired const int kill90V = 2; // Shut off 90V supply pin const int intercom = 3; // Activate Intercom relay, connects local line to internal supply const int hold = 4; // Activate Hold relay, connects telco line to 100 ohm load const int add27mASense = 5; // Extra current to bring loop sense positive. Can be always on, or hook to +5V instead. const int add100VTelco = 6; // Extra current to bring telcoVSense positive. Can be always on, or hook to +5V instead. const int add100VLocal = 7; // Extra current to bring localVSense positive. Can be always on, or hook to +5V instead. const int intercomSense = 8; // Contact closure to ground when intercom relay active const int holdSense = 9; // Contact closure to ground when hold relay active const int hvTo90 = 10; // When low, raises high voltage to ~160 V. Leave low (voltage high) when safe (intercom off) to minimize power dissipation. const int led = 13; // Standard output pin for Arduino LED const int loopSense = 0; // Analog in pin, loop current through 4.7 ohms const int localVSense = 1; // Analog in pin, scaled value of local line voltage const int telcoVSense = 2; // Analog in pin, scaled value of telco line voltage const int P90VSense = 4; // Analog in pin, scaled value of 90V after limiting resistors const int ringFrequency = 20; // Intercom internal ringing frequency, Hz const int ringPattern[] = { 1000, 1000, 1000, 1000, 1000, 3000 }; // On/Off pattern for intercom ringing. 4194 ms max is limit of 16 bit timer 1 const long ringMaxTime = 120000; // Maximum time to ring before exiting intercom mode, in milliseconds. Set to 0 to ring forever. const int RINGING = 1; // Values for ringingStatus const int PULSING = 2; const int SILENT = 3; const int TERMINATED = 4; const int ONHOOK = 0; const int OFFHOOK = 1; const int minOffFlash = 200; // Minimum off hook time to count as off-flash (milliseconds) const int maxOffFlash = 2000; // Maximum off hook time to count as off-flash const int minOnFlash = 200; // Minimum on hook time to count as on-flash const int maxOnFlash = 2000; // Maximum on hook time to count as on-flash const int hookVThreshold = 250; // Corresponds to ~1.22V at localVSense. Expect ~1V off hook, 2V on hook with 90V applied int ringPatternLength = sizeof(ringPattern) / sizeof(int); //Number of elements in ringPattern int errorCode = 0; volatile int ringingStatus = false; volatile int ringPatternPosition = 0; // Pointer to current ringPattern[] element volatile unsigned int timerLoadValue; // For use in ISR // Variables used in ISR int pulseLevel; int pulseNumber; int ringLength; int numPulses; int pulseWidth; int localVoltage; int termConfirm = false; // Confirm off-hook ringing termination // Variables used in loop(): int loopReading; // For loopSense readings int loopCurrent; // Map loopReading to mA int hookStatus; // ONHOOK or OFFHOOK int prevHookStatus; // For detecting hook status change unsigned long lastOnHook; // For detecting flash periods unsigned long lastOffHook; // For detecting flash periods long offHookTime = 0; // For detecting flash periods long onHookTime = 0; // For detecting flash periods unsigned long lastRingStart = 0; int localV; // Local line voltage int telcoV; // Telephone line voltage int telcoReading; // ADC reading on telcoVSense int localReading; // ADC reading on localVSense int prevRingingStatus; // Debounce ringingStatus int hookDetector; // Debugging variable, determine where hook status detected int telcoJumper = false; // Set true in setup() if power supply is jumpered to Telco line (for debugging) // Prepare Timer1 for use as ring pattern generator void setupTimer1() { //Timer1 Settings: Timer Prescaler /1024, mode 0 (normal mode) //Timer clock = 16MHz/1024 = 15,625 Hz or 64us //Maximum overflow time = 65535/15625 = 4.19424 seconds TCCR1A = 0; TCCR1B = 1< */ TCNT1 = initTo10ms; /* Restore global interrupt flag */ SREG = sreg; interrupts(); } //Timer1 overflow interrupt vector handler ISR(TIMER1_OVF_vect) { char sreg; if ( !ringingStatus || ( ringingStatus == TERMINATED ) ) { // Monitor status every 10 ms /* Save global interrupt flag */ sreg = SREG; /* Disable interrupts */ noInterrupts(); /* Set TCNT1 to */ TCNT1 = 65379; // 10 ms to go: 65535 - 156 /* Restore global interrupt flag */ SREG = sreg; interrupts(); } else { // Manipulate kill90V per ringPattern[] and ringFrequency // Manipulate hvTo90 for maximum ring voltage during pulsing if ( ringingStatus == SILENT ) // Period between rings is complete, was set to SILENT on previous interrupt { ringingStatus = RINGING; ringPatternPosition++; if ( ringPatternPosition >= ringPatternLength ) { ringPatternPosition = 0; } } if ( ringingStatus == RINGING ) // Entering a new ring period { ringLength = ringPattern[ringPatternPosition]; numPulses = ringLength * ringFrequency / 1000; pulseWidth = ( ringLength / 2 ) / numPulses; // 50% duty cycle timerLoadValue = 65535 - ( pulseWidth * 15.625 ); // value to load for duration of 1/2 ringing cycle pulseLevel = LOW; pulseNumber = 0; ringingStatus = PULSING; digitalWrite(hvTo90, LOW); // Allow high voltage to rise to maximum during ring pulsing } if ( ringingStatus == PULSING ) { if ( pulseLevel == HIGH ) { pulseLevel = LOW; pulseNumber++; } else { pulseLevel = HIGH; // Check for low voltage on localVSense, indicating off-hook condition if ( pulseNumber > 0 ) // Avoid 1st pulse - experience shows low readings somtimes { localVoltage = analogRead(localVSense); if ( localVoltage < hookVThreshold ) // Intercom phone is off hook, stop ringing { // Check current first loopReading = analogRead(loopSense); loopCurrent = map( loopReading, 23, 46, 0, 27); //Measures 23 counts with add27mASense applied if ( loopCurrent >= 15 ) // Yep, really is off hook { if ( termConfirm ) // Require off hook detection on two successive pulses as glitch protection { ringingStatus = TERMINATED; digitalWrite(hvTo90, HIGH); digitalWrite(kill90V, LOW); timerLoadValue = 65379; // Check ringing status again in 10 mS termConfirm = false; } else { termConfirm = true; } } } else { termConfirm = false; } } } if ( ringingStatus && ( ringingStatus != TERMINATED ) ) // Not off hook, still ringing { digitalWrite(kill90V, pulseLevel); if ( pulseNumber >= numPulses ) // Ringing on-time is complete, begin silent period { ringingStatus = SILENT; digitalWrite(hvTo90, HIGH); // Minimize high voltage period ringPatternPosition++; timerLoadValue = 65535 - ( ringPattern[ringPatternPosition] * 15.625 ); } } } /* Save global interrupt flag */ sreg = SREG; /* Disable interrupts */ noInterrupts(); /* Set TCNT1 to */ TCNT1 = timerLoadValue; /* Restore global interrupt flag */ SREG = sreg; interrupts(); } } // End TIMER1_OVF_vect void setup() { pinMode(kill90V, OUTPUT); digitalWrite(hvTo90, HIGH); // Assume safe level in case jumper is present pinMode(hvTo90, OUTPUT); pinMode(hold, OUTPUT); pinMode(intercom, OUTPUT); pinMode(add27mASense, OUTPUT); pinMode(add100VLocal, OUTPUT); pinMode(add100VTelco, OUTPUT); pinMode(led, OUTPUT); digitalWrite(intercomSense, HIGH); // Activate pullup on intercom sense input digitalWrite(holdSense, HIGH); // Activate pullup on hold sense input digitalWrite(add27mASense, HIGH); // Use this line at all times digitalWrite(add100VLocal, HIGH); // And this one digitalWrite(add100VTelco, HIGH); // And this one if ( digitalRead(intercomSense) != HIGH ) { errorCode = 1; } else if ( digitalRead(holdSense) != HIGH ) { errorCode = 2; } telcoReading = analogRead(telcoVSense); telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied if ( telcoV > 50 ) // Telco line could be jumpered to internal power supply, test if it drops when internal supply killed { digitalWrite(kill90V, HIGH); delay(2); telcoReading = analogRead(telcoVSense); digitalWrite(kill90V, LOW); // Don't leave this on longer than necessary telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied if ( telcoV < 10 ) // Must be from internal supply { telcoJumper = true; } } if ( !telcoJumper ) { digitalWrite(hvTo90, LOW); // Minimize power dissipation for normal, unjumpered operation } //Announce startup by blinking LED 3 times for (int i=0; i < 3; i++) { digitalWrite(led, HIGH); delay(500); digitalWrite(led, LOW); delay(500); } setupTimer1(); if ( serialReport ) { Serial.begin(9600); Serial.print("Setup complete. Telco Jumper = "); Serial.print(telcoJumper); if ( digitalRead(hvTo90) ) Serial.println(". Power Supply 90V."); else Serial.println(". Power Supply High."); } } void loop() { // If error detected, flash errorCode value indefinitely if ( serialReport && errorCode ) { Serial.print("Error Code "); Serial.println(errorCode); } while ( errorCode ) { // Assure intercom is off, voltages to nominal ringingStatus = false; digitalWrite(intercom, LOW); digitalWrite(hold, LOW); delay(4); if ( ( digitalRead(intercomSense) == HIGH ) && ( telcoJumper == false ) ) // Intercom relay is released, no jumper { digitalWrite(hvTo90, LOW); // Minimize power dissipation } else { digitalWrite(hvTo90, HIGH); // Do not apply high voltage to attached phone } digitalWrite(kill90V, LOW); // Display error code for ( int i=0; i 15 ) { hookStatus = ONHOOK; hookDetector = 3; } else { hookStatus = prevHookStatus; hookDetector = 0; } } else // high loop current { if ( localV < 15 ) { hookStatus = OFFHOOK; hookDetector = 3; } else { hookStatus = prevHookStatus; hookDetector = 0; } } } else { delay(50); // Wait for ringing transients to dissipate } if ( ringingStatus != SILENT ) // Ringing status has changed, so discard (potenitally corrupted) hook status { hookStatus = prevHookStatus; hookDetector = 0; } if ( serialReport ) { localReading = analogRead(localVSense); localV = map( localReading, 197, 365, 0, 89 ); // Measures 197 with telcoVSense applied, 365 with 89V (and telcoVSense) applied telcoReading = analogRead(telcoVSense); telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied } } else // Line in steady state { loopReading = analogRead(loopSense); loopCurrent = map( loopReading, 23, 46, 0, 27); //Measures 23 counts with add27mASense applied if ( digitalRead(intercom) ) { localReading = analogRead(localVSense); localV = map( localReading, 197, 365, 0, 89 ); // Measures 197 with telcoVSense applied, 365 with 89V (and telcoVSense) applied if ( loopCurrent < 15 ) { if ( localV > 15 ) { hookStatus = ONHOOK; hookDetector = 4; } else { hookStatus = prevHookStatus; hookDetector = 0; } } else // high loop current { if ( localV < 15 ) { hookStatus = OFFHOOK; hookDetector = 4; } else { hookStatus = prevHookStatus; hookDetector = 0; } } if ( serialReport ) { telcoReading = analogRead(telcoVSense); telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied } } else // intercom mode off { if ( ( loopCurrent < 15 ) && ( loopCurrent > -15 ) ) { telcoReading = analogRead(telcoVSense); telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied if ( telcoV > 15 || telcoV < -15 ) // Voltage is high { hookStatus = ONHOOK; hookDetector = 6; } else { hookStatus = prevHookStatus; } if ( serialReport ) { localReading = analogRead(localVSense); localV = map( localReading, 197, 365, 0, 89 ); // Measures 197 with telcoVSense applied, 365 with 89V (and telcoVSense) applied telcoReading = analogRead(telcoVSense); telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied } } else // Could be off hook, but assure voltage is low so current is not due to telco ringing { delay(2); // Reconfirm loop current with averaged value - could be from Telco line noise or audio loopReading = analogRead(loopSense); int loopReadings[31]; for ( int i = 0; i < 31; i++ ) { delay(1); loopReadings[i] = analogRead(loopSense); loopReading = loopReading + loopReadings[i]; } loopReading = loopReading / 32; loopCurrent = map( loopReading, 23, 46, 0, 27); //Measures 23 counts with add27mASense applied telcoReading = analogRead(telcoVSense); telcoV = map( telcoReading, 203, 370, 0, 89 ); // Measures 203 counts with add100VTelco applied, 370 counts with 89V applied if ( ( telcoV > 15 ) || ( telcoV < -15 ) ) // Voltage is high { hookStatus = ONHOOK; hookDetector = 7; } else if ( ( ( telcoV > 0 ) && ( loopCurrent > 15 ) ) || ( ( telcoV < 0 ) && ( loopCurrent < -15 ) ) ) // Current, voltage polarities match { hookStatus = OFFHOOK; hookDetector = 7; } else { hookStatus = prevHookStatus; hookDetector = 0; } if ( serialReport ) { localReading = analogRead(localVSense); localV = map( localReading, 197, 365, 0, 89 ); // Measures 197 with telcoVSense applied, 365 with 89V (and telcoVSense) applied } } } } if ( hookStatus != prevHookStatus ) { if ( hookStatus == ONHOOK ) { lastOnHook = millis(); if ( serialReport ) { Serial.print("On Hook @ "); Serial.print(hookDetector); } digitalWrite(led, HIGH); delay(10); digitalWrite(led, LOW); if ( lastOnHook < lastOffHook ) // timer rollover has occurred { /* This is illegal. lastOffHook must be unsigned long, cannot take negative value lastOffHook -= 4294967295; So do the best we can: */ lastOffHook = 0; } offHookTime = lastOnHook - lastOffHook; if ( serialReport ) { Serial.print(", offHookTime = "); Serial.print(offHookTime); Serial.print(", "); Serial.print(loopCurrent); Serial.print("mA, localV = "); Serial.print(localV); Serial.print(", telcoV = "); Serial.print(telcoV); Serial.print(", ringingStatus = "); Serial.print(ringingStatus); Serial.print(", prevRingingStatus = "); Serial.println(prevRingingStatus); } if ( offHookTime > minOffFlash && offHookTime < maxOffFlash ) { offFlashDetected = true; } } else // hookStatus == OFFHOOK { lastOffHook = millis(); // Flash LED twice for off hook event digitalWrite(led, HIGH); delay(10); digitalWrite(led, LOW); delay(50); digitalWrite(led, HIGH); delay(10); digitalWrite(led, LOW); if ( lastOffHook < lastOnHook ) // timer rollover has occurred { /* This is illegal. lastOnHook must be unsigned long, cannot take negative value lastOnHook -= 4294967295; So do the best we can: */ lastOnHook = 0; } onHookTime = lastOffHook - lastOnHook; if ( serialReport ) { Serial.print("Off Hook @ "); Serial.print(hookDetector); Serial.print(", onHookTime = "); Serial.print(onHookTime); Serial.print(", "); Serial.print(loopCurrent); Serial.print("mA, localV = "); Serial.print(localV); Serial.print(", telcoV = "); Serial.print(telcoV); Serial.print(", ringingStatus = "); Serial.print(ringingStatus); Serial.print(", prevRingingStatus = "); Serial.println(prevRingingStatus); } if ( onHookTime > minOnFlash && onHookTime < maxOnFlash ) { onFlashDetected = true; } } if ( offFlashDetected && !digitalRead(intercom) ) { // ** Turn on Intercom digitalWrite(hvTo90, HIGH); // Assure power supply not at high level when intercom on delay(2); // Wait for hi V to reduce digitalWrite(intercom, HIGH); ringingStatus = RINGING; digitalWrite(kill90V, LOW); lastRingStart = millis(); delay(4); // Assure intercom relay has time to pull in if ( digitalRead(intercomSense) != LOW ) { errorCode = 3; } if ( serialReport ) { Serial.print("Intercom on. "); Serial.println("Power Supply 90V./Ringing"); } } else if ( hookStatus == ONHOOK && digitalRead(intercom) ) { // ** Turn off Intercom digitalWrite(intercom, LOW); ringingStatus = false; digitalWrite(hvTo90, HIGH); // Assure boosted high voltage is off digitalWrite(kill90V, LOW); // And 90V not turned off ringPatternPosition = 0; delay(4); // Assure intercom relay has time to release if ( digitalRead(intercomSense) != HIGH ) { errorCode = 4; } else if ( telcoJumper == false ) { digitalWrite(hvTo90, LOW); // Minimize power dissipation while intercom is inactive } if ( serialReport ) { Serial.print("Intercom off. "); if ( digitalRead(hvTo90) ) Serial.println("Power Supply 90V."); else Serial.println("Power Supply High."); } } if ( ( hookStatus == OFFHOOK ) && digitalRead(intercom) ) { // Intercom to talk mode ringingStatus = false; digitalWrite(hvTo90, HIGH); // Assure boosted high voltage is off digitalWrite(kill90V, LOW); // And 90V not turned off ringPatternPosition = 0; if ( serialReport ) { Serial.print("Intercom to talk mode. "); if ( digitalRead(hvTo90) ) Serial.println("Power Supply 90V."); else Serial.println("Power Supply High."); } } } if ( ringingStatus && ( ringMaxTime > 0 ) ) { unsigned long now = millis(); if ( now < lastRingStart ) // rollover has occurred { lastRingStart = 0; // fix lastRingStart value as best we can } if ( ( now - lastRingStart ) > ringMaxTime ) // Cease ringing and abandon intercom mode { digitalWrite(intercom, LOW); ringingStatus = false; digitalWrite(hvTo90, HIGH); // Assure boosted high voltage is off digitalWrite(kill90V, LOW); // And 90V not turned off ringPatternPosition = 0; delay(4); // Assure intercom relay has time to release if ( digitalRead(intercomSense) != HIGH ) { errorCode = 5; } else if ( telcoJumper == false ) { digitalWrite(hvTo90, LOW); // Minimize power dissipation while intercom is inactive } if ( serialReport ) { Serial.print("Intercom off - ringing timeout. "); if ( digitalRead(hvTo90) ) Serial.println("Power Supply 90V."); else Serial.println("Power Supply High."); } } loopReading = analogRead(loopSense); loopCurrent = map( loopReading, 23, 46, 0, 27); //Measures 23 counts with add27mASense applied } // Potential future exercise: implement Hold mode if ( serialReport ) { delay(5); // Missing ringingStatus prints without this if ( ringingStatus != prevRingingStatus ) { if ( ringingStatus == TERMINATED ) { Serial.print("TERMINATED<-"); Serial.println(prevRingingStatus); } else if ( ringingStatus == false ) { Serial.print("NOT RINGING<-"); Serial.println(prevRingingStatus); } else if ( ringingStatus == PULSING ) { Serial.print("PULSING<-"); Serial.println(prevRingingStatus); } else if ( ringingStatus == SILENT ) { Serial.print("SILENT<-"); Serial.println(prevRingingStatus); } else { Serial.print("ringingStatus = "); Serial.print(ringingStatus); Serial.print("<-"); Serial.println(prevRingingStatus); } } } prevHookStatus = hookStatus; prevRingingStatus = ringingStatus; }