Some notes/question on the OCS - Interlock, Single-button openers/closers


pbkoden@...
 

First, thank you Howard for developing this software. I really appreciate your work on this.

I loaded the OCS up today onto a test breadboard to simulate a ROR with open/close sensors, an interlock switch, and an LED to stand-in for an open/close relay. I want to make sure I have it somewhat functional before I take it out and put it up live in the observatory. In my testing I have found a few things that confused me. I think I have the bugs all worked out now, but wanted to run the things by the group.

1) The interlock is only active on closing the roof. I plan to use the interlock to detect my scope in the home position, which is needed to safely open OR close the roof. I added the same check under the opening code, which seems to work well. Any reason it is currently only checked for closing? I guess in theory the scope shouldn't be moving at all if the roof is closed, but I just want that extra layer of safety.

2) The interlock is checked during the closing process, but not before initiating closing. This caused my closing relay to trigger very briefly before the interlock error popped up. I'm using a gate opener, and the brief closure of the relay would be enough to open or close it. To prevent this, I added an interlock check before initiating gate closing or opening (under startRoofOpen() and startRoofClosed()). This way, it never gets to the relay activation point. I would recommend this as an additional safety checkpoint

3) My gate opener has a single button for open and close, like a garage door opener. I figured I would just set the same relay number under the Config.h file under both open and close relays. This seemed to cause some issues, with the closing state not triggering properly during testing. I found using two separate relay outputs (even if the wires are spliced together) seems to fix the issue.

Thanks!
Phil Brewer


Howard Dutton
 
Edited

On Thu, Jul 9, 2020 at 09:58 AM, <pbkoden@...> wrote:
1) The interlock is only active on closing the roof. I plan to use the interlock to detect my scope in the home position, which is needed to safely open OR close the roof. I added the same check under the opening code, which seems to work well. Any reason it is currently only checked for closing? I guess in theory the scope shouldn't be moving at all if the roof is closed, but I just want that extra layer of safety.
I figured close only would be sufficient.

2) The interlock is checked during the closing process, but not before initiating closing. This caused my closing relay to trigger very briefly before the interlock error popped up. I'm using a gate opener, and the brief closure of the relay would be enough to open or close it. To prevent this, I added an interlock check before initiating gate closing or opening (under startRoofOpen() and startRoofClosed()). This way, it never gets to the relay activation point. I would recommend this as an additional safety checkpoint
Ok, as above.  Agree with the concept and it's a good enhancement to the functionality.  Also, the idea here is the ROR_POWER_RELAY will "unplug" the garage door opener if the interlock is tripped.  So pressing the close button isn't going keep the roof moving for very long.

3) My gate opener has a single button for open and close, like a garage door opener. I figured I would just set the same relay number under the Config.h file under both open and close relays. This seemed to cause some issues, with the closing state not triggering properly during testing. I found using two separate relay outputs (even if the wires are spliced together) seems to fix the issue.
That was my intention.

If you could describe exactly how to trigger the issue I will investigate.


Howard Dutton
 

Here's a Roof.ino with the interlock changes.  If you could test/report back that would be great.

I implemented this so the roof power relay can be used to power up the interlock system (a design goal.)  And also so that when opening even after the roof is in motion the interlock can still be triggered...

// -----------------------------------------------------------------------------------------------------------------
// Roof control functions
// ======= add your roof support code here =======

#if ROR == ON

// roof status and errors
volatile char roofState = 'i';
// bit 7 = not used, reserved
// bit 6 = open roof failed with closed limit switch failure to disengage
// bit 5 = open roof failed with over time
// bit 4 = open roof failed with under time
// bit 3 = not used, reserved
// bit 2 = close roof failed with open limit switch failure to disengage
// bit 1 = close roof failed with over time
// bit 0 = close roof failed with under time
uint16_t roofStatusRegister=0;
String roofLastError="";

// roof power and safety
volatile boolean roofSafetyOverride = false;
volatile boolean roofMaxPower = false;
volatile int     roofCurrentPower = 0;

// roof timing and travel
const long roofTimeAvg        = (long)(ROR_TIME_AVG)*1000L;
const long roofTimeErrorLimit = (long)(ROR_TIME_TOL)*1000L;
long lastSecondsOfTravel,roofOpenStartTime,timeLeftToOpenAtStart,roofCloseStartTime,timeLeftToCloseAtStart;
long roofTravel=0;

// this gets called once on startup to initialize roof operation (required)
void roofInit() {
}

// called repeatedly if roof is moving (required)
void moveRoof() {
  // Open the roof, keeping track of time limit and sensor status
  if (roofIsOpening()) continueOpeningRoof();

  // Close the roof, keeping track of time limit and sensor status
  if (roofIsClosing()) continueClosingRoof();
}

// called repeatedly to open the roof
void continueOpeningRoof() {
    cli(); long msOfTravel=((long)millis()-roofOpenStartTime); sei();

#if ROR_POWER_PWM_SOFTSTART == ON
    if (roofCurrentPower < ROR_POWER_PWM_POWER) {
      cli(); roofCurrentPower=msOfTravel/200; if (roofCurrentPower > ROR_POWER_PWM_POWER) roofCurrentPower=ROR_POWER_PWM_POWER; sei();
    }
#endif

    // calculate how far we are from opening and closing the roof right now
    long timeLeftToOpenNow=timeLeftToOpenAtStart-msOfTravel;
    if (timeLeftToOpenNow<0) { timeLeftToOpenNow=0; }
    long timeLeftToCloseNow=roofTimeAvg-timeLeftToOpenNow;

    // keep track of where we are (to the nearest five seconds)
    long secondsOfTravel=round(msOfTravel/5000)*5000;
    if (lastSecondsOfTravel!=secondsOfTravel) {
      lastSecondsOfTravel=secondsOfTravel;
      EEPROM_writeLong(EE_timeLeftToOpen,timeLeftToOpenNow);
      EEPROM_writeLong(EE_timeLeftToClose,timeLeftToCloseNow);
    }

    // Or a stuck limit switch
    if ((!roofSafetyOverride) && (((roofTimeAvg-timeLeftToOpenNow)>ROR_TIME_LIMIT_SENSE_FAIL*1000) && senseIsOn(ROR_SENSE_LIMIT_CLOSED))) {
      // Set the error in the status register, the user can resume the opening operation by checking for any malfunction then using the safety override if required
      roofStatusRegister=roofStatusRegister|0b01000000; // 64
      // Go idle
      roofState='i';
    }

    // Or interlock was triggered
    if (ROR_SENSE_INTERLOCK != OFF && !senseIsOn(ROR_SENSE_INTERLOCK)) {
      // Set the error in the status register, the user can resume the opening operation by checking for any malfunction then using the safety override if required
      roofStatusRegister=roofStatusRegister|0b1000000000; // 512
      // Go idle
      roofState='i';
    }

    // Or the whole process taking too long
    if ((!roofSafetyOverride) && ((timeLeftToOpenAtStart-msOfTravel)<-roofTimeErrorLimit)) {
      // Set the error in the status register, the user can resume the opening operation by checking for any malfunction then using the safety override if required
      roofStatusRegister=roofStatusRegister|0b00100000; // 32
      // Go idle
      roofState='i';
    }

    // Calculate a percentage of roof travel
    roofTravel=((double)(roofTimeAvg-(timeLeftToOpenAtStart-msOfTravel))/(double)roofTimeAvg)*100;

    // Detect that the roof has finished opening
    if (senseIsOn(ROR_SENSE_LIMIT_OPENED)) {
      // wait for two seconds before powering off the roof drive (for automatic opener that stops itself)
      if (ROR_MOTOR_RELAY_MOMENTARY != OFF) delay(2000);
      // reset position timers
      EEPROM_writeLong(EE_timeLeftToOpen,0);
      EEPROM_writeLong(EE_timeLeftToClose,roofTimeAvg);
      // Go idle
      roofState='i';
    }

    // Finished opening? stop the motion and clear state
    if (roofState=='i') {
      // Stop the roof motor (redundant for momentary control)
      setRelayOff(ROR_MOTOR_OPEN_RELAY);
      // Reset possible override of roof timer
      roofSafetyOverride=false;
      // Reset roof power to normal level
      roofMaxPower=false;
    }
}

// called repeatedly to close the roof
void continueClosingRoof() {
    cli(); long msOfTravel=(long)millis()-roofCloseStartTime; sei();

#if ROR_POWER_PWM_SOFTSTART == ON
    if (roofCurrentPower < ROR_POWER_PWM_POWER) {
      cli(); roofCurrentPower=msOfTravel/200; if (roofCurrentPower > ROR_POWER_PWM_POWER) roofCurrentPower=ROR_POWER_PWM_POWER; sei();
    }
#endif
   
    // calculate how far we are from opening and closing the roof right now
    long timeLeftToCloseNow=timeLeftToCloseAtStart-msOfTravel;
    if (timeLeftToCloseNow<0) { timeLeftToCloseNow=0; }
    long timeLeftToOpenNow=roofTimeAvg-timeLeftToCloseNow;

    // keep track of where we are, if power goes out, for example
    long secondsOfTravel=round(msOfTravel/5000)*5000;
    if (lastSecondsOfTravel!=secondsOfTravel) {
      lastSecondsOfTravel=secondsOfTravel;
      EEPROM_writeLong(EE_timeLeftToOpen,timeLeftToOpenNow);
      EEPROM_writeLong(EE_timeLeftToClose,timeLeftToCloseNow);
    }

    // Or a stuck limit switch
    if ((!roofSafetyOverride) && (((roofTimeAvg-timeLeftToCloseNow)>ROR_TIME_LIMIT_SENSE_FAIL*1000) && senseIsOn(ROR_SENSE_LIMIT_OPENED))) {
      // Set the error in the status register, the user can resume the closing operation by checking for any malfunction then using the safety override if required
      roofStatusRegister=roofStatusRegister|0b00000100; // 4
      // Go idle
      roofState='i';
    }

    // Or interlock was triggered
    if (ROR_SENSE_INTERLOCK != OFF && !senseIsOn(ROR_SENSE_INTERLOCK)) {
      // Set the error in the status register, the user can resume the closing operation by checking for any malfunction then using the safety override if required
      roofStatusRegister=roofStatusRegister|0b100000000; // 256
      // Go idle
      roofState='i';
    }

    // Or the whole process is taking too long
    if ((!roofSafetyOverride) && ((timeLeftToCloseAtStart-msOfTravel)<-roofTimeErrorLimit)) {
      // Set the error in the status register, the user can resume the closing operation by checking for any malfunction then using the safety override if required
      roofStatusRegister=roofStatusRegister|0b00000010; // 2
      // Go idle
      roofState='i';
    }

    // Calculate a percentage of roof travel
    roofTravel=((double)(roofTimeAvg-(timeLeftToCloseAtStart-msOfTravel))/(double)roofTimeAvg)*100;

    // Detect that the roof has finished closing
    if (senseIsOn(ROR_SENSE_LIMIT_CLOSED)) {
      // wait for two seconds before powering off the roof drive (for automatic opener that stops itself)
      if (ROR_MOTOR_RELAY_MOMENTARY != OFF) delay(2000);
      // reset position timers
      EEPROM_writeLong(EE_timeLeftToOpen,roofTimeAvg);
      EEPROM_writeLong(EE_timeLeftToClose,0);
      // Go idle
      roofState='i';
    }
   
    // Finished closing? stop the motion and clear state
    if (roofState=='i') {
      // Stop the roof motor (redundant for momentary control)
      setRelayOff(ROR_MOTOR_CLOSE_RELAY);
      // Reset possible override of roof timer
      roofSafetyOverride=false;
      // Reset roof power to normal level
      roofMaxPower=false;
    }
}

// Start opening the roof, returns true if successful or false otherwise (required)
bool startRoofOpen() {
  if (roofState != 'i' || relayIsOn(ROR_MOTOR_OPEN_RELAY) || relayIsOn(ROR_MOTOR_CLOSE_RELAY)) { roofLastError="Error: Open already in motion"; return false; }

  // Handle case of Garage door opener where we're not sure which way it'll move
  if (!roofSafetyOverride && ROR_SINGLE_OPEN_CLOSE_RELAY == ON && !senseIsOn(ROR_SENSE_LIMIT_OPENED) && !senseIsOn(ROR_SENSE_LIMIT_CLOSED)) { roofLastError="Error: Motion direction unknown"; return false; }
 
  // Figure out where the roof is right now best as we can tell...
  // Check for limit switch and reset times
  if (senseIsOn(ROR_SENSE_LIMIT_CLOSED)) { EEPROM_writeLong(EE_timeLeftToOpen,roofTimeAvg); EEPROM_writeLong(EE_timeLeftToClose,0); }
  if (senseIsOn(ROR_SENSE_LIMIT_OPENED)) { EEPROM_writeLong(EE_timeLeftToOpen,0); EEPROM_writeLong(EE_timeLeftToClose,roofTimeAvg); }
  timeLeftToOpenAtStart =EEPROM_readLong(EE_timeLeftToOpen);
  timeLeftToCloseAtStart=EEPROM_readLong(EE_timeLeftToClose);

  // Check for validity of roof position timers before starting (they need to be within +/- 2 seconds)
  if (!roofSafetyOverride && (abs((timeLeftToOpenAtStart+timeLeftToCloseAtStart)-roofTimeAvg)>2000)) { roofLastError="Error: Open location unknown"; return false; }

  // Check to see if the roof is already opened
  if (senseIsOn(ROR_SENSE_LIMIT_OPENED) && !senseIsOn(ROR_SENSE_LIMIT_CLOSED)) { roofLastError="Warning: Already open"; return false; }

  // Just one last sanity check before we start moving the roof
  if (senseIsOn(ROR_SENSE_LIMIT_OPENED) && senseIsOn(ROR_SENSE_LIMIT_CLOSED)) { roofLastError="Error: Opened/closed limit sw on"; return false; }

  // Flag status, no errors
  roofState='o';
  roofStatusRegister=0;
  delay(2000);

  if (ROR_SENSE_INTERLOCK != OFF && !senseIsOn(ROR_SENSE_INTERLOCK)) { roofState='i'; roofLastError="Error: Open safety interlock"; return false; }

  // Set relay/MOSFET
  if (ROR_MOTOR_RELAY_MOMENTARY != OFF) {
    setRelayOnDelayedOff(ROR_MOTOR_OPEN_RELAY,2);
  } else {
    setRelayOff(ROR_MOTOR_CLOSE_RELAY);
    setRelayOn(ROR_MOTOR_OPEN_RELAY);
  }

  // Log start time
  roofOpenStartTime=(long)millis();

#if ROR_POWER_PWM_SOFTSTART == ON
  roofCurrentPower=0;
#else
  roofCurrentPower=ROR_POWER_PWM_POWER;
#endif

  roofLastError="";
  return true;
}

// Start closing the roof, returns true if successful or false otherwise (required)
bool startRoofClose() {
  if (roofState != 'i' || relayIsOn(ROR_MOTOR_OPEN_RELAY) || relayIsOn(ROR_MOTOR_CLOSE_RELAY)) { roofLastError="Error: Close already in motion"; return false; }

  // Handle case of Garage door opener where we're not sure which way it'll move
  if (!roofSafetyOverride && ROR_SINGLE_OPEN_CLOSE_RELAY == ON && !senseIsOn(ROR_SENSE_LIMIT_OPENED) && !senseIsOn(ROR_SENSE_LIMIT_CLOSED)) { roofLastError="Error: Motion direction unknown"; return false; }

  // Figure out where the roof is right now best as we can tell...
  // Check for limit switch and reset times
  if (senseIsOn(ROR_SENSE_LIMIT_CLOSED)) { EEPROM_writeLong(EE_timeLeftToOpen,roofTimeAvg); EEPROM_writeLong(EE_timeLeftToClose,0); }
  if (senseIsOn(ROR_SENSE_LIMIT_OPENED)) { EEPROM_writeLong(EE_timeLeftToOpen,0); EEPROM_writeLong(EE_timeLeftToClose,roofTimeAvg); }
  timeLeftToOpenAtStart =EEPROM_readLong(EE_timeLeftToOpen);
  timeLeftToCloseAtStart=EEPROM_readLong(EE_timeLeftToClose);

  // Check for validity of roof position timers before starting (they need to be within +/- 2 seconds)
  if (!roofSafetyOverride && abs((timeLeftToOpenAtStart+timeLeftToCloseAtStart)-roofTimeAvg) > 2000) { roofLastError="Error: Close location unknown"; return false; }

  // Check to see if the roof is already closed
  if (senseIsOn(ROR_SENSE_LIMIT_CLOSED) && (!senseIsOn(ROR_SENSE_LIMIT_OPENED))) { roofLastError="Warning: Already closed"; return false; }

  // Just one last sanity check before we start moving the roof
  if (senseIsOn(ROR_SENSE_LIMIT_CLOSED) && senseIsOn(ROR_SENSE_LIMIT_OPENED)) { roofLastError="Error: Closed/opened limit sw on"; return false; }

  // Flag status, no errors
  roofState='c';
  roofStatusRegister=0;
  delay(2000);

  if (ROR_SENSE_INTERLOCK != OFF && !senseIsOn(ROR_SENSE_INTERLOCK)) { roofState='i'; roofLastError="Error: Close safety interlock"; return false; }

  // Set relay/MOSFET
  if (ROR_MOTOR_RELAY_MOMENTARY != OFF) {
    setRelayOnDelayedOff(ROR_MOTOR_CLOSE_RELAY,2);
  } else {
    setRelayOff(ROR_MOTOR_OPEN_RELAY);
    setRelayOn(ROR_MOTOR_CLOSE_RELAY);
  }

  // Log start time
  roofCloseStartTime=(long)millis();

#if ROR_POWER_PWM_SOFTSTART == ON
  roofCurrentPower=0;
#else
  roofCurrentPower=ROR_POWER_PWM_POWER;
#endif

  roofLastError="";
  return true;
}

// stop the roof, this must be ISR safe! (required)
void stopRoof() {
  // Reset possible override of roof timer
  roofSafetyOverride=false;
  // Reset roof power to normal level
  roofMaxPower=false;
  // Set the state to idle
  roofState='i';
  // Stop any DC motor
  setRelayOff(ROR_MOTOR_OPEN_RELAY);
  setRelayOff(ROR_MOTOR_CLOSE_RELAY);
  // And press the stop button if this roof has one
  if (ROR_MOTOR_RELAY_MOMENTARY != OFF && ROR_MOTOR_STOP_RELAY != OFF) setRelayOnDelayedOff(ROR_MOTOR_STOP_RELAY,2);
}

// clear errors (required)
void clearRoofStatus() {
  // Reset the status register
  roofStatusRegister=0;
  roofLastError="";
}

// returns an error description string if an error has occured, otherwise must return "Travel: n%" or "No Error" (required)
String getRoofStatus() {
  String s=getRoofLastError();
  if (s=="" && roofIsMoving()) { s="Travel: "+String(roofTravel)+"%"; return s; }
  if (s=="") s="No Error";
  return s;
}

// returns an error description string if an error has occured, "" if no error (required)
String getRoofLastError() {
  String s="";
  if (roofStatusRegister&512) s="Error: Open safety interlock";
  if (roofStatusRegister&256) s="Error: Close safety interlock";
  if (roofStatusRegister&128) s="Error: Open unknown error"; else
  if (roofStatusRegister&64) s="Error: Open limit sw fail"; else
  if (roofStatusRegister&32) s="Error: Open over time"; else
  if (roofStatusRegister&16) s="Error: Open under time"; else
  if (roofStatusRegister&8) s="Error: Close unknown error"; else
  if (roofStatusRegister&4) s="Error: Close limit sw fail"; else
  if (roofStatusRegister&2) s="Error: Close over time"; else
  if (roofStatusRegister&1) s="Error: Close under time";
  if (s=="") {
    if (roofState=='i') {
      if (roofLastError=="") {
        // one final check for any wierd relay stuff going on
        if (senseIsOn(ROR_SENSE_LIMIT_CLOSED) && senseIsOn(ROR_SENSE_LIMIT_OPENED)) s="Error: Limit switch malfunction";
      } else s=roofLastError;
    }
  }
  return s;
}

// true if the roof is closed (required)
bool roofIsClosed() {
  return senseIsOn(ROR_SENSE_LIMIT_CLOSED) && (!senseIsOn(ROR_SENSE_LIMIT_OPENED));
}

// true if the roof is opened (required)
bool roofIsOpened() {
  return senseIsOn(ROR_SENSE_LIMIT_OPENED) && (!senseIsOn(ROR_SENSE_LIMIT_CLOSED));
}

// true if the roof is moving (required)
bool roofIsMoving() {
  return (roofState!='i');
}

// true if the roof is moving (closing, required)
bool roofIsClosing() {
  return (roofState=='c');
}

// true if the roof is moving (opening, required)
bool roofIsOpening() {
  return (roofState=='o');
}

// safety override, ignores stuck limit switch and timeout (required)
void setRoofSafetyOverride() {
  roofSafetyOverride=true;
}

// required
bool roofIsSafetyOverride() {
  return roofSafetyOverride;
}

// forces pwm power to 100%
void setRoofMaxPower() {
  roofMaxPower=true;
}

// required
bool roofMaxPowerOn() {
  return roofMaxPower;
}

// for soft start etc, pwm power level (required)
int roofPowerLevel() {
  return roofCurrentPower;
}
    
#endif


pbkoden@...
 

Interlock update works great, and much better than my hacked together modification. Thanks for a prompt solution to my issue. I don't plan to use any of the power relay controls just yet, but I can see some future value to them.

As far as setting both open and close relays to the same relay number. The issue I was running into is that if I initiated a closure (through the webpage), it never said "Closing" and immediately changed to "Stopped" with no error. It did not show the closing progress bar. Basically it skipped the closing state completely. But it did trigger the closing relay before it stopped. Today however, I can't seem to re-create the issue. It could have been a modification I made to the code that broke something.


pbkoden@...
 

Doing more testing this morning (adding a rain safe condition) and came up with a couple more questions.

If the rain sensor is the only sensor, the condition will always be unsafe, because the rain sensor section in WeatherPoll.ino doesn't have a mainDeviceCount++ increment. Is this intentional?

There also appears to be a missing ")" in OCS.ino on line 386: if ((!roofIsClosed()) && (!roofIsMoving())) startRoofClose();

After those two tweaks, I think this is ready to solder together and put out in the observatory. Thanks Howard!


Howard Dutton
 
Edited

On Fri, Jul 10, 2020 at 09:22 AM, <pbkoden@...> wrote:
After those two tweaks, I think this is ready to solder together and put out in the observatory. Thanks Howard!
I've added those fixes, thank you for review and testing.

I'm thinking about another possible issue with your proposed setup.  The OCS shows a Stop button but it really doesn't do anything to stop the roof unless one of the following is available:

ROR_POWER_RELAY
ROR_MOTOR_OPEN_RELAY and ROR_MOTOR_CLOSE_RELAY (for DC motor mode only, not momentary.)
ROR_MOTOR_STOP_RELAY

Additionally the checks for stuck limit switches, interlock, and time have no way to actually stop the roof.

For this reason I added code to compile out any (inactive/invalid) code for stopping the roof (garage door opener is on its own) when none of the above are available.  No Stop button, no case where you ask the roof to stop and it doesn't, etc.  I'll do a little testing tonight before updating GitHub.

It was never my intention (added for an opener with a physical stop button) but ROR_MOTOR_STOP_RELAY could be set same as OPEN and CLOSE, so than another momentary button press occurs if the movement needs to be stopped (or reversal is ok.)  Provided the opener behaves that way.

One of these days I will document all of this in a Wiki!


pbkoden@...
 

My gate opener DOES stop on a second press of the button while moving, so that functionality could work.

Maybe I will have to re-wire my other outlet (that my gate opener is run off of) through a power relay. You have obviously thought this all through. That would give me that extra layer of safety.

Thanks,
Phil 


Howard Dutton
 

I did this update.  Much of how it behaves depends on the options selected.

For an situation where ROR_MOTOR_OPEN_RELAY == ROR_MOTOR_CLOSE_RELAY == ROR_MOTOR_STOP_RELAY and there is no ROR_POWER_RELAY:

  • There is now a button called [Press!] instead of [Stop!] since the OCS doesn't know with high confidence what will happen when that button is pressed (up to the user to decide.)  When [Open] or [Close] are pressed we can make the assumption of direction of travel based on the limit switches.
  • All automatic stopping of roof motion is disabled.

In situations where the ROR_MOTOR_STOP_RELAY is different than the OPEN & CLOSE that's assumed to be a valid way to always stop the roof.
As is having a ROR_POWER_RELAY.


Howard Dutton
 
Edited

There's actually a bunch of little refinements in the update too.  I coded the momentary button press timer to work in 0.1s intervals instead of 1s so the length of the press is more tightly controlled and consistent.  That allows it to be on average a shorter press.  It checks to see if a press is being held and either denies or waits until the press is done, depending on the situation before doing something else.  Other checks are made too, no press immediately after a prior press ends, etc.