• Fan auto-detection

    WJCarpenter6 days ago 0 comments

    The board and software I've designed has connectors and logic for 4 fans. I myself only plan to use 3 fans due to physical dimensions of my vent openings. Someone else might choose to use only 1 or 2 fans. Or, out of some kind of contrarian independence streak, they might plug the fans into connectors that are not adjacent. 

    I want to be able to automatically detect which fans are actually present. (There is a board jumper option to have the power or PWM signal for any of fans 2, 3, or 4 to be controlled by fan 1. For the purposes of this exercise, those fans controlled that way don't count as "present".)

    The autodetection turns out to be straightforward. At boot-up time, I turn on each fan in sequence at partial PWM speed. I wait a short while to let the fan spin up, and then I check its tachometer output. If it's spinning, the fan is present (and controlled by its individual signals). This only takes a few seconds, and each fan only spins for a short period of time. It only happens at boot-up, so it's not too annoying.

    It is also possible to detect which fans were controlled by the signals for fan 1 in a variation of this scheme.

    There's not much practical reason for doing either of these things right now, other than logging the findings. It does avoid having the ESP32 sending signals to non-existent destinations, but that's not very interesting. I guess it could be worked into a notification scheme for letting someone know if a fan went bad or came unwired or something.

  • Home Assistant integration

    WJCarpenter7 days ago 0 comments

    In case someone else wants to build some of these, I'm designing it with no external dependencies. The devices can operate completely standalone once built and configured. However, I do run Home Assistant in my house, and it's fairly easy to integrate ESPHome devices with Home Assistant. In particular, you can define services on the ESPHome device and call those services from Home Assistant. I never fooled around with that stuff before, but I found it pleasantly easy to set up.

    This might change in the final form, but here's all it takes to define the services on the ESPHome device. (Most of the logic resides in separate ESPHome scripts, not shown in this listing. Except for the names being suggestive, don't worry about the implementation logic at this point. Those will be visible in the ESPHome config file when it's ready and published.)

    api:
      # These services can be called from Home Assistant for manual testing and temporary overrides.
      services:
    
        # Force pretending that the temperature is either in (true) or out (false) of the neutral zone.
        # Temperature sensor readings will be ignored until the duration expires or the hold is
        # canceled.
        - service: set_inout_neutral_zone_and_hold
          variables: {in_neutral_zone: bool, duration_seconds: int}
          then:
            - globals.set:
                id: IN_NEUTRAL_ZONE
                value: !lambda 'return in_neutral_zone;'
            - script.execute:
                id: S10_set_inout_neutral_zone_and_hold
                in_neutral_zone: !lambda 'return in_neutral_zone;'
                duration_seconds: !lambda 'return duration_seconds;'
    
        # This can be used to cancel the above hold action before the duration has expired.
        - service: cancel_hold_inout_neutral_zone
          then:
            - script.execute: S07_cancel_hold_inout_neutral_zone
    
        # Moves the fan speeds one step UP the ramp
        - service: crank_up
          then:
            - script.execute: S08_crank_up
    
        # Moves the fan speeds one step DOWN the ramp
        - service: crank_down
          then:
            - script.execute: S09_crank_down
    

     I made a simple Home Assistant dashboard for my breadboard rig. 

    The bottom portion is just normal Home Assistant despiction of entities from the device. The top row of buttons were created with custom:button-card, a popular Home Assistant add-on. It doesn't have a GUI editor, but here is the raw YAML configuration for the leftmost button:

    name: Step Up
    show_name: true
    show_icon: true
    type: custom:button-card
    tap_action:
      action: call-service
      service: ESPHome.ventbot_f56ed8_crank_up
    entity: automation.ventbot
    icon: mdi:fan-chevron-up
    show_state: false
    

     You can see that it calls the "crank_up" service defined on an ESPHome device known to Home Assistant as "ventbot_f56ed8" (the hex stuff is part of that device's specific MAC address, for disambiguation).

    With these services and controls, I can:

    • Turn the fans on and have them ramp all the way up to the configured full speed.
    • Turn the fans off and have them ramp all the way down to off.
    • At any point, I can step the fan speeds up or down to the next step of the configured ramps.
    • Not shown, but when any of these services are called, they tell the device to ignore temperature triggers for a while (5 minutes).
    • The cancel button turns off that hold and lets the temperature triggers have their normal effects.

  • Totally hot stuff with BMP280

    WJCarpenter12/01/2022 at 01:16 0 comments

    All along, I've been planning to use BMP280 sensors for temperature on this project. I have quite a few BMP280 breakout boards laying around unused. I got a few of them before I realized I could get BME280 boards that also detect relative humidity. I got a bunch more because when you try to buy BME280 boards from random overseas sellers on eBay, you stand a good chance of receiving BMP280 boards instead.

    Anyhow, any temperature sensor with an I2C interface can work pretty easily with the rest of the design.

    A funny thing happened. OK, two funny things. When I first added a BMP280 to the breadboard, I had some kind of brain glitch and wired it's Vcc to the 12v supply instead of the 3.3v supply. No smoke came out, but that little board was completely dead by the time I figured it out. I grabbed another BMP280 and wired it correctly on the breadboard. It worked, except it was reading a temperature of 189 C. I've never seen that behavior from a BMP280 or BME280 before. I don't even know what could make it happen. I popped it out and replaced it with a third BMP280, and things worked as expected.

    So, that was weird. I'll remember to include a step for sanity testing the temperature read-outs when assembling things.

  • Some observed breadboard results

    WJCarpenter11/24/2022 at 22:14 0 comments

    With things wired on the breadboard as described in my last project log, I decided to see how things looked. I configured my ESPHome configuration to step up from 0% to 100% PWM in 10% increments. At each step, I waited for 60 seconds to give the fan speed time to stabilize.  I logged the tach output every few seconds, with the fan pulse counts converted to RPMs. On the "PWM" line, I logged both the percentage from the point of view of the user and also the decimal fraction of the inverted signal, which is what is sent to the ESPHome LEDC control.

    The results are in rough agreement with what I measured on my scope a few weeks ago.

    PWM: 0.90   10
      tach:  150 rpm
      tach:  164 rpm
      tach:  162 rpm
      tach:  164 rpm
      tach:  165 rpm
      tach:  161 rpm
     -----
    PWM: 0.80   20
      tach:  237 rpm
      tach:  362 rpm
      tach:  363 rpm
      tach:  359 rpm
      tach:  363 rpm
      tach:  362 rpm
     -----
    PWM: 0.70   30
      tach:  459 rpm
      tach:  620 rpm
      tach:  621 rpm
      tach:  620 rpm
      tach:  624 rpm
      tach:  620 rpm
     -----
    PWM: 0.60   40
      tach:  708 rpm
      tach:  851 rpm
      tach:  852 rpm
      tach:  851 rpm
      tach:  855 rpm
      tach:  854 rpm
     -----
    PWM: 0.50   50
      tach:  930 rpm
      tach:  1062 rpm
      tach:  1059 rpm
      tach:  1058 rpm
      tach:  1059 rpm
      tach:  1058 rpm
     -----
    PWM: 0.40   60
      tach:  1131 rpm
      tach:  1247 rpm
      tach:  1253 rpm
      tach:  1251 rpm
      tach:  1254 rpm
      tach:  1250 rpm
     -----
    PWM: 0.30   70
      tach:  1313 rpm
      tach:  1419 rpm
      tach:  1418 rpm
      tach:  1416 rpm
      tach:  1418 rpm
      tach:  1418 rpm
     -----
    PWM: 0.20   80
      tach:  1482 rpm
      tach:  1583 rpm
      tach:  1581 rpm
      tach:  1583 rpm
      tach:  1590 rpm
      tach:  1589 rpm
     -----
    PWM: 0.10   90
      tach:  1646 rpm
      tach:  1737 rpm
      tach:  1736 rpm
      tach:  1744 rpm
      tach:  1740 rpm
      tach:  1744 rpm
     -----
    PWM: 0.00   100
      tach:  1804 rpm
      tach:  1913 rpm
      tach:  1911 rpm
      tach:  1913 rpm
      tach:  1917 rpm
      tach:  1913 rpm
    

    Here is a nice graphical result from a later similar test.

  • Modified PWM and tach wiring

    WJCarpenter11/24/2022 at 21:58 0 comments

    I had a dream of a fairly simple control arrangement for the fans. Since most fans don't switch off at 0% PWM duty cycle, I designed in an N-channel MOSFET to cut the ground link to the fan when I wanted to turn it completely off. That was a grand idea until I wired it up on the breadboard and found that the fan kept gently turning even with the MOSFET switched off. Hmmm. Maybe the reason is obvious to someone who does electronics for a living, but I never completely connected the dots. I did figure out experimentally that the fan shut off completely if I disconnected both the PWM input and the tach output. My theory is that fan motor was getting enough leakage to ground through those lines to give it a feeble spin.

    I first attacked the PWM input line. Instead of connecting it directly to the fan PWM line, I connected it to a 2N3904 NPN transistor. The PWM signal went to the base, the collector went to 3.3v, and the emitter went to ground. I also tried it with emitter and collector swapped. Both ways worked in the sense of controlling the PWM to the fan, but it didn't affect the fan slowly turning at 0% PWM. (This arrangement acts as a sort of inverter of the PWM signal, so I had to subtract the desired value from 100% to apply to the control line. I could also have just marked the pin inverted in the ESPHome config, but that made some other parts less clear.) I eventually found that ESPHome would let me explicitly turn off the PWM signal output, and that would stop the final piece of fan movement (again, as long as the tach line was not connected).

    (I also tried connecting the same power control from the MOSFET to the base of the NPN to try to disable the PWM signal going through the emitter/collector pins. That didn't work, but I didn't pursue it.)

    One down, one to go.

    The tach signal is trickier. The same approach doesn't work for that. I'm using the ESPHome pulse_counter component, and there's no way to explicitly switch it off, probably because it's an input signal. I tried the same tricks as for the PWM signal, but they didn't work. 

    While experimenting, I "cooked" 2 ESP32 boards. When that happened, I thought maybe the boards weren't really happy with my 12v inputs to the 5v ESP32 power pin. I changed things to run the fans off 12v and the ESP32 from the USB connector. My plan was to figure out some kind of power conditioning later, but I have more or less concluded that I killed the boards by feeding them some variation of a 12v tach signal on the ESP32 3.3v GPIOs. Why did it kill things now instead of over the last couple of weeks of messing around with that 12v signal? I hypothesize that there wasn't enough current at 12v to do damage, and something in one of my experiments let more current through on the same line. (That might have happened by me mis-wiring something as I moved jumpers around. No smoke came out, but some parts of the ESP32 boards got pretty hot.)

    After burning up the second ESP32 board, I decided on the irrational course of trying to figure out what I should do instead of poking electricity into places where it didn't want to be. Looking through all my notes about fan interfaces, I re-read this StackExchange reply from Michael Karas. It's a clear explanation, and includes helpful info in the comments below the posting. I don't know the original source of the circuit diagram he posted. It's the same as the circuit given in this blog by a different author, who describes it as a "detailed portion of a bit old official motherboard schematic" and gives the missing value for R251 as 4.7k. It's not from the 2005 version of the Intel spec for PWM fans.

    When I read it a while back, I thought, "Nice, but I don't need anything this complicated." It turns out I do need something that complicated. The PWM part of the circuit is pretty much what I ended up with. I wired up the tach part on the breadboard according to this schematic, and it worked as desired.

    The...

    Read more »

  • Ramp up and down

    WJCarpenter11/20/2022 at 21:40 0 comments

    I plan to have a configurable way to spin the fans up and down when needed. When I say configurable, I currently don't have in mind anything fancier than editing some tabular data in the ESPHome configuration file. Maybe someday that table will be stored in ESP32 preferences storage so it can be modified without rebuilding the firmware. That's a bigger adventure than I am thinking about at the moment. I'm imagining the ESPHome configuration file being edited by someone who is not necessarily technically sophisticated. So, I don't want them to have to dig around too much in the file or have to deal with syntax that is too complex.

    Here is what I have right now:

    • The ramp consists of any number of steps. Presumably, they start with low speeds and move increasingly upwards, but that is not enforced.
    • There's a kind of implied first step, which is "everything is off".
    • Each step consists of a duration (I'm not sure yet if it will be in seconds or milliseconds) and a set of PWM duty cycle percentages. The meaning is "set the fans to the specified PWM duty cycle values and wait for the duration time before moving to the next step".
    • The duration for the final step is ignored (or treated as "infinity") since there is nothing to wait for. The fans will continue at those settings until it's time to ramp down.
    • For ramp down, the steps are used in the reverse order. The highest-numbered step will ordinarily be where the ramp up left us. For ramp down, the duration of that step is taken as 0.

    I'm not completely sure the ramp up and ramp down behavior should be symmetric. I'm doing that for simplicity but may have to add more complexity to it later.

    ESPHome has a feature for defining global variables. Theoretically, the ramp up and ramp down could be done completely using the table defined in global variables and ESPHome primitives. It's rather inconvenient to do that for more than a few simple variables, and I think it would obscure the logic of the simple table I'm using. I'm planning to use an ESPHome lambda block to do the ramp up and ramp down in C++. 

    To avoid the need to naive users to grovel around in C++ code located deep in the ESPHome configuration file, I'm using the ESPHome substitutions feature to define the guts of the table up near the top of the ESPHome configuration file. That fragment is then plugged into some C++ code in an ESPHome lambda later. It's a purely mechanical substitution. Here is an example of the table. The numbers in this example are not sensible and were chosen just to be sure of what's going on in debug logging.

    substitutions:
      RAMP_TABLE: |-
        {1, {11, 12, 13, 14}},
        {2, {21, 22, 23}},
        {3, {31, 32}},
        {4, {41}},
    

    The syntax here is not too onerous, and I think the average person can handle it. 

    It's still possible to get it wrong. When you're trying to do a static initialization of an array of structs in C++, it's like trying to get help from a lawyer with whom you do not share a common language. You say what you think you want, and they respond with a load of gibberish. There are a few words in there that you recognize, but the sequence of words makes no sense and is not helpful. With the aid of some web searches, you eventually get something that does not bring forth a string of complaints from them, but you're not sure what you asked for is what you want. (I spent a lot of years doing development in C. I was at Bell Labs when C++ came along. For a while, the motto was "a better C", but it eventually changed to "a better C, plus a couple of train wrecks thrown in for free".) 

    Here is some ESPHome lambda code that prints out the ramp table. You can see the ${RAMP_TABLE} value substituted at around line 7. ${FAN_COUNT} is another ESPHome substitution and has the value 4 (for the maximum possible fans, not necessarily the number of actual fans).

          - lambda: |-
              typedef struct {
                short duration;
                short pwm[${FAN_COUNT}];
              } RAMP_STEP;
              static RAMP_STEP ramp_steps[] = {
                ${RAMP_TABLE}
              };
              static short number_of_ramp_steps...
    Read more »

  • Design thoughts snapshot

    WJCarpenter11/20/2022 at 20:12 0 comments

    This is a recap of my thinking about the design of this project. Some of it I've mentioned or hinted in earlier project logs, and some has lived only in my tiny brain.

    • For the software framework, I plan to use ESPHome. I've used that already for several projects. Except for the aggravations inherent in YAML, it's very easy to work with, and it's very reliable. ESPHome will take care of a lot of plumbing work (WiFi, sensor readings, and more) so I don't have to worry about it.
    • Using ESPHome makes it very simple to hook into my Home Assistant server. I'm not sure yet what I want to do via Home Assistant. I know I don't want to make it a "must have" part of the system, but there are lots of "nice to have" possibilities.
    • The microcontroller in the project will be a 38-pin ESP32-DevKitC. It's arguably overkill for this design, but it has plenty of available GPIO pins for what I want to do. They are widely available for US$10-15 or so. It has several kinds of communications capabilities to give me some flexibility for as-yet-unplanned user interface possibilities.
    • Although I'm using "quiet" fans, they could still be startling when they switch on in a quiet bedroom in the dead of night. Fan noise itself, at low levels, is not disturbing. It's the sudden change from silence to something audible that pokes our sleeping brains. To ease the transition, I plan to ramp up the fan speeds from dead stop to my target speed. When turning them off, I'll also ramp back down. I'll have a later project log about this ramping.

    I've started working on a schematic and PCB layout to hold all this. It provided me a chance to finally try out KiCad (which is going pretty well; the Freerouting plugin has already saved me hours and hours of time). Even with all the stuff I'm about to describe, it all fits onto a double-side board that is less than 75 mm square. Partly for philosophical reasons and partly for practical reasons, I'm designing some flexibility options into the PCB. I'm only planning to make at most 4-5 of these devices, but the PCB fabrication places generally include 10 copies of the board. When ordering from Asia, shipping is a non-trivial percentage of the cost of getting the boards in your hands, so I might order more than 10 copies once I'm confident in my PCB design. It will be nice to have other uses for them. My experience with the PCB prototyping services is that the boards are pretty darned nice.

    • For my house, I'll be using three 92 mm fans. I have enough ESP32 pins available, so the board allows for four fans. I'm planning some software help to automatically detect which of the four fans is actually present at boot-up. Things will probably work OK if 3-pin non-PWM fans are used, though some of the beauty of the overall design will be lost.
    • Fan #1 must always be present, but all other fan positions are optional. I plan to control all the fans individually, but there are jumpers on the board so that everything (well, PVM control lines and power) can be wired to be controlled by whatever happens to Fan #1.
    • Most fans do not turn off completely, even at 0% PWM duty cycle. I plan to handle that by controlling the 12v supply to the fan with a MOSFET. The particular part I'm using is something I had used in a previous project, and I still had a couple on-hand. It's N-Channel, which to my non-EE brain is slightly confusing because you end up switching the "ground supply" to the device. Yeah, yeah, I know it works, and I prototyped it on a breadboard to make sure I got it right. It will be possible to omit the MOSFETs and use a wire jumper between pads to always have power supplied to the fans.
    • There are current-limiting resistors on the signal going to the MOSFET Gate. I'm not really sure they are needed. If not, they can be replaced with wire jumpers.
    • The temperature sensor, which controls when the fans need to turn on or turn off, will be off-board but connected via I2C. Since I2C devices can chain together using exactly the same 4 wires,...
    Read more »

  • The fan blink

    WJCarpenter11/20/2022 at 00:08 0 comments

    To test my idea of controlling the power to the fan via the MOSFET, I wired up a fan on the breadboard to be controlled by the MOSFET, similar to how I wired up the LED to blink for an earlier project log. (It's a Noctua NF-B9 redux-1600 PWM. For this test, it doesn't have a PWM input and so it runs at full speed.) I kept the LED (with a source voltage coming from the 3.3v output of the ESP32) as a visual reference. I changed the on/off interval from half a second to 5 seconds to give the fan time to come up to speed. The fan is powered by a 12v input (as is the ESP32, come to think of it).

  • Temperature rises

    WJCarpenter11/19/2022 at 23:14 0 comments

    Now that it's autumn in my area, and the temperatures are frequently in the 30-40 F range, I can see how things look in my vents when the heat is on. This is analogous to the measurements I made in the summer when my heat pump was in cooling mode.

    I returned the temperature sensor to the same vent and monitored it for a few hours. Here is that history graphed:

    The yellow-ish/orange-ish blocks are when the heat was on. As hoped, it corresponds nicely to the large rise in temperature inside the vent opening. By eyeball, if I use about 85 F as a trigger point, it's a 5-10 minute delay from the heat coming on to reaching that trigger point, and there is something similar when the heat turns off.  Based on the summer measurements, I was thinking something like 60 F for the cool trigger. Because of different duct lengths, some of the other rooms might have different high and low temperature triggers. I'll make that configurable.

    A couple of the morning heat cycles were short in duration and rose quite a bit higher than the later, longer cycles. During those shorter, hotter cycles, my gas furnace was providing auxiliary heat. The longer, not-as-hot cycles were the heat pump living up to its name and pumping heat from the outside into my house.

  • On the blink

    WJCarpenter11/19/2022 at 04:35 0 comments

    Since I plan to use MOSFETs to turn the power to the fans off and on, I wanted to make sure I understood how that should be wired up. I wrote the equivalent of the ubiquitous "blink" program in ESPHome configuration.

    substitutions:
      node_name: esp32-blink
      log_level: DEBUG
    
      wifi_ssid: !secret wifi_ssid
      wifi_password: !secret wifi_password
    
      ota_password: !secret ota_password
      
      LED:     'GPIO14'
    
    esphome:
      name: ${node_name}
      platform: esp32
      board: esp32dev
    wifi:
      ssid:      ${wifi_ssid}
      password:  ${wifi_password}
    ota:
      password: ${ota_password}
    
    switch:
      - platform: gpio
        name: "${node_name} LED"
        id: i_led
        internal: true
        pin:
          number: ${LED}
          inverted: yes
    
    interval:
      - interval: 0.5s
        then:
          - switch.toggle: i_led
    
    • GPIO14 is the control signal. It's wired to the Gate of the MOSFET. Because I've seen it illustrated a couple of places, I also used a resistor for current limiting.
    • The MOSFET Source is wired to one of the ground pins of the ESP32.
    • The MOSFET Drain is wired through a current limiting resistor to the cathode leg of an LED.
    • The anode leg of the LED is wired directly to the 3.3v output pin of the ESP32.
    • The ESPHome configuration toggles the GPIO14 control signal every 0.5 seconds.