// BLE_LED_TimerOff R4 // this sketch adds an Off timer to the light control // to Turn light Off after 8mins use OFF_TIME_MS setting below, default 0, no timeout // by default BLE control is enabled // This sketch needs the pfod_lp_nrf52_2023 Rev 11 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 // 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. */ #include #include #include #include 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 const char bleName[] = "BLE Light Switch"; // <<<<<<<< set your device name here lp_timer offTimer; const unsigned long OFF_TIME_MS = 0; //or 8ul * 60 * 1000; // 8mins, set to 0 to disable, max 8mins timer // DEBUG create serial on 11,12 //#define DEBUG // DEBUG_BLE need DEBUG defined and start BLE in setup //#define DEBUG_BLE int debugPin = 10; bool debugPinOn = false; void initializeDebugPin() { pinMode(debugPin, OUTPUT); digitalWrite(debugPin, LOW); debugPinOn = false; } void DebugPinOn() { digitalWrite(debugPin, HIGH); debugPinOn = true; } void DebugPinOff() { digitalWrite(debugPin, LOW); debugPinOn = false; } void toggleDebugPin() { if (debugPinOn) { DebugPinOff(); } else { DebugPinOn(); } } bool LightOn = true; // always powers up with light on, change this to false to switch off immediately after startup // it takes ~350ms 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 lightOnOff_pin = 7; // name the output pin for 'Switch Light Off' High to switch Off, Low for On const int zenerShort_pin = 6; // set high to short out zener when have enough voltage on capacitor, initially LOW to charge up capacitor bool ZenerOn = true; 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; // 10ms ADC intervals need this for blue blocker light to get Volts up when on (don't know why) const unsigned long adcTimeOn_us = 3000; // for actual adc timer needed for 60Hz 10ms ADC intervals need this for blue blocker light to get Volts up when on (don't know why) 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 = 195; //assume 560K + 56K//1M => 86.5e-3 ratio ADC range is 3V for 1023 so 195 => 6.61V for 560K + 56K => 6.29V const int voltsLowCount = 190; // //assume 560K + 56K//1M => 86.5e-3 ratio ADC range is 3V for 1023 so 168 => 6.44V for 560K + 56K => 6.13V unsigned long plot_msOffset = 0; // set by {@} response bool clearPlot = false; // set by the {@} response code lp_timer zenerOnTimer; uint32_t MIN_ZENER_PULSE_LEN = 156; 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_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() { if (LightOn) { turnZenerOff(); } else { turnZenerOn(); } } void pulseZenerOn(unsigned long zenerPulseLen_) { turnZenerOn(); zenerOnTimer.startDelay_us(zenerPulseLen_, handleZenerOnTimer); // zenerPulseLen in us here } /** Startup zener on time selection Wait for voltage to get down to voltsStartADCreadings == 168 Run for 100ms if count < voltsHighCount at end, increment zenerOnChoices_idx and repeat This checks that supply volts can recover in 1/10sec after BLE transmission drains capacitor. */ // switch debounce lp_timer delaySwitchReadTimer; const uint32_t DELAY_SWITCH_READ_MS = 1000; // ignore switch 1sec after power up to skip initial L/H reads // always get initial Low reading and then what ever the switch is currentl set to // so ignore these initial readings on power up, i.e. leave light in what ever LightOn was set at bool switchEnabled = false; // set true after DELAY_SWITCH_READ_MS int switchPin = 29; //AIN5 lp_timer debounceTimer; const uint32_t debounceTimeOut = 20; // ms int lastButtonPinState = -1; // not set initially int lastButtonState = -1; // not set initially int buttonState = -1; // not set initially // called when pin changes state, pinState is state detected, HIGH or LOW void handleSwitchPinLevelChange(int pinState) { if (pinState != lastButtonPinState) { // pin level changed from last one recorded lastButtonPinState = pinState; } // always start debounce on every pin change as may have dropped off some if the queue was full // if queue fills up the last place is ALWAYS filled with the most current pinState, so on debounce timeout the lastButtonPinState will be correct debounceTimer.stop(); // stop last timer if any debounceTimer.startDelay(debounceTimeOut, handleDebounceTimeout); } // button state has settled check if has changed void handleDebounceTimeout() { if (lastButtonPinState != buttonState) { // button state changed if (buttonState != -1) { // ignore first trigger on power up, otherwise toggle on each trigger after delay if (switchEnabled) { // otherwise ignore toggleLight(); } } buttonState = lastButtonPinState; // input has settled } } bool bleRunning = false; 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 toggleLight() { if (LightOn) { // turn off turnLightOff(); } else { // power off turn on turnLightOn(); } } void turnLightOff() { turnZenerOn(); digitalWrite(lightOnOff_pin, 1); // set output High ADC_OnTimer.stop(); // to conserve current consumption if (offTimer.isRunning()) { offTimer.stop(); // if light turned off, stop offTimer } LightOn = false; } void handleOffTimer() { turnLightOff(); } bool reachedHighOn = false; bool wasNotHigh = false; int initialOnADC_count = 1023; 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 turnLightOn() { if (!ADC_OnTimer.isRunning()) { ADC_OnTimer.startTimer_us(adcTimeOn_us, handleADCTimer); } if (OFF_TIME_MS > 0) { offTimer.startTimer_us(OFF_TIME_MS * MS_TO_US_SCALING, handleOffTimer); // turning light on start off timer } // setup for lightOn //turnZenerOff(); wasNotHigh = false; fallingOnCounter = MAX_FALLING_ON_CYCLE_COUNT; initialOnADC_count = 0; reachedHighOn = false; initializeZenerPulseLen(); //156us nextZenerStep(); // 312us pulseZenerOn(1000); // do a 1ms pulse now to handle startup digitalWrite(lightOnOff_pin, 0); // set output Low LightOn = true; } void handleZenerOffTimer() { turnZenerOn(); //stops timer turn zener back on } void turnZenerOn() { if (!ZenerOn) { digitalWrite(zenerShort_pin, 0); // set output low, re-enable zener ZenerOn = true; } } void turnZenerOff() { if (ZenerOn) { digitalWrite(zenerShort_pin, 1); // set output high to short out zener ZenerOn = false; } } int lowCounter = 0; const int maxLowCounter = 5; // this method is called on the loop() thread void handle_ADC_result(int adcCount) { 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) { 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); } void handleADCTimer() { uint32_t err = lp_ADC_start(voltsPin, handle_ADC_result); // pin30 } void handleADCcalibationTimer() { lp_ADC_calibrate(); // calibrate before next sample } void handleSwitchReadDelay() { switchEnabled = true; delaySwitchReadTimer.stop(); } // the setup routine runs once on reset: void setup() { #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(lightOnOff_pin, OUTPUT); // output for 'Switch Light On' is initially LOW, // now set initial power on state to match the LightOn setting at the top of this file delaySwitchReadTimer.startTimer_us(DELAY_SWITCH_READ_MS * MS_TO_US_SCALING, handleSwitchReadDelay); pinMode(switchPin, INPUT); // set compare pin with INPUT only as is hard wired to either VCC or GND by switch lp_comparator_start(switchPin, REF_8_16Vdd, handleSwitchPinLevelChange); // always triggers pin LOW first, then if pin HIGH, will trigger HIGH // 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_OnTimer.startTimer_us(adcTimeOn_us, handleADCTimer); pinMode(zenerShort_pin, OUTPUT); // output for 'Zener Short' digitalWrite(zenerShort_pin, 0); // set output is initially LOW i.e. zener enabled, ON ZenerOn = true; turnZenerOff(); // now turn it off low volts will turn it on again if (LightOn) { turnLightOn(); } else { turnLightOff(); } #ifdef DEBUG #ifdef DEBUG_BLE startBLE(); #endif #endif } // the loop routine runs over and over again forever: void loop() { sleep(); // wait here for a trigger // skip ble msg handling if not BLE #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 } // handle {@} request } else if ('@' == cmd) { // pfodApp requested 'current' time plot_msOffset = millis(); // capture current millis as offset rawdata timestamps clearPlot = true; // clear plot on reconnect as have new plot_msOffset parser.print(F("{@`0}")); // return `0 as 'current' raw data milliseconds // 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(); 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(); 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 toggleLight(); parser.print(F("{~Light is\n")); parser.print(LightOn ? "ON" : "OFF"); parser.print("}"); } else if ('!' == cmd) { // CloseConnection command closeConnection(parser.getPfodAppStream()); } else { // unknown command parser.print(F("{}")); // always send back a pfod msg otherwise pfodApp will disconnect. } } } #endif // #ifdef BLE } 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("`10000"); // request refresh menu every 10sec 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("`10000"); // request refresh menu every 10 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 =========