Project 3: I2C Sensor Station (Temperature + Humidity)

Build a reliable I2C sensor reader that discovers devices, converts raw bytes into real units, and survives missing sensors.

Quick Reference

Attribute Value
Difficulty Intermediate
Time Estimate 1–2 weekends
Main Programming Language Python (Alternatives: C, Go, Rust)
Alternative Programming Languages C, Go, Rust
Coolness Level Medium
Business Potential Medium
Prerequisites GPIO basics, Linux CLI, basic I2C wiring
Key Topics I2C bus, sensor addressing, Linux device files, calibration math

1. Learning Objectives

By completing this project, you will:

  1. Explain how I2C addressing and pull-ups allow multiple devices on one bus.
  2. Discover sensors and handle missing devices gracefully.
  3. Convert raw sensor bytes into temperature/humidity values.
  4. Implement robust retry and error handling in a polling loop.

2. All Theory Needed (Per-Concept Breakdown)

Concept 1: I2C Bus Operation, Addressing, and Linux Device Access

Fundamentals

I2C is a two-wire serial bus that uses a shared clock (SCL) and data line (SDA) with pull-up resistors. Devices communicate by addressing; the master initiates a transaction and selects a device by its 7-bit address. Because the lines are open-drain, any device can pull the line low, and the pull-ups restore it high. This makes bus wiring simple but also sensitive to electrical issues. On Linux, I2C devices appear as /dev/i2c-* files, and you can communicate with sensors using system calls or higher-level libraries. Understanding the I2C transaction sequence and the Linux user-space interface is essential for reading sensors reliably.

Deep Dive into the concept

I2C uses open-drain signaling, which means devices can only pull the line low; a pull-up resistor brings the line back high. This allows multiple devices to share the same lines without shorting each other when one device asserts a bit. The master controls SCL and starts a transaction by pulling SDA low while SCL is high (start condition), then sends a 7-bit address plus a read/write bit. The addressed slave acknowledges by pulling SDA low during the ACK clock pulse. After this, the master and slave exchange bytes, each followed by ACK/NACK. The transaction ends when SDA goes high while SCL is high (stop condition). A missing device means no ACK; a bus error usually manifests as SDA or SCL stuck low.

On the Raspberry Pi, the I2C bus is exposed as /dev/i2c-1 by default. Tools like i2cdetect scan addresses by attempting reads; if a device responds with an ACK, the tool prints its address. In code, you typically open the device file and perform ioctl calls to set the target device address, then read or write registers. Libraries like smbus2 wrap this process, but the underlying model is still the same: set address, send register pointer, read bytes.

Sensors often require a specific initialization sequence and a conversion delay. For example, a temperature/humidity sensor might require you to send a “start measurement” command, wait for conversion, then read multiple bytes that include raw measurement values and a checksum. The conversion from raw bytes to engineering units is typically a linear formula provided in the datasheet. A robust sensor loop therefore includes: device discovery, initialization, read attempt, conversion, and sanity checking. You should also handle transient I2C errors: if a read fails, log it and retry after a short delay instead of crashing.

Electrical considerations matter. Because the Pi uses 3.3V logic, your sensors must also be 3.3V compatible. Pull-up values (commonly 4.7k) affect rise time; too weak a pull-up can cause slow edges and communication errors, while too strong a pull-up increases current draw. The maximum bus length is limited by capacitance; long wires or many devices can degrade signal integrity. In practice, keep wires short and tidy, and verify with a logic analyzer if communication appears flaky.

On Linux, you can implement sensor reads in multiple ways: direct I2C access, kernel drivers, or user-space libraries. For learning, user-space access is ideal because it exposes the raw bytes and lets you implement conversion. You should still understand that kernel drivers can provide standardized interfaces (like hwmon), but those are out of scope for this project. The core concept is that I2C is deterministic but fragile: correct addressing, pull-ups, and timing matter.

How this fit on projects

This concept is central to §3 and §5.10. It also appears again in Project 11 (power measurement sensors) and Project 12 (logger input).

Definitions & key terms

  • I2C: Inter-Integrated Circuit, two-wire serial bus.
  • SDA/SCL: Data and clock lines.
  • ACK/NACK: Acknowledgement bits in I2C transactions.
  • Open-drain: Output can only pull low; pull-up resistor provides high.
  • /dev/i2c-*: Linux I2C device file interface.

Mental model diagram (ASCII)

Master (Pi) ---- SDA ----+---- Sensor A (0x44)
           ---- SCL ----+---- Sensor B (0x40)
          Pull-ups -> 3.3V

How it works (step-by-step, with invariants and failure modes)

  1. Enable I2C in config.txt (invariant: dtparam=i2c_arm=on).
  2. Connect SDA/SCL with pull-ups.
  3. Scan bus with i2cdetect.
  4. Send measurement command.
  5. Wait for conversion, then read bytes.
  6. Convert raw values to units.

Failure modes:

  • Missing ACK -> wrong address or wiring.
  • Stuck lines -> short or wrong pull-up.
  • Wrong conversion -> misread datasheet formula.

Minimal concrete example

bus = SMBus(1)
addr = 0x44
bus.write_i2c_block_data(addr, 0x2C, [0x06])
time.sleep(0.02)
data = bus.read_i2c_block_data(addr, 0x00, 6)

Common misconceptions

  • “Any pull-up will work.” Too weak or too strong can break communication.
  • “I2C is always slower than SPI.” It depends on bus speed and payload size.
  • “If i2cdetect sees it, reading will be fine.” Not always; reads may still fail.

Check-your-understanding questions

  1. Why are pull-up resistors required on I2C?
  2. What does a missing ACK indicate?
  3. Why must you wait after a measurement command?

Check-your-understanding answers

  1. Lines are open-drain; pull-ups define logic high.
  2. The addressed device did not respond or is miswired.
  3. The sensor needs time to perform conversion.

Real-world applications

  • Environmental sensors in HVAC systems.
  • Battery monitors and power meters.

Where you’ll apply it

  • This project: §3.2 requirements, §5.10 Phase 2.
  • Other projects: Project 11.

References

  • “The Book of I2C” — bus fundamentals.
  • “The Linux Programming Interface” — device file I/O.

Key insights

I2C is easy to wire but unforgiving of timing and electrical mistakes.

Summary

I2C combines simple wiring with strict protocol rules. Understanding ACKs, pull-ups, and Linux access lets you read sensors reliably.

Homework/Exercises to practice the concept

  1. Use i2cdetect to scan and document all devices.
  2. Intentionally remove a pull-up and observe bus failure.
  3. Implement a retry loop and measure error rate.

Solutions to the homework/exercises

  1. Record addresses and confirm they match datasheets.
  2. Without pull-ups, lines float and devices do not ACK.
  3. A retry loop should reduce transient errors without blocking.

3. Project Specification

3.1 What You Will Build

A sensor station that reads temperature and humidity via I2C, logs the results, and handles missing sensors gracefully.

3.2 Functional Requirements

  1. Scan the I2C bus and detect the sensor address.
  2. Read temperature and humidity at a fixed interval.
  3. Convert raw bytes to engineering units.
  4. Handle missing sensor or read errors without crashing.

3.3 Non-Functional Requirements

  • Performance: Read cycle completes within 200 ms.
  • Reliability: 99% of reads succeed over 1 hour.
  • Usability: Clear logs and error messages.

3.4 Example Usage / Output

$ ./sensor_station
I2C device found at 0x44
[2026-01-01 09:12:01] Temp: 22.6 C  Humidity: 45.2 %

3.5 Data Formats / Schemas / Protocols

CSV log format:

2026-01-01T09:12:01Z,22.6,45.2

3.6 Edge Cases

  • Device not present.
  • Bus stuck low.
  • Out-of-range temperature from raw data.

3.7 Real World Outcome

Sensor data is visible in logs and continues even if the sensor disconnects temporarily.

3.7.1 How to Run (Copy/Paste)

python3 sensor_station.py --addr 0x44 --interval 10 --log data.csv

3.7.2 Golden Path Demo (Deterministic)

export FIXED_TIME="2026-01-01T09:12:01Z"
python3 sensor_station.py --simulate --raw "0x66 0x6F 0x80 0x5A 0x00 0x00"

Expected output:

[2026-01-01T09:12:01Z] Temp: 22.6 C  Humidity: 45.2 %

3.7.3 Failure Demo (Deterministic)

python3 sensor_station.py --addr 0x55

Expected output:

[ERROR] I2C device 0x55 not found

Exit code: 31

3.7.4 CLI Exit Codes

  • 0: Success
  • 30: I2C bus init failure
  • 31: Device not found
  • 32: Read error

4. Solution Architecture

4.1 High-Level Design

I2C Bus -> Sensor Read -> Conversion -> Logger -> Console Output

4.2 Key Components

| Component | Responsibility | Key Decisions | |—|—|—| | Bus Scanner | Detect devices | Scan range and retries | | Sensor Driver | Read and convert | Datasheet formula | | Logger | Store readings | CSV vs JSON |

4.3 Data Structures (No Full Code)

reading = {"temp_c": 22.6, "humidity": 45.2, "ts": ts}

4.4 Algorithm Overview

Key Algorithm: Read-Convert-Log Loop

  1. Trigger measurement.
  2. Read raw bytes.
  3. Convert to units.
  4. Log or retry.

Complexity Analysis:

  • Time: O(1) per reading
  • Space: O(1)

5. Implementation Guide

5.1 Development Environment Setup

sudo apt-get install -y python3-smbus i2c-tools

5.2 Project Structure

project-root/
├── sensor_station.py
├── sensor_driver.py
└── README.md

5.3 The Core Question You’re Answering

“How does a shared two-wire bus let multiple sensors coexist without confusion?”

5.4 Concepts You Must Understand First

  1. I2C start/stop conditions and ACK.
  2. Pull-ups and electrical constraints.
  3. Sensor conversion formulas.

5.5 Questions to Guide Your Design

  1. What sampling interval avoids sensor self-heating?
  2. How will you retry without blocking the loop?

5.6 Thinking Exercise

Draw an I2C transaction for a single sensor read.

5.7 The Interview Questions They’ll Ask

  1. Why are pull-ups required on I2C?
  2. What is the difference between I2C and SPI?
  3. How do you detect a missing I2C device?

5.8 Hints in Layers

Hint 1: Use i2cdetect -y 1 to confirm the address.

Hint 2: Print raw bytes before conversion.

Hint 3: Add retries and a timeout counter.

5.9 Books That Will Help

| Topic | Book | Chapter | |—|—|—| | I2C fundamentals | The Book of I2C | Ch. 1–3 | | Linux I/O | The Linux Programming Interface | Ch. 13 |

5.10 Implementation Phases

Phase 1: Bus Discovery (2 hours)

  • Enable I2C and scan the bus.

Phase 2: Sensor Read (4 hours)

  • Implement conversion formula and logging.

Phase 3: Robustness (2 hours)

  • Add retries, missing-device handling.

5.11 Key Implementation Decisions

| Decision | Options | Recommendation | Rationale | |—|—|—|—| | Logging format | CSV / JSON | CSV | Simple and efficient | | Retry policy | Immediate / backoff | Backoff | Avoid bus thrashing |


6. Testing Strategy

6.1 Test Categories

| Category | Purpose | Examples | |—|—|—| | Unit Tests | Conversion math | Raw bytes -> Celsius | | Integration Tests | Full read loop | Sensor connected | | Edge Case Tests | Missing device | Wrong address |

6.2 Critical Test Cases

  1. Correct address returns valid reading.
  2. Wrong address exits with code 31.
  3. Corrupted data is detected by range check.

6.3 Test Data

Raw bytes: 0x66 0x6F 0x80 0x5A 0x00 0x00

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

| Pitfall | Symptom | Solution | |—|—|—| | No pull-ups | Bus hangs | Add 4.7k resistors | | Wrong address | Device not found | Scan bus | | Bad conversion | Nonsense values | Re-check datasheet |

7.2 Debugging Strategies

  • Use i2cdetect and i2cget to verify hardware.
  • Log raw bytes before conversion.

7.3 Performance Traps

  • Reading too frequently can heat the sensor and skew readings.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a second sensor on the bus.

8.2 Intermediate Extensions

  • Add calibration offsets in config file.

8.3 Advanced Extensions

  • Implement CRC checking if supported by sensor.

9. Real-World Connections

9.1 Industry Applications

  • Environmental monitoring, HVAC, greenhouse control.
  • bme280 Linux driver and user-space libraries.

9.3 Interview Relevance

  • I2C and sensor interfacing are common embedded topics.

10. Resources

10.1 Essential Reading

  • Sensor datasheet and I2C protocol documentation.

10.2 Video Resources

  • I2C protocol explained (educational videos).

10.3 Tools & Documentation

  • i2cdetect, i2cget man pages.

11. Self-Assessment Checklist

11.1 Understanding

  • I can describe an I2C transaction and ACK behavior.
  • I can explain why pull-ups are needed.

11.2 Implementation

  • Sensor readings are stable and logged.
  • Missing device is handled gracefully.

11.3 Growth

  • I can explain I2C tradeoffs in an interview.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • Sensor detected and one valid reading logged.

Full Completion:

  • Robust logging with retries and error handling.

Excellence (Going Above & Beyond):

  • CRC validation and calibration pipeline.