# Reading Sensors Using the ESP32-Bit-Pirate I2C Interface: A Complete Guide

> Easily read sensors with the ESP32-Bit-Pirate I2C interface. Scan, identify, read, and write I2C sensors without low-level code. Get the complete guide.

- Repository: [Geo/ESP32-Bit-Pirate](https://github.com/geo-tp/ESP32-Bit-Pirate)
- Tags: how-to-guide
- Published: 2026-08-02

---

**The ESP32-Bit-Pirate firmware provides a terminal-driven I2C toolbox that lets you scan, identify, read, and write any I2C-compatible sensor without writing low-level driver code.**

Reading sensors via I2C on the ESP32-Bit-Pirate involves using the built-in `I2cController` class to execute interactive commands like `scan`, `read`, and `identify`. The firmware abstracts the ESP32's I2C hardware into a service-oriented architecture that handles address validation, register operations, and formatted output automatically.

## How the I2C System Is Architected

The ESP32-Bit-Pirate organizes its I2C functionality into four distinct layers, as implemented in the [geo-tp/ESP32-Bit-Pirate](https://github.com/geo-tp/ESP32-Bit-Pirate) repository:

| Layer | Responsibility | Key Source File |
|-------|---------------|---------------|
| **Command dispatcher** | Routes the `i2c` keyword to the controller | [`src/Dispatchers/ActionDispatcher.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Dispatchers/ActionDispatcher.cpp) |
| **Controller** | Parses sub-commands and orchestrates workflows | [`src/Controllers/I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/I2cController.cpp) |
| **Service layer** | Provides I2C primitives (`beginTransmission`, `requestFrom`) and advanced features | [`src/Services/I2cService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.h) |
| **Utility helpers** | Argument parsing, user input, and terminal formatting | Various files under `src/Utilities/` |

The controller uses a **command-per-function pattern**: each public method (`handleRead`, `handleWrite`, `handleScan`, `handleDump`, etc.) corresponds to one terminal sub-command. All commands follow a consistent four-step flow:

1. **Validate the address** — `tryParseAddress` (lines 212–233 in [`I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/I2cController.cpp)) ensures 7-bit address format (0x00–0x7F)
2. **Check device presence** — `i2cService.beginTransmission(addr)` followed by `endTransmission()` confirms the target ACKs
3. **Execute the operation** — delegate to `II2cService` methods; for reads, write the register pointer then call `requestFrom`
4. **Format output** — use `argTransformer.toHex` and `terminalView.println` for human-readable results

## Essential I2C Commands for Sensor Reading

### Configure Bus Settings (One-Time Setup)

Before reading any sensor, configure the SDA pin, SCL pin, and clock frequency:

```text
> i2c config
Enter SDA pin: 21
Enter SCL pin: 22
Enter frequency (Hz) [100000]: 400000

```

This persists for the session and affects all subsequent I2C operations.

### Scan for Connected Devices

The `scan` command enumerates all responsive addresses on the bus:

```text
> i2c scan
I2C Scan: Scanning I2C bus... Press [ENTER] to stop

Found device at 0x40
Found device at 0x5A
...

```

Press **Enter** to abort the scan early. The implementation (lines 70–94 in [`I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/I2cController.cpp)) tests each address with a zero-byte write and reports ACK responses.

### Identify a Specific Sensor

Once you know an address, use `identify` to match it against known device signatures:

```text
> i2c identify 0x40
📟 I2C 0x40 Identification Result
  ➤ Could be: - [SENSOR] Temperature/Humidity (e.g., SHT31)

```

The `identifyToString` method maps addresses to friendly names based on a built-in database of common sensors.

## Reading and Writing Sensor Registers

### Read a Single Register

The most common sensor operation—write a register pointer, then read the data:

```text
> i2c read 0x40 0x01
I2C Read: 0x5A (90) from reg 0x01 @ dev 0x40.

```

**What happens under the hood:** The `handleRead` method (lines 42–89) performs a write-then-read sequence: it calls `i2cService.beginTransmission(0x40)`, writes `0x01` as the register address, restarts with `requestFrom(0x40, 1)`, and returns the byte in both hex and decimal formats.

### Write to a Configuration Register

Configure sensor settings by writing to control registers:

```text
> i2c write 0x40 0x02 0x03
I2C Write: 0x03 -> reg 0x02 @ dev 0x40.

```

The `handleWrite` method validates the address, writes the register pointer followed by the data byte, and confirms successful transmission.

### Dump an Entire Register Map

For reverse engineering unknown sensors or verifying configuration:

```text
> i2c dump 0x40 64
I2C Dump: 0x40 from 0x00 for 64 bytes...
00:  3F  A2  ??  ??  7C  00  12  FF  .?.?.|...
08:  00  00  81  3E  ??  ??  00  00 ....?.>....
...

```

The `handleDump` command first attempts a **register-style read**; if the device doesn't support register pointers, it automatically falls back to `performRawRead` (raw sequential reads).

## Advanced Diagnostics and Debugging

### Health Check a Device

Verify signal integrity and bus timing for problematic sensors:

```text
> i2c health 0x40
I2C Health: Analyzing @ 0x40...
[ACK latency (ping)]
...
✅ Stable ACK
✅ Stable reads

```

The `handleHealth` method uses the service layer's `Stats` helper to measure ACK latency, read stability, and bus error rates across multiple iterations.

### Low-Level Bus Recovery

If a sensor locks the bus (SDA or SCL stuck low), trigger the bit-bang recovery routine:

```text
> i2c recover

```

This invokes `i2cBitBangRecoverBus` from the `I2cService` layer, which manually toggles SCL until SDA releases.

## Working with I2C EEPROM and Specialized Devices

For memory devices like 24Cxx EEPROMs, use the dedicated shell that reuses the controller's API:

```text
> i2c eeprom
[I2C EEPROM Shell]
eeprom> read 0x50 0x0000 16
00: 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 FF FF FF  Hello World!....

```

The `I2cEepromShell` class ([`src/Shells/I2cEepromShell.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Shells/I2cEepromShell.h)) wraps `I2cController` with page-aware read/write logic and address size handling (8-bit vs 16-bit device addresses).

## Key Source Files for Reference

| File | Purpose |
|------|---------|
| [`src/Controllers/I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/I2cController.cpp) | Implements all I2C commands: scan, read, write, dump, health, identify, jam, glitch |
| [`src/Controllers/I2cController.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/I2cController.h) | Class declaration with `handleCommand` entry point and sub-command handlers |
| [`src/Services/I2cService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.h) | Abstract `II2cService` interface exposing `beginTransmission`, `requestFrom`, `sniff`, `stats`, and bit-bang recovery |
| [`src/Dispatchers/ActionDispatcher.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Dispatchers/ActionDispatcher.cpp) | Routes terminal input starting with "i2c" to `I2cController::handleCommand` |
| [`src/Utilities/ArgTransformer.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Utilities/ArgTransformer.cpp) | Parses decimal/hex arguments and formats hex output strings |
| [`src/Utilities/UserInputManager.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Utilities/UserInputManager.cpp) | Handles interactive prompts and abort-on-Enter functionality |

## Summary

- **Reading sensors using the ESP32-Bit-Pirate I2C interface** requires no driver coding—use interactive terminal commands instead
- **The four-layer architecture** (dispatcher → controller → service → utilities) separates concerns and enables extensibility
- **Core workflow**: configure pins → scan → identify → read/write registers → optionally dump or health-check
- **All commands support abort-on-Enter** for long-running operations like scans or sniffers
- **Advanced features** include bus recovery, glitching, jamming, and EEPROM shells for specialized devices

## Frequently Asked Questions

### How do I find the I2C address of an unknown sensor?

Use the `i2c scan` command to enumerate all responsive devices. Disconnect your suspected sensor, run a scan, reconnect it, and scan again—the new address is your target. According to the [`I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/I2cController.cpp) source (lines 70–94), the scan tests addresses 0x03 through 0x77 with zero-byte transmissions.

### Can I read multiple bytes at once from a sensor register?

Yes. The `i2c read` command accepts an optional length parameter: `i2c read <addr> <reg> <len>`. The controller repeatedly calls `requestFrom` and buffers results, formatting them as space-separated hex values with ASCII preview.

### What happens if a sensor doesn't use register pointers?

Use `i2c rawread <addr> <len>` or the `dump` command—the `handleDump` implementation automatically falls back to `performRawRead` when register-style access fails. This reads sequential bytes without first writing a register address, compatible with simple I2C devices like some ADCs or port expanders.

### Why does my sensor read return `??` in dump output?

The `??` placeholder indicates a read failure (NAK during `requestFrom`). Check wiring, pull-up resistors, and bus frequency—`i2c health <addr>` diagnoses ACK timing and stability issues. The controller marks failed reads rather than stopping, so you can see partial results from problematic devices.