// BLE_LightSwitch_R15 // comment out #define BLE to disable BLE and just have off timer. #define BLE /* ===== pfod Command for Power Switch ==== pfodApp msg {.} --> {,<+9>~Light is ?`0~V5|A<+6>`0~Switch Light On~~\~t|B<+6>`0~Switch Light Off~~\~t} */ // Using pfod_lp_nrf52_2023 Rev 11 addon for Generic nRF52832 bare modules // Using Arduino V1.8.19 IDE /* Code generated by pfodDesignerV3 V3.0.4181 */ /* (c)2014-2021 Forward Computing and Control Pty. Ltd. NSW Australia, www.forward.com.au This code is not warranted to be fit for any purpose. You may only use it at your own risk. This generated code may be freely used for both private and commercial use provided this copyright is maintained. */ // Keep Zener short out (OFF) almost all of the time when lamp on so that when power on // current through forward biased zener does not overheat them. // Adjust supply zener On pulses, to charge capacitors, based on ADC readings // // When light ON check every 3ms (turn zener ON for short time) to see if light still ON (missing pulse detector shows lamp off) // When light OFF only check supply volts every 300ms to reduce current usage // // To protect against surge when turning ON, // disable Zener so that when turning on zener is shorted to start with // ADC reading while Off is long say 160ms // When On ADC reading is 3ms, Off ADC readings are 300ms // // These tests using 240VAC with the main Zener shorted out. // Turn off via relay switch. With 66ms ADC interval while off // A 25W incandescent bulb takes 4.3sec to recover supply volts after turn Off // A 75W incandescent bulb takes 3.6sec to recover supply volts after turn Off // A 10.5W led dimmable bulb takes 5.2sec to recover supply volts after turn Off // A 5W dimmable led bulb takes 6.4sec to recover supply volts after turn off // A 5W led non-dimmable (indivigual leds) does not recover // // When the light is turned ON, as detected by the lp_comparitor // A 1sec delay is added before turning the Zener on to recharge the supply capacitors. // That protects it from Zener from the Inrush Current of incandescent bulbs (~15x the steady state current) // Then the Zener on time is tuned starting from ~300us per 3ms and increasing/decreasing the on time until the // supply capacitor voltage rises over a full AC cycle. // This sketch needs the pfod_lp_nrf52_2022 add on to be installed and compile using Arduino V1.8.19 IDE // see https://www.forward.com.au/pfod/BLE/LowPower_2022/index.html #include #include #include #include // define this for 50Hz operation else assumes 60Hz #define BLE const uint32_t MS_TO_US_SCALING = 1000; // ms to us for lp_timer in us // download the pfodParser library V3.52+ from http://www.forward.com.au/pfod/pfodParserLibraries/index.html #include void turnZenerOn(); void turnZenerOff(); void turnLightOn(); void turnLightOff(); void handleADCTimer(); void handleADC_OffTimer(); const char bleName[] = "Garage Lights"; // <<<<<<<< set your device name here lp_timer offTimer; const unsigned long OFF_TIME_ms = 0; // 8ul * 60 * 1000; // 8mins, set to 0 to disable, max 8.5 mins (511sec) timer when lp_timer_us enabled in lp_timer_init.h lp_timer disableOffTimer; const unsigned long DISABLE_OFF_TIME_ms = 3000; // switch light OFF then ON within 3sec to disable off timer and keep light on bool msgRtn = false; lp_timer BLE_MessageDelay; // wait for LightOn/Off to stabalize after switching unsigned long MESSAGE_DELAY_ms = 250; // wait for LightOn to change state void handleBLE_MessageDelay() { msgRtn = true; //releases pfodApp sendMainMenuUpdate() } enum State {STARTING, CHECK_ONOFF, FIRST_SWITCH, RUNNING}; State state = STARTING; //#define TURN_BLE_ON // skips volts check bool bleRunning = false; // DEBUG create serial on 11,12 //#define DEBUG int debugPin = 10; bool debugPinOn = false; void initializeDebugPin() { pinMode(debugPin, OUTPUT); digitalWrite(debugPin, LOW); debugPinOn = false; } void DebugPinOn() { if (!debugPinOn) { debugPinOn = true; digitalWrite(debugPin, HIGH); } } void DebugPinOff() { if (debugPinOn) { debugPinOn = false; digitalWrite(debugPin, LOW); } } void toggleDebugPin() { if (debugPinOn) { DebugPinOff(); } else { DebugPinOn(); } } int powerOnPin = 29; //just read as input each adc time reads low if light ON volatile unsigned powerPinMissingTimer = 0; // count of missing highs for powerpin if none in 20ms than is off. bool LightOn = false; // assume light is off on power only start slow speed ADC_OffTimer initally // it takes ~500ms from 3V3 applied to any output set high, // so takes that long to turn off zener on startup // with 100uF 16V +/-20% (actually measured as 80uF) when light off, // volts varies from 6.9V down to 5.5V when sending short BLE msg int swap01(int); // method prototype for slider end swaps pfodParser parser("V1"); // create a parser to handle the pfod messages #ifdef BLE lp_BLESerial bleSerial; // create a BLE serial connection #endif // give the board pins names, if you change the pin number here you will change the pin controlled const int relayCoilSet_pin = 8; const int relayCoilReset_pin = 7; const int zenerShort_pin = 6; // set high to short out zener when have enough voltage on capacitor, initially LOW to charge up capacitor bool relayCoilSet = true; lp_timer RelayPulse_timer; unsigned long RELAY_PULSE_LENGTH = 21; // 0.020 secs Relay operate time ~7ms at 80% coil volts so 21ms is 3 times lp_timer AdcCalibration_timer; // this is for very low wattage LEDs to prevent the capacitor voltage getting too low const unsigned long ADC_CALIBRATION_TIMER_MS = 8UL * 60 * 1000; // 8mins in ms lp_timer ADC_OnTimer; const unsigned long adcTimeOn_ms = 3; const unsigned long adcTimeOn_us = 3000; lp_timer ADC_OffTimer; const unsigned long adcTimeOff_ms = 300; // 0.3sec const int voltsPin = 31; //AIN6 // ADC input resistance >1Mh, this shunts the 56K resistor down to >53K // with 560K + 56K divider 0.1uF -> 5ms time delay const int voltsHighCount = 264; //assume 560K + 56K//1M => 9.3V to 8.2V // do not switch relay OR send response if volts less than voltsHighCount const int voltsLowCount = 245; // //assume 560K + 56K//1M => 8.63V to 7.45V const uint32_t BLE_MSG_DELAY = 500; // 0.5sec int missingPulseCounter = 0; lp_timer zenerOnTimer; uint32_t MIN_ZENER_PULSE_LEN = 40;// rise and fall time ~0.8us using OUTPUT_H0H1 uint32_t MIN_ZENER_PULSE_DELAY_LEN = 160; // >= this use lp_timer else use delayMicroseconds uint32_t MAX_ZENER_PULSE_LEN = adcTimeOn_us - 100; // 9.8ms in 10ms adc uint32_t zenerPulseLen = MIN_ZENER_PULSE_LEN; // min us void initializeZenerPulseLen() { zenerPulseLen = MIN_ZENER_PULSE_DELAY_LEN; } bool nextQrtZenerStep() { // returns false if failed to increment i.e. was already on last step if (zenerPulseLen >= MAX_ZENER_PULSE_LEN) { return false; } uint32_t len = zenerPulseLen + (zenerPulseLen >> 2); //1 + 1/4 if (len > MAX_ZENER_PULSE_LEN) { len = MAX_ZENER_PULSE_LEN; } zenerPulseLen = len; return true; } bool nextHalfZenerStep() { // returns false if failed to increment i.e. was already on last step if (zenerPulseLen >= MAX_ZENER_PULSE_LEN) { return false; } uint32_t len = zenerPulseLen + (zenerPulseLen >> 1); //1 + 1/2 if (len > MAX_ZENER_PULSE_LEN) { len = MAX_ZENER_PULSE_LEN; } zenerPulseLen = len; return true; } bool nextZenerStep() { // returns false if failed to increment i.e. was already on last step if (zenerPulseLen >= MAX_ZENER_PULSE_LEN) { return false; } uint32_t len = zenerPulseLen << 1; //*2 if (len > MAX_ZENER_PULSE_LEN) { len = MAX_ZENER_PULSE_LEN; } zenerPulseLen = len; return true; } bool previousQrtZenerStep() { // returns false if failed to decrement i.e. was already on first step if (zenerPulseLen <= MIN_ZENER_PULSE_LEN) { return false; } uint32_t len = zenerPulseLen - (zenerPulseLen >> 2); // -1/4 step if (len < MIN_ZENER_PULSE_LEN) { len = MIN_ZENER_PULSE_LEN; } zenerPulseLen = len; return true; } bool previousHalfZenerStep() { // returns false if failed to decrement i.e. was already on first step if (zenerPulseLen <= MIN_ZENER_PULSE_LEN) { return false; } uint32_t len = zenerPulseLen - (zenerPulseLen >> 1); // -1/2 step if (len < MIN_ZENER_PULSE_LEN) { len = MIN_ZENER_PULSE_LEN; } zenerPulseLen = len; return true; } bool previousZenerStep() { // returns false if failed to decrement i.e. was already on first step if (zenerPulseLen <= MIN_ZENER_PULSE_LEN) { return false; } uint32_t len = zenerPulseLen >> 1; // /2 if (len < MIN_ZENER_PULSE_LEN) { len = MIN_ZENER_PULSE_LEN; } zenerPulseLen = len; return true; } int msToCheckFalling = 20; int zenerOnTest_step = 0; int initialCount = 1023; int maxADCreadings = 0; int numberOfADCreadings = 0; void handleZenerOnTimer() { turnZenerOff(); } void pulseZenerOn(unsigned long zenerPulseLen_) { if (zenerPulseLen_ >= MIN_ZENER_PULSE_DELAY_LEN) { turnZenerOn(); zenerOnTimer.startDelay_us(zenerPulseLen_, handleZenerOnTimer); // zenerPulseLen in us here } else { turnZenerOn(); delayMicroseconds(zenerPulseLen_); handleZenerOnTimer(); // turns zener off } } void startBLE() { #ifdef BLE // skip this if no BLE if (bleRunning) { return; } // set advertised name bleSerial.setName(bleName); // <<<<<<<< set your device name here // begin initialization bleSerial.begin(); parser.connect(&bleSerial); #endif // #ifdef BLE bleRunning = true; // always set this true even if no BLE, used in handle_ADC_result() } void handlePulseTimer() { digitalWrite(relayCoilSet_pin, LOW); digitalWrite(relayCoilReset_pin, LOW); } void toggleRelay() { if (RelayPulse_timer.isRunning()) { return; // still pulsing } RelayPulse_timer.startDelay(RELAY_PULSE_LENGTH, handlePulseTimer); relayCoilSet = !relayCoilSet; if (relayCoilSet) { digitalWrite(relayCoilSet_pin, HIGH); } else { digitalWrite(relayCoilReset_pin, HIGH); } } void toggleLight() { if (LightOn) { // turn off turnLightOff(); } else { // power off turn on turnLightOn(); } } // auto off timer timed out turn lamp off void handleOffTimer() { turnLightOff(); } bool lastIsOn = true; bool needToSendTurnOffResponse = false; int initialOnADC_count = 1023; bool wasNotHigh = false; bool reachedHighOn = false; int initialOffADC_count = 0; bool syncToggle = false; // relay toggle in sync bool startUpLightStateSet = false; unsigned int startupCounter = 0; bool lightOffNextADC = false; const int MAX_LOW_CYCLE_COUNT = 100 / adcTimeOn_ms + 1; // if < lowVoltCount for 100ms increase pulse width unsigned int lowOnCounter = 0; const int MAX_BETWEEN_CYCLE_COUNT = 250 / adcTimeOn_ms + 1; // if between low/high volt count for 250ms increase pulse width and have not increased pulse due to low unsigned int betweenOnCounter = 0; const int MAX_HIGH_CYCLE_COUNT = 500 / adcTimeOn_ms + 1; // if >= highVoltCount for 500ms decrease pulse width unsigned int highOnCounter = 0; uint32_t MAX_FALLING_ON_CYCLE_COUNT = 30 / adcTimeOn_ms + 1; // check for volts falling when turned on each 30ms, if falling increase pulse width uint32_t fallingOnCounter = 0; void setUpForLightOn() { // setup for lightOn wasNotHigh = false; fallingOnCounter = MAX_FALLING_ON_CYCLE_COUNT; initialOnADC_count = 0; reachedHighOn = false; initializeZenerPulseLen(); //156us nextZenerStep(); // 312us } void turnLightOn() { if (!LightOn) { toggleRelay(); } } void turnLightOff() { if (LightOn) { toggleRelay(); } } void turnZenerOn() { digitalWrite(zenerShort_pin, LOW); // set output low, re-enable zener } void turnZenerOff() { digitalWrite(zenerShort_pin, HIGH); // set output high to short out zener } /** state starts at STARTING and handleStateChanges() is then called when lamp Off, when volts above voltsHighCount not more often the 300ms and when lamp On called when volts above voltsHighCount for 500ms On power applied first call is either 300ms or 500ms after nRF52832 calls setup() state set to CHECK_ONOFF Next call either 300ms if off or 3ms if on, latches current lamp state and trys toggles relay from its default start up state of relayCoilSet == true i.e. tris to reset relay state set to FIRST_SWITCH Then when volts recovers, check current lamp on/off against startup LightOn_OnStartup if lamp on/off did not change then relay was already in reset nothing more to do relay synchronized with internal state else if lamp on/off changed, then relay was initially in set state, need to toggle back to set to restore initial power up state of lamp. state set to RUNNING */ bool LightOn_OnStartup = false; void handleStateChanges() { if (state == RUNNING) { return; } if (state == STARTING) { state = CHECK_ONOFF; return; } if (state == CHECK_ONOFF) { LightOn_OnStartup = LightOn; toggleRelay(); startBLE(); DebugPinOn(); state = FIRST_SWITCH; return; } if (state == FIRST_SWITCH) { if (LightOn != LightOn_OnStartup) { toggleRelay(); // switch back } DebugPinOff(); state = RUNNING; } } // only called when Light was Off void handle_ADC_Off_result(int adcCount) { if (adcCount >= voltsHighCount) { startBLE(); handleStateChanges(); } } // nothing to do here, test for timer running instead void handleDisableOffTimer() { // nothing here } int lowCounter = 0; const int maxLowCounter = 5; // this method is called on the loop() thread void handle_ADC_result(int adcCount) { missingPulseCounter++; // count 3ms times reset on each high pin change if (LightOn) { if (missingPulseCounter > 5) { // based on 3ms samples missingPulseCounter = 0; LightOn = false; if (!disableOffTimer.isRunning()) { // start disableOffTimer when lamp turned off (if not already running) disableOffTimer.startDelay(DISABLE_OFF_TIME_ms, handleDisableOffTimer); // if light turned off, stop offTimer } ADC_OnTimer.stop(); // stop high speed ADC (3ms) offTimer.stop(); // stop auto off timer } } if (adcCount >= voltsLowCount) { startBLE(); } if (!LightOn) { turnZenerOn(); lowCounter = 0; return; } if (!reachedHighOn) { // first recovery after turn on lowOnCounter = 0; betweenOnCounter = 0; if (adcCount <= voltsHighCount) { wasNotHigh = true; if (adcCount < initialOnADC_count) { fallingOnCounter++; if (fallingOnCounter >= MAX_FALLING_ON_CYCLE_COUNT) { fallingOnCounter = 0; nextZenerStep(); initialOnADC_count = adcCount; } } else { fallingOnCounter = 0; initialOnADC_count = adcCount; } } else { reachedHighOn = true; if (wasNotHigh) { previousZenerStep(); // skip this if syncing as incandecent globs warm up initialOnADC_count = 0; } wasNotHigh = false; } } else { // have reached high since was turned on if (adcCount < voltsLowCount) { highOnCounter = 0; lowOnCounter++; betweenOnCounter++; if (lowOnCounter >= MAX_LOW_CYCLE_COUNT) { lowOnCounter = 0; betweenOnCounter = 0; nextQrtZenerStep(); wasNotHigh = true; } } else if (adcCount >= voltsHighCount) { betweenOnCounter = 0; lowOnCounter = 0; //force delay on low volts before incrementing pulse highOnCounter++; if (highOnCounter >= MAX_HIGH_CYCLE_COUNT) { handleStateChanges(); highOnCounter = 0; // force increment on first < voltsLowCount previousQrtZenerStep(); } wasNotHigh = false; } else { lowOnCounter = 0; highOnCounter = 0; betweenOnCounter++; if (betweenOnCounter >= MAX_BETWEEN_CYCLE_COUNT) { betweenOnCounter = 0; nextQrtZenerStep(); wasNotHigh = true; } } } pulseZenerOn(zenerPulseLen); } // called when PowerOn pin changes state, pinState is state detected, HIGH or LOW // HIGH if current > 3.1ma (1K5 + 2K7 =>3.4V for 9.5V zener compared to (15/16 * 3V3) == 3.1V void handlePowerOnPinLevelChange(int pinState) { if (pinState == HIGH) { // DebugPinOn(); } else { // DebugPinOff(); } if (pinState == HIGH) { // will set light on after testing for disableOffTimer missingPulseCounter = 0; if (!ADC_OnTimer.isRunning()) { // start high speed ADC timer 3ms when detect lamp on, if not already running ADC_OnTimer.startTimer_us(adcTimeOn_us, handleADCTimer); } // if disable timer has timed out then engage normal auto off timer (if OFF_TIME_ms is non-zero) // otherwise lamp was switch off and then on again within 3sec so do not auto turn off as use has over-ridded that function if (!disableOffTimer.isRunning() && (!LightOn)) { // only test this if light was off if (OFF_TIME_ms > 0) { if (!offTimer.isRunning()) { offTimer.startDelay(OFF_TIME_ms, handleOffTimer); // if light turned off, stop offTimer } } } disableOffTimer.stop(); if (!LightOn) { //first pulse after lamp turned on setUpForLightOn(); } LightOn = true; // set on here } // ignore lows } // this is called every 0.3sec void handleADC_OffTimer() { if (LightOn) { return; // all ready sampling with ADC_OnTimer } // else LightOff turnZenerOn(); // need this for startup // sample supply volts to detect when volts above voltsHighCount uint32_t err = lp_ADC_start(voltsPin, handle_ADC_Off_result); // pin30 } // this is called every 3ms when lamp On // handle_ADC_result() adjusts zener On pulses to keep volts up but limit zener dissipation. void handleADCTimer() { uint32_t err = lp_ADC_start(voltsPin, handle_ADC_result); // pin30 } void handleADCcalibationTimer() { lp_ADC_calibrate(); // calibrate before next sample } /** Startup Default state is LightOn false (Off) Once the 3v3 supply starts, the zener is biased OFF Once the nRF52832 starts up (~500ms later) the zener is turned OFF and the ADC_OffTimer repeating timer is started. 300ms later the timer times out the first time and turns the zener ON If the lamp is actually Off then voltage once the voltage reaches the voltsHighCount the relay state synchronization is started If the lamp is actually On then with the Zener On the first reverse AC wave through the zener will trip the lp_comparitor and switch the LightOn to true and start the ADC_OnTimer repeating at 3ms */ // the setup routine runs once on reset: void setup() { initializeDebugPin(); // setup pin 10 for output #ifdef DEBUG // nothing printed in DEBUG mode in this sketch Serial.setPins(11, 12); // remap Rx to P0.11 and Tx to P0.12 Serial.begin(115200); #endif pinMode(zenerShort_pin, OUTPUT_H0H1); // output for 'Zener Short' Note High drive High and Low for better zener drive // 10mA drive => 0.01C/sec coulombs/sec 10e-3 C/sec for FET about 7.5nC charge at end of plateau => 0.75us turn on // 2mA drive => 3.5us turn on digitalWrite(zenerShort_pin, LOW); // set output is initially LOW i.e. zener enabled, ON turnZenerOff(); // now turn it off ADC_OffTimer will turn it on again in 0.3s pinMode(relayCoilSet_pin, OUTPUT); pinMode(relayCoilReset_pin, OUTPUT); handlePulseTimer(); // turns off both pins // initialize startup zenerOnCheck lp_ADC_calibrate(); // calibrate offset on first sample AdcCalibration_timer.startTimer_us(ADC_CALIBRATION_TIMER_MS * MS_TO_US_SCALING, handleADCcalibationTimer); ADC_OffTimer.startTimer(adcTimeOff_ms, handleADC_OffTimer); // 0.3 sec interval pinMode(powerOnPin, INPUT); // set compare pin with INPUT lp_comparator_start(powerOnPin, REF_15_16Vdd, handlePowerOnPinLevelChange); // 3.09V always triggers pin LOW first, then if pin HIGH, will trigger HIGH // clear off timers on startup disableOffTimer.stop(); DebugPinOff(); offTimer.stop(); state = STARTING; } void handleParser() { #ifdef BLE if (bleRunning) { // else handle BLE after startup delay // check ble serial when triggered by anything uint8_t cmd = parser.parse(); // parse incoming data from connection // parser returns non-zero when a pfod command is fully parsed if (cmd != 0) { // have parsed a complete msg { to } // turn the zener on for this msg uint8_t* pfodFirstArg = parser.getFirstArg(); // may point to \0 if no arguments in this msg. pfod_MAYBE_UNUSED(pfodFirstArg); // may not be used, just suppress warning long pfodLongRtn; // used for parsing long return arguments, if any pfod_MAYBE_UNUSED(pfodLongRtn); // may not be used, just suppress warning if ('.' == cmd) { // pfodApp has connected and sent {.} , it is asking for the main menu if (!parser.isRefresh()) { sendMainMenu(); // send back the menu designed } else { sendMainMenuUpdate(); // menu is cached just send update } // ignore handle {@} request // now handle commands returned from button/sliders } else if ('A' == cmd) { // user moved slider -- 'Switch Power On' // in the main Menu of Power Switch // set output based on slider 0= 1= turnLightOn(); BLE_MessageDelay.startDelay(MESSAGE_DELAY_ms, handleBLE_MessageDelay); // max adcInterval is 138ms, LigthOn/Off time constant is <100ms //msgRtn = true; set in handler to sendMainMenuUpdate(); // always send back a pfod msg otherwise pfodApp will disconnect. } else if ('B' == cmd) { // user moved slider -- 'Switch Power Off' // in the main Menu of Power Switch // set output based on slider 0= 1= turnLightOff(); BLE_MessageDelay.startDelay(MESSAGE_DELAY_ms, handleBLE_MessageDelay); // max adcInterval is 138ms, LigthOn/Off time constant is <100ms //msgRtn = true; set in handler to sendMainMenuUpdate(); // always send back a pfod msg otherwise pfodApp will disconnect. // add toggle cmd for single button remote control // does not appear on pfodApp menu } else if ('T' == cmd) { // toggle cmd this is NOT displayed in the pfodApp menu toggleRelay(); // ignore supply volts for this cmd just do it now BLE_MessageDelay.startDelay(MESSAGE_DELAY_ms, handleBLE_MessageDelay); // max adcInterval is 138ms, LigthOn/Off time constant is <100ms //msgRtn = true; set in handler to sendMainMenuUpdate(); // always send back a pfod msg otherwise pfodApp will disconnect. } else if ('!' == cmd) { // CloseConnection command closeConnection(parser.getPfodAppStream()); } else { // unknown command parser.print(F("{}")); // always send back a pfod msg otherwise pfodApp will disconnect. } } if ( msgRtn) { // have process turn on/off cmd msgRtn = false; sendMainMenuUpdate(); } } #endif // #ifdef BLE } // the loop routine runs over and over again forever: void loop() { sleep(); // wait here for a trigger handleParser(); } void closeConnection(Stream * io) { // add any special code here to force connection to be dropped #ifdef BLE ((lp_BLESerial*)io)->close(); #endif } void sendMainMenu() { // !! Remember to change the parser version string // every time you edit this method parser.print(F("{,")); // start a Menu screen pfod message // send menu background, format, prompt, refresh and version parser.print(F("<+9>~Light is\n")); parser.print(LightOn ? "ON" : "OFF"); parser.print("`5000"); // request refresh menu every 5sec parser.sendVersion(); // send the menu version // send menu items parser.print(F("|A<+6>")); parser.print(F("~Switch Light\nOn")); parser.print(F("|B<+6>")); parser.print(F("~Switch Light\nOff")); parser.print(F("}")); // close pfod message } void sendMainMenuUpdate() { parser.print(F("{;")); // start an Update Menu pfod message parser.print(F("<+9>~Light is\n")); parser.print(LightOn ? "ON" : "OFF"); parser.print("`5000"); // request refresh menu every 5 sec // send menu items parser.print(F("}")); // close pfod message // ============ end of menu =========== } int swap01(int in) { // not used in this code return (in == 0) ? 1 : 0; } // ============= end generated code =========