Note

Hello, welcome to the SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasts Community on Facebook! Dive deeper into Raspberry Pi, Arduino, and ESP32 with fellow enthusiasts.

Why Join?

  • Expert Support: Solve post-sale issues and technical challenges with help from our community and team.

  • Learn & Share: Exchange tips and tutorials to enhance your skills.

  • Exclusive Previews: Get early access to new product announcements and sneak peeks.

  • Special Discounts: Enjoy exclusive discounts on our newest products.

  • Festive Promotions and Giveaways: Take part in giveaways and holiday promotions.

👉 Ready to explore and create with us? Click [here] and join today!

4.7 Smart Fan

Introduction

In this project, you’ll build a “smart fan” that operates in both manual and automatic modes. By combining motors, buttons, and a thermistor, the fan can have adjustable wind speeds and respond to changes in temperature. This makes it a perfect experiment for learning about motor control, temperature sensing, and GPIO usage.


What You’ll Need

Here are the components you’ll need for this project:

COMPONENT INTRODUCTION

PURCHASE LINK

Breadboard

BUY

Jumper Wires

BUY

Resistor

BUY

Thermistor

BUY

Button

BUY

DC Motor

BUY

Fusion HAT+

-

Raspberry Pi

-


Circuit Diagram

The circuit diagram below illustrates how to connect the thermistor, button, motor driver, and motor:

../_images/4.1.7_sch.png

Wiring Diagram

Refer to the following image for the breadboard layout and wiring connections:

../_images/4.1.7_bb.png

Running the Example

All example code used in this tutorial is available in the ai-lab-kit directory. Follow these steps to run the example:

cd ~/ai-lab-kit/python/
sudo python3 4.7_SmartFan.py

Code

Here’s the Python script for this project:

#!/usr/bin/env python3

from fusion_hat.motor import Motor
from fusion_hat.pin import Pin, Mode, Pull
from fusion_hat.adc import ADC
from time import sleep, time
import math

BtnPin = Pin(22, mode=Mode.IN, pull=Pull.DOWN)
motor = Motor("M0")
thermistor = ADC("A3")

level = 0
markTemp = None

last_button_state = 0
last_press_time = 0
_last_print = 0.0

DEBOUNCE_TIME = 0.3
PRINT_INTERVAL = 1.0
MANUAL_HOLD_TIME = 2.0
TEMP_THRESHOLD = 5.0


def temperature(samples=5, delay=0.01):
    """Read thermistor temperature and return average Celsius value."""

    vals = []

    for _ in range(samples):
        analogVal = thermistor.read()
        Vr = 3.3 * float(analogVal) / 4095.0

        if Vr <= 0 or (3.3 - Vr) <= 0.1:
            sleep(delay)
            continue

        Rt = 10000.0 * Vr / (3.3 - Vr)

        if Rt <= 0:
            sleep(delay)
            continue

        tempK = 1.0 / (
            ((math.log(Rt / 10000.0)) / 3950.0)
            + (1.0 / (273.15 + 25.0))
        )

        vals.append(tempK - 273.15)
        sleep(delay)

    if not vals:
        return None

    return sum(vals) / len(vals)


def motor_run(lv):
    """Set motor power according to level."""

    lv = max(0, min(4, lv))
    power_table = [0, 25, 50, 75, 100]
    motor.power(power_table[lv])

    return lv


def read_button():
    """Read current button state."""

    try:
        return BtnPin.value()
    except TypeError:
        return BtnPin.value


def main():
    """Main loop for button control and temperature auto adjustment."""

    global level, markTemp, last_button_state, last_press_time, _last_print

    while markTemp is None:
        markTemp = temperature()
        sleep(0.1)

    while True:
        now = time()

        button_state = read_button()

        if button_state == 1 and last_button_state == 0:
            if now - last_press_time >= DEBOUNCE_TIME:
                old = level
                level = (level + 1) % 5
                last_press_time = now

                currentTemp = temperature()
                if currentTemp is not None:
                    markTemp = currentTemp

                level = motor_run(level)

                print(
                    f"[Button] {old} -> {level} | "
                    f"Power: {0 if level == 0 else level * 25}%"
                )

        last_button_state = button_state

        currentTemp = temperature()

        if currentTemp is None:
            print("Sensor read failed.")
            sleep(0.5)
            continue

        auto_allowed = (now - last_press_time) >= MANUAL_HOLD_TIME

        if auto_allowed and level != 0:
            diff = currentTemp - markTemp

            if diff >= TEMP_THRESHOLD:
                level = min(4, level + 1)
                markTemp = currentTemp
                level = motor_run(level)
                print(f"[Auto] Temp up -> Level {level}")

            elif diff <= -TEMP_THRESHOLD:
                level = max(0, level - 1)
                markTemp = currentTemp
                level = motor_run(level)
                print(f"[Auto] Temp down -> Level {level}")

        if now - _last_print >= PRINT_INTERVAL:
            print(
                f"Temp: {currentTemp:.2f} C | "
                f"Mark: {markTemp:.2f} C | "
                f"Level: {level}"
            )
            _last_print = now

        level = motor_run(level)

        sleep(0.05)


try:
    main()

except KeyboardInterrupt:
    print("\nExiting...")

finally:
    motor.stop()
    sleep(0.1)

This Python script integrates a motor, button, and temperature sensor to create a temperature-controlled fan system with adjustable speed. When executed:

  1. Temperature Sensing: Reads the current temperature in Celsius using the thermistor. Multiple samples are averaged for accuracy, and invalid readings are skipped rather than causing immediate failure.

  2. Manual Speed Adjustment:

    • A button connected to GPIO 22 allows the user to cycle through five speed levels (0 to 4).

    • Pressing the button increases the speed level, and the motor runs at the corresponding power (0%, 25%, 50%, 75%, or 100%). Speed level 0 stops the motor.

    • Button input includes debounce protection (0.3 seconds) to prevent false triggers from contact bounce.

  3. Manual Priority Window: After a button press, automatic speed adjustment is suppressed for 2 seconds (MANUAL_HOLD_TIME). This ensures the user’s manual setting is not immediately overridden by an automatic adjustment.

  4. Automatic Speed Control: After the manual priority window expires (and when the fan is not off), the system adjusts the motor speed automatically based on temperature changes:

    • If the temperature increases by 5°C or more, the speed level increases (up to level 4).

    • If the temperature decreases by 5°C or more, the speed level decreases (down to level 0).

  5. Continuous Monitoring: The system continuously monitors the temperature and button state with a fast 0.05-second loop interval, ensuring responsive adjustments.

  6. Graceful Exit: On Ctrl+C, the motor stops, and the script exits cleanly.


Understanding the Code

  1. Temperature Calculation:

    def temperature(samples=5, delay=0.01):
        """Read thermistor temperature and return average Celsius value."""
    
        vals = []
    
        for _ in range(samples):
            analogVal = thermistor.read()
            Vr = 3.3 * float(analogVal) / 4095.0
    
            if Vr <= 0 or (3.3 - Vr) <= 0.1:
                sleep(delay)
                continue
    
            Rt = 10000.0 * Vr / (3.3 - Vr)
    
            if Rt <= 0:
                sleep(delay)
                continue
    
            tempK = 1.0 / (
                ((math.log(Rt / 10000.0)) / 3950.0)
                + (1.0 / (273.15 + 25.0))
            )
    
            vals.append(tempK - 273.15)
            sleep(delay)
    
        if not vals:
            return None
    
        return sum(vals) / len(vals)
    

    The temperature() function takes multiple samples from the thermistor, converts each analog reading into a resistance value, and calculates the temperature in Celsius using the Steinhart-Hart equation. Individual bad readings (voltage too low or resistance zero) are skipped via continue rather than causing the entire measurement to fail. Only if all samples are invalid does the function return None. The final result is the arithmetic mean of all valid samples, giving a stable and noise-resistant reading.

  2. Motor Speed Control:

    def motor_run(lv):
        """Set motor power according to level."""
    
        lv = max(0, min(4, lv))
        power_table = [0, 25, 50, 75, 100]
        motor.power(power_table[lv])
    
        return lv
    

    The motor_run() function maps the numeric level (0–4) to a percentage of motor power using a lookup table. Level 0 sends 0% power (off), while levels 1 through 4 correspond to 25%, 50%, 75%, and 100% power respectively. The level is first clamped to the valid range with max(0, min(4, lv)).

  3. Button Reading with Debounce:

    def read_button():
        """Read current button state."""
    
        try:
            return BtnPin.value()
        except TypeError:
            return BtnPin.value
    
    
    # In main():
    button_state = read_button()
    
    if button_state == 1 and last_button_state == 0:
        if now - last_press_time >= DEBOUNCE_TIME:
            old = level
            level = (level + 1) % 5
            last_press_time = now
    
            currentTemp = temperature()
            if currentTemp is not None:
                markTemp = currentTemp
    
            level = motor_run(level)
    
            print(
                f"[Button] {old} -> {level} | "
                f"Power: {0 if level == 0 else level * 25}%"
            )
    
    last_button_state = button_state
    

    Instead of using an interrupt callback, the code polls the button state in the main loop. A button press is detected by a rising edge: the button state transitions from 0 to 1 (last_button_state == 0button_state == 1). A debounce timer (DEBOUNCE_TIME = 0.3 seconds) prevents contact bounce from triggering multiple presses. When a valid press is detected, the level cycles through 0→1→2→3→4→0, the reference temperature (markTemp) is updated, and the motor speed is applied immediately.

  4. Main Loop and Automatic Adjustment:

    def main():
        """Main loop for button control and temperature auto adjustment."""
    
        global level, markTemp, last_button_state, last_press_time, _last_print
    
        while markTemp is None:
            markTemp = temperature()
            sleep(0.1)
    
        while True:
            now = time()
    
            # ... button polling (see above) ...
    
            currentTemp = temperature()
    
            if currentTemp is None:
                print("Sensor read failed.")
                sleep(0.5)
                continue
    
            auto_allowed = (now - last_press_time) >= MANUAL_HOLD_TIME
    
            if auto_allowed and level != 0:
                diff = currentTemp - markTemp
    
                if diff >= TEMP_THRESHOLD:
                    level = min(4, level + 1)
                    markTemp = currentTemp
                    level = motor_run(level)
                    print(f"[Auto] Temp up -> Level {level}")
    
                elif diff <= -TEMP_THRESHOLD:
                    level = max(0, level - 1)
                    markTemp = currentTemp
                    level = motor_run(level)
                    print(f"[Auto] Temp down -> Level {level}")
    
            # ... periodic print ...
    
            sleep(0.05)
    

    The main() function runs a fast control loop (0.05-second interval). Key behaviors:

    • Initialization: It repeatedly reads the temperature until a valid markTemp is obtained, ensuring the system starts with a known reference.

    • Manual Priority: After a button press, automatic adjustments are blocked for MANUAL_HOLD_TIME (2 seconds). This auto_allowed gate prevents the system from immediately counteracting the user’s manual change.

    • Automatic Adjustment: When auto is allowed and the fan is not at level 0, the code checks whether the current temperature has deviated by TEMP_THRESHOLD (±5°C) from the reference. If so, the level is stepped up or down by one, and the reference is reset to the current temperature — this creates a latching behavior that prevents oscillation around the threshold.

    • Periodic Output: Every PRINT_INTERVAL (1 second), the current temperature, reference temperature, and level are printed for monitoring.


Troubleshooting

  1. Motor Does Not Run:

    • Cause: Incorrect wiring or insufficient power supply.

    • Solution:

      • Verify the motor is connected to M0.

      • Ensure the motor’s power supply matches its voltage requirements.

  2. Temperature Reading is Incorrect:

    • Cause: Faulty thermistor.

    • Solution:

      • Check the thermistor wiring and ensure it is within the specified range.

  3. Button Press Not Detected:

    • Cause: Incorrect button wiring or GPIO configuration.

    • Solution:

      • Verify the button is connected to GPIO 22 and ground.

      • Test the button independently to confirm it closes the circuit when pressed.

  4. Speed Level Does Not Change Automatically:

    • Cause: Incorrect temperature difference calculation.

    • Solution: Ensure the currentTemp and markTemp values update correctly in the main() function.


Extendable Ideas

  1. Overheat Alert: Add a buzzer or LED to alert the user when the temperature exceeds a critical threshold.

    if currentTemp > 50:
        buzzer.on()
    
  2. Smart Button Functions: Long-press the button to reset the speed level to 0 or toggle automatic/manual modes.


Conclusion

The Smart Fan project demonstrates how to combine manual and automatic control in a single system. It’s a practical example of integrating sensors, motors, and user interaction into a functional and efficient design. Try enhancing it with additional features to create your personalized climate control solution!