split dht sensor, add oled impl, add button triggered display example

This commit is contained in:
OMGeeky
2025-05-07 20:19:42 +02:00
parent f5af3419d5
commit 46f4e5a95e
9 changed files with 803 additions and 22 deletions

View File

@@ -0,0 +1,89 @@
# Button-Triggered Sensor Display
This document explains how to use the button-triggered sensor display example, which demonstrates an energy-efficient approach to reading and displaying sensor data on ESP32/ESP8266 devices.
## Overview
The button-triggered display example shows how to:
1. Set up a button input on an ESP device
2. Use low-power sleep mode to conserve energy
3. Wake up and read sensor data when the button is pressed
4. Display the data on an OLED screen
This approach is ideal for battery-powered applications where energy conservation is important.
## Hardware Requirements
- ESP32 or ESP8266 development board
- DHT22 temperature and humidity sensor
- SSD1306 OLED display (128x64 pixels recommended)
- Pushbutton
- 10K pull-up resistor (if your button doesn't have an internal pull-up)
- Breadboard and jumper wires
## Wiring
1. **DHT22 Sensor**:
- Connect VCC to 3.3V
- Connect GND to ground
- Connect DATA to GPIO4 (or change the pin in the code)
2. **OLED Display**:
- Connect VCC to 3.3V
- Connect GND to ground
- Connect SCL to GPIO22 (or change the pin in the code)
- Connect SDA to GPIO21 (or change the pin in the code)
3. **Button**:
- Connect one side to GPIO0 (or change the pin in the code)
- Connect the other side to ground
- Connect a 10K pull-up resistor between GPIO0 and 3.3V (if not using internal pull-up)
## Running the Example
1. Flash MicroPython to your ESP device if you haven't already
2. Upload the `button_triggered_display.py` script to your device
3. Run the script
```python
import button_triggered_display
button_triggered_display.main()
```
## How It Works
### Energy Conservation
The example uses ESP32's light sleep mode to conserve energy when not actively reading or displaying data. In light sleep mode:
- The CPU is paused
- RAM is preserved
- Peripherals can be configured to wake the device
- Power consumption is significantly reduced
### Button Wake-Up
The device is configured to wake up from sleep when the button is pressed. This is done using the `wake_on_ext0` function, which allows an external pin to trigger a wake-up event.
### Simulation Mode
The example includes a simulation mode that runs when not on actual ESP hardware. This allows you to test the functionality on a development computer before deploying to the ESP device.
## Customization
You can customize the example by:
1. Changing the GPIO pins for the sensor, display, or button
2. Adjusting the display time before going back to sleep
3. Adding additional sensors
4. Modifying the information displayed on the OLED screen
## Power Consumption
Typical power consumption in different states:
- Active mode (reading sensors and updating display): ~80-120mA
- Light sleep mode: ~0.8-1.5mA
This represents a power saving of approximately 98% during idle periods, significantly extending battery life.

165
docs/oled_display.md Normal file
View File

@@ -0,0 +1,165 @@
# OLED Display Module
This module provides a class for interfacing with SSD1306 OLED displays via I2C on ESP32/ESP8266 microcontrollers.
## Features
- Compatible with SSD1306 OLED displays
- I2C interface support
- Display text at specific coordinates
- Display a list of values (e.g., sensor readings)
- Simulation mode for testing without hardware
- Integration with the ESP Sensors framework
## Hardware Requirements
- ESP32 or ESP8266 microcontroller
- SSD1306 OLED display (common sizes: 128x64, 128x32, 64x48)
- I2C connection (2 pins: SCL and SDA)
## Installation
The OLED display module is part of the ESP Sensors package. No additional installation is required if you have already installed the package.
## Usage
### Basic Initialization
```python
from esp_sensors.oled_display import OLEDDisplay
# Initialize the display
display = OLEDDisplay(
name="Status Display",
scl_pin=22, # GPIO pin for I2C clock
sda_pin=21, # GPIO pin for I2C data
width=128, # Display width in pixels
height=64, # Display height in pixels
address=0x3C # I2C address (default: 0x3C)
)
```
### Displaying Text
```python
# Clear the display
display.clear()
# Display text at specific coordinates
display.display_text("Hello, World!", x=0, y=0)
display.display_text("Line 2", x=0, y=10)
display.display_text("ESP32", x=64, y=30)
```
### Displaying Multiple Values
```python
# Display a list of values (e.g., sensor readings)
display.display_values([
"Temperature: 25.5°C",
"Humidity: 45%",
"Pressure: 1013 hPa",
"Time: 12:34:56"
])
```
### Integration with Sensors
```python
from esp_sensors.dht22 import DHT22Sensor
# Initialize a DHT22 sensor
sensor = DHT22Sensor("Living Room", pin=4)
# Read sensor values
temperature = sensor.read_temperature()
humidity = sensor.read_humidity()
# Display sensor values
display.display_values([
f"Temp: {temperature:.1f}°C",
f"Humidity: {humidity:.1f}%"
])
```
## API Reference
### Class: OLEDDisplay
#### Constructor
```python
OLEDDisplay(name, scl_pin, sda_pin, width=128, height=64, address=0x3C, interval=60)
```
Parameters:
- `name` (str): The name of the display
- `scl_pin` (int): The GPIO pin number for the SCL (clock) line
- `sda_pin` (int): The GPIO pin number for the SDA (data) line
- `width` (int): Display width in pixels (default: 128)
- `height` (int): Display height in pixels (default: 64)
- `address` (int): I2C address of the display (default: 0x3C)
- `interval` (int): Refresh interval in seconds (default: 60)
#### Methods
##### clear()
Clears the display.
```python
display.clear()
```
##### display_text(text, x=0, y=0, color=1)
Displays text at the specified position.
Parameters:
- `text` (str): The text to display
- `x` (int): X coordinate (default: 0)
- `y` (int): Y coordinate (default: 0)
- `color` (int): Pixel color (1 for white, 0 for black, default: 1)
```python
display.display_text("Hello", x=10, y=20)
```
##### display_values(values)
Displays a list of values on the OLED screen.
Parameters:
- `values` (list): List of values to display (strings or objects with __str__ method)
```python
display.display_values(["Line 1", "Line 2", "Line 3"])
```
##### get_metadata()
Returns a dictionary containing display metadata.
```python
metadata = display.get_metadata()
print(metadata)
```
## Troubleshooting
### Display Not Working
1. Check the I2C address (common addresses are 0x3C and 0x3D)
2. Verify the SCL and SDA pin connections
3. Ensure the display is powered correctly (usually 3.3V)
4. Try a different I2C bus speed if available
### Text Not Displaying Correctly
1. Check that the coordinates are within the display dimensions
2. Ensure the text doesn't exceed the display width
3. Try using smaller font or breaking text into multiple lines
## Example
See the `examples/oled_display_example.py` file for a complete example of using the OLED display with sensors.

View File

@@ -0,0 +1,137 @@
"""
Example of an energy-efficient sensor display that activates on button press.
This example demonstrates how to:
1. Set up a button input
2. Use low-power sleep mode to conserve energy
3. Wake up and read sensor data when the button is pressed
4. Display the data on an OLED screen
"""
import time
import sys
import os
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from src.esp_sensors.oled_display import OLEDDisplay
from src.esp_sensors.dht22 import DHT22Sensor
# Import hardware-specific modules if available (for ESP32/ESP8266)
try:
from machine import Pin, deepsleep
import esp32
SIMULATION = False
except ImportError:
# Simulation mode for development on non-ESP hardware
SIMULATION = True
print("Running in simulation mode - hardware functions will be simulated")
def simulate_button_press():
"""Simulate a button press in simulation mode."""
print("\nPress Enter to simulate a button press (or 'q' to quit, Ctrl+C to exit)...")
try:
user_input = input()
if user_input.lower() == 'q':
return False
return True
except KeyboardInterrupt:
return False
def main():
"""
Main function to demonstrate button-triggered sensor display.
"""
# Initialize a DHT22 sensor
dht_sensor = DHT22Sensor(
name="Living Room",
pin=4, # GPIO pin for DHT22 data
interval=5, # Read every 5 seconds
unit="C" # Celsius
)
# Initialize an OLED display
display = OLEDDisplay(
name="Status Display",
scl_pin=22, # GPIO pin for I2C clock
sda_pin=21, # GPIO pin for I2C data
width=128, # Display width in pixels
height=64, # Display height in pixels
interval=1 # Update every second
)
# Set up button on GPIO pin 0 (common button pin on many ESP boards)
button_pin = 0
if not SIMULATION:
button = Pin(button_pin, Pin.IN, Pin.PULL_UP)
# Display initialization message
display.clear()
display.display_text("Ready - Press Button", 0, 0)
print("System initialized. Waiting for button press...")
# Main loop - sleep until button press, then read and display sensor data
try:
while True:
# Wait for button press
if SIMULATION:
# In simulation mode, wait for Enter key
if not simulate_button_press():
break # Exit if Ctrl+C was pressed
else:
# In hardware mode, check if button is pressed (active low)
if button.value() == 1: # Button not pressed
# Go to light sleep mode to save power
# Wake up on pin change (button press)
print("Entering light sleep mode...")
esp32.wake_on_ext0(pin=button, level=0) # Wake on button press (low)
esp32.light_sleep() # Light sleep preserves RAM but saves power
# When we get here, the button was pressed
print("Button pressed! Reading sensor data...")
# Read sensor values
temperature = dht_sensor.read_temperature()
humidity = dht_sensor.read_humidity()
# Format values for display
temp_str = f"Temp: {temperature:.1f} C"
hum_str = f"Humidity: {humidity:.1f}%"
time_str = f"Time: {time.time():.0f}"
name_str = f"Sensor: {dht_sensor.name}"
# Display values
display.display_values([
name_str,
temp_str,
hum_str,
time_str,
"Press button again"
])
# Print to console
print(f"Updated display with: {temp_str}, {hum_str}")
# Keep display on for a few seconds before going back to sleep
time.sleep(5)
# Clear display to save power
display.clear()
display.display_text("Ready - Press Button", 0, 0)
if SIMULATION:
print("Display cleared. Ready for next button press.")
except KeyboardInterrupt:
# Clean up on exit
display.clear()
display.display_text("Shutting down...", 0, 0)
time.sleep(1)
display.clear()
print("Program terminated by user")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,84 @@
"""
Example usage of the OLED display with temperature and humidity sensors.
"""
import time
import sys
import os
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from src.esp_sensors.oled_display import OLEDDisplay
from src.esp_sensors.dht22 import DHT22Sensor
def main():
"""
Main function to demonstrate OLED display usage with sensors.
"""
# Initialize a DHT22 sensor
dht_sensor = DHT22Sensor(
name="Living Room",
pin=4, # GPIO pin for DHT22 data
interval=5, # Read every 5 seconds
unit="C" # Celsius
)
# Initialize an OLED display
display = OLEDDisplay(
name="Status Display",
scl_pin=22, # GPIO pin for I2C clock
sda_pin=21, # GPIO pin for I2C data
width=128, # Display width in pixels
height=64, # Display height in pixels
interval=1 # Update every second
)
# Display initialization message
display.clear()
display.display_text("Initializing...", 0, 0)
time.sleep(2)
# Main loop - run for 5 iterations as a demonstration
try:
print("Starting demonstration (5 iterations)...")
for i in range(5):
print(f"\nIteration {i+1}/5:")
# Read sensor values
temperature = dht_sensor.read_temperature()
humidity = dht_sensor.read_humidity()
# Format values for display
temp_str = f"Temp: {temperature:.1f} C"
hum_str = f"Humidity: {humidity:.1f}%"
time_str = f"Time: {time.time():.0f}"
name_str = f"Sensor: {dht_sensor.name}"
# Display values
display.display_values([
name_str,
temp_str,
hum_str,
time_str,
f"Demo ({i+1}/5)"
])
# Print to console in simulation mode
print(f"Updated display with: {temp_str}, {hum_str}")
# Wait for next update
print(f"Waiting {display.interval} second(s)...")
time.sleep(display.interval)
except KeyboardInterrupt:
# Clean up on exit
display.clear()
display.display_text("Shutting down...", 0, 0)
time.sleep(1)
display.clear()
print("Program terminated by user")
if __name__ == "__main__":
main()

View File

@@ -10,10 +10,11 @@ except ImportError:
import random
SIMULATION = True
from .sensor import Sensor
from .temperature import TemperatureSensor
from .humidity import HumiditySensor
class DHT22Sensor(Sensor):
class DHT22Sensor(TemperatureSensor, HumiditySensor):
"""DHT22 temperature and humidity sensor implementation."""
def __init__(self, name: str, pin: int, interval: int = 60, unit: str = "C"):
@@ -26,17 +27,15 @@ class DHT22Sensor(Sensor):
interval: Reading interval in seconds (default: 60)
unit: Temperature unit, either "C" for Celsius or "F" for Fahrenheit (default: "C")
"""
super().__init__(name, pin, interval)
if unit not in ["C", "F"]:
raise ValueError("Unit must be either 'C' or 'F'")
self.unit = unit
self._last_humidity = None
# Initialize both parent classes
TemperatureSensor.__init__(self, name, pin, interval, unit)
HumiditySensor.__init__(self, name, pin, interval)
# Initialize the sensor if not in simulation mode
if not SIMULATION:
self._sensor = dht.DHT22(Pin(pin))
def read(self) -> float:
def read_temperature(self) -> float:
"""
Read the current temperature.
@@ -44,14 +43,11 @@ class DHT22Sensor(Sensor):
The temperature reading as a float
"""
if SIMULATION:
# Simulate temperature reading
if self.unit == "C":
self._last_reading = round(random.uniform(15.0, 30.0), 1)
else:
self._last_reading = round(random.uniform(59.0, 86.0), 1)
# Simulate humidity reading (between 30% and 90%)
self._last_humidity = round(random.uniform(30.0, 90.0), 1)
# Use parent class simulation for temperature
temp_reading = super().read_temperature()
# Also update humidity in simulation mode
self._last_humidity = super().read_humidity()
return temp_reading
else:
# Actual hardware reading
try:
@@ -63,6 +59,7 @@ class DHT22Sensor(Sensor):
temp = (temp * 9 / 5) + 32
self._last_reading = round(temp, 1)
# Also read humidity while we're at it
self._last_humidity = round(self._sensor.humidity(), 1)
except Exception as e:
print(f"Error reading DHT22 sensor: {e}")
@@ -74,6 +71,15 @@ class DHT22Sensor(Sensor):
return self._last_reading
def read(self) -> float:
"""
Read the current temperature (wrapper for read_temperature).
Returns:
The temperature reading as a float
"""
return self.read_temperature()
def read_humidity(self) -> float:
"""
Read the current humidity.
@@ -84,8 +90,8 @@ class DHT22Sensor(Sensor):
# If we haven't read yet, read only humidity
if self._last_humidity is None:
if SIMULATION:
# Simulate humidity reading (between 30% and 90%)
self._last_humidity = round(random.uniform(30.0, 90.0), 1)
# Use parent class simulation
return super().read_humidity()
else:
# Actual hardware reading
try:
@@ -104,9 +110,13 @@ class DHT22Sensor(Sensor):
Returns:
A dictionary containing sensor metadata
"""
metadata = super().get_metadata()
metadata["unit"] = self.unit
metadata["last_humidity"] = self._last_humidity
# Get metadata from TemperatureSensor
temp_metadata = TemperatureSensor.get_metadata(self)
# Get metadata from HumiditySensor
humidity_metadata = HumiditySensor.get_metadata(self)
# Combine metadata from both parent classes
metadata = {**temp_metadata, **humidity_metadata}
metadata["type"] = "DHT22"
return metadata

View File

@@ -0,0 +1,44 @@
"""
Humidity sensor module for ESP-based sensors.
"""
import random
from .sensor import Sensor
class HumiditySensor(Sensor):
"""Humidity sensor implementation."""
def __init__(self, name: str, pin: int, interval: int = 60):
"""
Initialize a new humidity sensor.
Args:
name: The name of the sensor
pin: The GPIO pin number the sensor is connected to
interval: Reading interval in seconds (default: 60)
"""
super().__init__(name, pin, interval)
self._last_humidity = None
def read_humidity(self) -> float:
"""
Read the current humidity.
Returns:
The humidity reading as a float (percentage)
"""
# This is a simulation for testing purposes
# In a real implementation, this would read from the actual sensor
self._last_humidity = round(random.uniform(30.0, 90.0), 1)
return self._last_humidity
def get_metadata(self):
"""
Get sensor metadata including humidity information.
Returns:
A dictionary containing sensor metadata
"""
metadata = super().get_metadata()
metadata["last_humidity"] = self._last_humidity
return metadata

View File

@@ -0,0 +1,136 @@
"""
OLED display module for ESP32 using SSD1306 controller.
"""
import time
try:
from machine import Pin, I2C
import ssd1306
SIMULATION = False
except ImportError:
SIMULATION = True
from .sensor import Sensor
class OLEDDisplay(Sensor):
"""SSD1306 OLED display implementation."""
def __init__(self, name: str, scl_pin: int, sda_pin: int, width: int = 128, height: int = 64,
address: int = 0x3C, interval: int = 60):
"""
Initialize a new OLED display.
Args:
name: The name of the display
scl_pin: The GPIO pin number for the SCL (clock) line
sda_pin: The GPIO pin number for the SDA (data) line
width: Display width in pixels (default: 128)
height: Display height in pixels (default: 64)
address: I2C address of the display (default: 0x3C)
interval: Refresh interval in seconds (default: 60)
"""
# Use sda_pin as the pin parameter for the Sensor base class
super().__init__(name, sda_pin, interval)
self.scl_pin = scl_pin
self.sda_pin = sda_pin
self.width = width
self.height = height
self.address = address
self._values = []
# Initialize the display if not in simulation mode
if not SIMULATION:
try:
i2c = I2C(0, scl=Pin(scl_pin), sda=Pin(sda_pin))
self._display = ssd1306.SSD1306_I2C(width, height, i2c, addr=address)
self._display.fill(0) # Clear the display
self._display.text("Initialized", 0, 0, 1)
self._display.show()
except Exception as e:
print(f"Error initializing OLED display: {e}")
self._display = None
else:
# In simulation mode, just print to console
print(f"Simulated OLED display initialized: {width}x{height}")
self._display = None
def clear(self):
"""
Clear the display.
"""
if SIMULATION:
print("Simulated OLED display cleared")
else:
if self._display:
self._display.fill(0)
self._display.show()
def display_text(self, text: str, x: int = 0, y: int = 0, color: int = 1):
"""
Display text at the specified position.
Args:
text: The text to display
x: X coordinate (default: 0)
y: Y coordinate (default: 0)
color: Pixel color (1 for white, 0 for black, default: 1)
"""
if SIMULATION:
print(f"Simulated OLED display text at ({x}, {y}): {text}")
else:
if self._display:
self._display.text(text, x, y, color)
self._display.show()
def display_values(self, values: list):
"""
Display a list of values on the OLED screen.
Args:
values: List of values to display (strings or objects with __str__ method)
"""
self._values = values
if SIMULATION:
print("Simulated OLED display values:")
for i, value in enumerate(values):
print(f" Line {i}: {value}")
else:
if self._display:
self._display.fill(0) # Clear the display
# Display each value on a new line (8 pixels per line)
for i, value in enumerate(values):
if i * 10 < self.height: # Make sure we don't go off the screen
self._display.text(str(value), 0, i * 10, 1)
self._display.show()
def read(self) -> float:
"""
Update the display (placeholder to satisfy Sensor interface).
Returns:
Always returns 1.0 to indicate success
"""
# This method is required by the Sensor interface but doesn't make sense for a display
# We'll just return a constant value
return 1.0
def get_metadata(self):
"""
Get display metadata.
Returns:
A dictionary containing display metadata
"""
metadata = super().get_metadata()
metadata["scl_pin"] = self.scl_pin
metadata["sda_pin"] = self.sda_pin
metadata["width"] = self.width
metadata["height"] = self.height
metadata["address"] = self.address
metadata["type"] = "SSD1306"
metadata["values_count"] = len(self._values)
return metadata

View File

@@ -23,7 +23,7 @@ class TemperatureSensor(Sensor):
raise ValueError("Unit must be either 'C' or 'F'")
self.unit = unit
def read(self) -> float:
def read_temperature(self) -> float:
"""
Read the current temperature.
@@ -38,6 +38,15 @@ class TemperatureSensor(Sensor):
self._last_reading = round(random.uniform(59.0, 86.0), 1)
return self._last_reading
def read(self) -> float:
"""
Read the current temperature (wrapper for read_temperature).
Returns:
The temperature reading as a float
"""
return self.read_temperature()
def get_metadata(self):
"""
Get sensor metadata including temperature unit.

107
tests/test_oled_display.py Normal file
View File

@@ -0,0 +1,107 @@
"""
Tests for the OLED display module.
"""
import pytest
from src.esp_sensors.oled_display import OLEDDisplay
def test_oled_display_initialization():
"""Test that an OLED display can be initialized with valid parameters."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
assert display.name == "test_display"
assert display.scl_pin == 22
assert display.sda_pin == 21
assert display.pin == 21 # pin in base class is set to sda_pin
assert display.width == 128
assert display.height == 64
assert display.address == 0x3C
assert display.interval == 60
assert display._values == []
def test_oled_display_custom_parameters():
"""Test that an OLED display can be initialized with custom parameters."""
display = OLEDDisplay(
"custom_display",
scl_pin=22,
sda_pin=21,
width=64,
height=32,
address=0x3D,
interval=30
)
assert display.name == "custom_display"
assert display.scl_pin == 22
assert display.sda_pin == 21
assert display.width == 64
assert display.height == 32
assert display.address == 0x3D
assert display.interval == 30
def test_oled_display_read():
"""Test that reading from the display returns a success value."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
reading = display.read()
assert reading == 1.0
def test_oled_display_metadata():
"""Test that metadata includes the display parameters."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
metadata = display.get_metadata()
assert metadata["name"] == "test_display"
assert metadata["pin"] == 21
assert metadata["scl_pin"] == 22
assert metadata["sda_pin"] == 21
assert metadata["width"] == 128
assert metadata["height"] == 64
assert metadata["address"] == 0x3C
assert metadata["interval"] == 60
assert metadata["type"] == "SSD1306"
assert metadata["values_count"] == 0
def test_oled_display_values():
"""Test that display values are stored correctly."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
# Test with empty values
assert display._values == []
# Test with string values
test_values = ["Temperature: 25.0°C", "Humidity: 45.0%"]
display.display_values(test_values)
assert display._values == test_values
# Check that metadata reflects the number of values
metadata = display.get_metadata()
assert metadata["values_count"] == 2
def test_oled_display_clear():
"""Test that clearing the display works in simulation mode."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
# This is mostly a coverage test since we can't check the actual display in tests
display.clear()
# Verify that clearing doesn't affect stored values
test_values = ["Temperature: 25.0°C"]
display.display_values(test_values)
display.clear()
assert display._values == test_values
def test_oled_display_text():
"""Test that displaying text works in simulation mode."""
display = OLEDDisplay("test_display", scl_pin=22, sda_pin=21)
# This is mostly a coverage test since we can't check the actual display in tests
display.display_text("Hello, World!")
# Verify that displaying text doesn't affect stored values
test_values = ["Temperature: 25.0°C"]
display.display_values(test_values)
display.display_text("Hello, World!")
assert display._values == test_values