OK, I think I know what's going on there.
In the IDLE state within relay() I wait until the bRelay flag is set by the code that checks the voltage at the analog input. When that happens, I immediately clear that flag, log the current millis() count and move to time the relay delay.
I think the problem is that as soon as we get back to recvOneChar() a few 10s of microseconds later, the voltage is still present and so the flag, bReset, is set again. When the relay timing is done and we get back to IDLE state, it sees the flag is set and starts timing again.
You can try this slightly modified relay() function. In it, I clear the bReset flag after the relay timing is done so, if the bRelay flag was set "in error", it's cleared before we get back to IDLE.
void relay( void )
{
uint8_t
stateRelay = RLY_OFF;
static uint32_t
timeRelay;
uint32_t
timeNow = millis();
switch( stateRelay )
{
case RLY_IDLE:
//waiting for flag to indicate relay sequence
if( bRelay == true )
{
timeRelay = timeNow;
stateRelay = RLY_ON;
}//if
break;
case RLY_DELAY:
//delay before turning the relay on
if( millis() - timeRelay >= K_RELAY_DELAY_TIME )
{
digitalWrite( relay2, LOW ); //relay on
timeRelay = timeNow;
stateRelay = RLY_HOLD;
}//if
break;
case RLY_HOLD:
//relay on-timing
if( millis() - timeRelay >= K_RELAY_HOLD_TIME )
{
digitalWrite( relay2, HIGH );
bRelay = false;
stateRelay = RLY_IDLE;
}//if
break;
}//switch
}//relay