thorrak / brewpi-esp8266

An implementation of the BrewPi device code on the ESP8266, ESP32, and ESP32-S2
GNU General Public License v3.0
85 stars 30 forks source link

Optional Reconnect on Power Loss #23

Closed thorrak closed 1 year ago

thorrak commented 5 years ago

On the heels of a point that came up in a HomeBrewTalk thread, I'm considering either adding a feature to the main line firmware (or starting to compile an alternative firmware version) that changes the behavior of the firmware after a power loss.

After a power outage, the ESP8266 controller will restart, and then search for the configured WiFi network. If the network is unavailable (such as if the router hasn't finished rebooting) then - as you noted - it will go into Soft AP mode thinking that the network it needs to connect to no longer exists. After ~5 minutes or so, it will give up on this, and just go into "offline" mode where it maintains the last setpoint indefinitely.

For users who have installations in remote/inaccessible areas (second homes, stuffed behind keezers, etc) this approach may not be the desired one. A better approach may be rather than defaulting to starting the Soft AP, to just keep attempting to reconnect until a connection is made.

This issue as a means of gauging interest in changing this behavior in the hopes that anyone sufficiently annoyed by the default behavior will come to this thread to complain. If there's enough interest, then I'll see if I can change how this works the next time I crack the firmware open for changes.

lbussy commented 5 years ago

If it matters, and not knowing the impact of what I am about to suggest: I would think that a "positive action" should be all that would cause the firmware to go into soft AP mode after it's been configured the first time. Hardware solutions suck, but something like "short pin x to in y and reboot" would be what I'm talking about. I don't remember if there's any other way to "signal" the firmware starting up so just spitballing here ....

treefiddy05 commented 4 years ago

I'm curious to know if there is any follow up to this. My controller box is far from my router so I put in an external USB WiFi to receive WiFi signal and create its own hotspot (hostapd). It takes a minute or so to start up the hotspot, so the ESP8266 controllers won't reconnect after power loss. I can add a physical switch for RST, but this requires me noticing and being there. This firmware feature would be useful to me. Alternatively (sorry if off topic), do you think this feature could be met by connecting the Wemos D1 mini RST pin to an RPi GPIO pin set to 0 for a few ms when the ESP8266 are not detected?

lbussy commented 4 years ago

Your idea to let the RPi do the reset has merit. The RPi has 3V3 logic, so you would want to use a ~10k pull-up. What I am not sure of however is if the pull up should be from the RPi power or the ESP8266.

To fix this in software, I'd go here:

https://github.com/thorrak/brewpi-esp8266/blob/259de38f08fb5124f21758b32bd25693fd3f13ea/src/brewpi-esp8266.cpp#L144-L151

Just spit-balling, but I'd change it to:


    if(wifiManager.autoConnect()) {
        WiFi.softAPdisconnect(true);
        WiFi.mode(WIFI_AP_STA);
    } else {
        // We failed to connect - turn WiFi off
        WiFi.softAPdisconnect(true);
        WiFi.mode(WIFI_OFF);
        ESP.restart();
    }
treefiddy05 commented 4 years ago

Thanks man, I hadn't considered that the changes would be so straight forward. I'm not an EE or a dev, but maybe feel more comfortable with the latter. The Wemos D1 mini reset it triggered by ground on in RST pin. I wasn't sure if I needed a pull down resistor, but then I figured that would just keep the RST pin constantly resetting. Maybe I've misunderstood the concept a little. If I understand your suggestion correctly, the pull up resistor is to keep the normal state (high) at 3.3v, so that when the RPi detects no ESP8266's, GPIO will flip to 0/ground for a few ms?

lbussy commented 4 years ago

I'm not an EE or a dev

Join the club. 👍

If I understand your suggestion correctly, the pull up resistor is to keep the normal state (high) at 3.3v

Right. A pin with nothing on it doesn't have a state. If you try to read it, it could be high, it could be low. It's the microcontroller equivalent of Schrödinger's cat. Think of it this way:

You have a glass of water with a large dump valve representing your reset pin. If the glass is empty (low), reset the controller. If it's full (high), the controller runs. If you check it, thinking it will be full (high) because the valve is closed, what happens if nothing has filled up the glass? Worse, what happens if there's a little water left? You have absolutely no idea how it will be interpreted.

The pull-up resistor is a small trickle of water that fills the glass. You close the valve and the glass fills and when you check it, it's is obviously full (high). When you dump the water, even if the water (pull-up) is still trickling in, the glass will be empty (low).

Some pins have internal pull-ups and I am not sure if the reset is one such pin. If it does, then a simple wire to the RPi GPIO would not work. Pins GPIO2 and GPIO3 on the Pi have fixed pull-up resistors, so I don't think those would be appropriate. I think we want to leave the ESP8266 powering the pull-up.

I'd probably use the RPi.GPIO library and some code like:

import RPi.GPIO as GPIO
import time

resetpin = 17 # Pin 11

GPIO.setmode(GPIO.BOARD)
GPIO.setup(resetpin, GPIO.OUT)
GPIO.output(resetpin, GPIO.HIGH)

def resetController():
    GPIO.output(resetpin, GPIO.LOW)
    time.sleep(1)
    GPIO.output(resetpin, GPIO.HIGH)

I'm not sure what the behavior of the pin will be when set high. Ideally it would be "not low" and expecting a pull-up to pull it high. You can test this by setting it high as above, and checking with a multimeter between that pin and the board's ground. If it's 0, all is well. If it's actively pulled high internally, you'd need a relay in between the RPi and the controller to do the resetting.

treefiddy05 commented 4 years ago

Lee, I really appreciate your help on this. From what I've been able to find, pull-up resistors resistors are not always necessary on outputs. For what it's worth, the D1 mini's schematic show there is already a resistor between the D1 mini 3.3v and the RST pin.

I took a cautious approach and put a resistor in between RPi GPIO pin 11 and RST. This did not trigger the reset. I assume because in your analogy, the dump valve was too small and possibly clogged with hops. I ditched the resistor and now have a simple wire between pin 11 and RST. This works! High is a steady 3.3v, and low for 1 second (0v) resets the controller. I checked the current flow between pin 11 and RST but it was not measurable on my cheap multimeter in the 200μA range. I guess this means I'm in the clear.

I'll use something like "iw dev wlan0 station dump" to retrieve the list of currently connected wifi devices, check if the controllers mac address is present, and run the gpio.py if not found. Use cron to run this every 30 minutes or so and that should be it. I'll post the results so others may use it and/or make fun of my novice skills :D

treefiddy05 commented 4 years ago

Good thing I don't get paid to do this, it took me a little longer than expected. All part of the fun though. Here's what I've come up with:

For multiple D1 mini's I'll just create extra .py scripts and assign then to different pins.

keep_esp_alive.sh

{
if systemctl is-active hostapd | grep -qw 'active'
    then echo "Hotspot is active, continuing"
else
    echo "Hotspot is down, exiting"; 
    exit 0
fi
}

#Is the mac address of Wemos A connected to the hotspot? Reset it with the python script.
{
if iw dev wlan0 station dump | grep -q 'bc:dd:c2:25:34:c4' 
    then echo "Wemos A is connected, checking next device"  
else 
    echo "Wemos A   NOT   connected. Resetting.";
    python3 RST_wemos_A.py
fi
}

RST_wemos_A.py

#import the GPIO and time package
import RPi.GPIO as GPIO
import time
resetpin = 5
GPIO.setmode(GPIO.BOARD)
GPIO.setup(resetpin, GPIO.OUT)
GPIO.output(resetpin, GPIO.HIGH)

#def resetController():
GPIO.output(resetpin, GPIO.LOW)
time.sleep(1)
GPIO.output(resetpin, GPIO.HIGH)
lbussy commented 4 years ago

Well, the circuit is almost right. I know it works, and it may always work, but I think it may cause you issues. Here's what the reset circuit looks like on the controller:

reset

There's a pull-up on the line with a 12kΩ resistor from GPIO15. When you press the switch (SW1) it shorts RESET to GND. The capacitor (C1) is there to limit bouncing. You need to emulate that switch to most effectively reset the controller.

A better solution is to use a Bipolar Junction transistor (BJT) of the NPN type. A good choice would be the 2N3904. That transistor will act as a switch when you apply current to its base, providing your reset:

reset_npn

Since the current will be coming from an RPi at 3V3, you want a current-limiting resistor in there. A 2.7kΩ should be perfect.

Now, you apply 3V3 (HIGH) from the RPi whenever you want to reset the controller. Easy peasy! (Not tested of course, definitely bread-board it.)

Adding a line in the WiFi setup to keep it from going off into never-never land is idea #2. The trade-off, of course, is that the controller will never start back into its heating/cooling which really is its main job. A toss-up to be sure. You'll have to decide which is better for you.

thorrak commented 2 years ago

The way I'm thinking about solving this is having it so that there are three states for WiFi that is not yet connected, but has saved credentials:

  1. At initial boot, attempt to connect to the saved AP
  2. If this fails, kick off the configuration AP for five minutes to allow reconfiguration
  3. If no user logs in and kicks off the configuration AP, go into fallback mode. Every X minutes, attempt to connect to the saved AP.

I think this compromise - adding periodic reconnection attempts when in "maintain current setpoint" mode - makes the most sense for most use cases.