Reading Sensors Using the ESP32-Bit-Pirate I2C Interface: A Complete Guide
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 repository:
| Layer | Responsibility | Key Source File |
|---|---|---|
| Command dispatcher | Routes the i2c keyword to the controller |
src/Dispatchers/ActionDispatcher.cpp |
| Controller | Parses sub-commands and orchestrates workflows | src/Controllers/I2cController.cpp |
| Service layer | Provides I2C primitives (beginTransmission, requestFrom) and advanced features |
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:
- Validate the address —
tryParseAddress(lines 212–233 inI2cController.cpp) ensures 7-bit address format (0x00–0x7F) - Check device presence —
i2cService.beginTransmission(addr)followed byendTransmission()confirms the target ACKs - Execute the operation — delegate to
II2cServicemethods; for reads, write the register pointer then callrequestFrom - Format output — use
argTransformer.toHexandterminalView.printlnfor 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:
> 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:
> 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) 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:
> 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:
> 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:
> 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:
> 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:
> 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:
> 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:
> 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) 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 |
Implements all I2C commands: scan, read, write, dump, health, identify, jam, glitch |
src/Controllers/I2cController.h |
Class declaration with handleCommand entry point and sub-command handlers |
src/Services/I2cService.h |
Abstract II2cService interface exposing beginTransmission, requestFrom, sniff, stats, and bit-bang recovery |
src/Dispatchers/ActionDispatcher.cpp |
Routes terminal input starting with "i2c" to I2cController::handleCommand |
src/Utilities/ArgTransformer.cpp |
Parses decimal/hex arguments and formats hex output strings |
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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →