METF Python Client
HTTP client library for testing hardware using ESP boards (NodeMCU, WeMos, ESP32C6)
Test your embedded hardware through HTTP requests. Perfect for Continuous Integration in hardware development.
Features
- Digital I/O Control: pinMode, digitalRead, digitalWrite operations
- I2C Communication: Full I2C master support (begin, ask, setClock, setClockStretchLimit)
- Serial Port Sniffing: Read and monitor serial output from connected devices
- RGB LED Control: Control RGB LEDs connected to ESP boards
- Multiple Board Support: NodeMCU, WeMos D1 Mini, ESP32C6 Super Mini
- Simple HTTP API: Test hardware from any language/platform that supports HTTP
- Hardware CI/CD: Integrate hardware tests into your continuous integration pipeline
Installation
pip install metf-python-client
Quick Start
1. Setup Hardware
- Connect your MCU/device to an ESP board (NodeMCU, WeMos, or ESP32C6)
- Flash METF firmware to the ESP board
- Connect ESP to your network (note its IP address)
2. Write Test Script
from metf_python_client import METFClient
from metf_python_client.boards.nodemcu import LED_BUILTIN_AUX, HIGH, LOW, OUTPUT
# Connect to ESP board
api = METFClient('192.168.1.100')
# Test connectivity
api.ping() # Returns 'pong'
# Control GPIO
pin = LED_BUILTIN_AUX
api.pinMode(pin, OUTPUT)
api.digitalWrite(pin, LOW)
assert api.digitalRead(pin) == LOW
api.delay(1000)
api.digitalWrite(pin, HIGH)
assert api.digitalRead(pin) == HIGH
API Reference
Connection
from metf_python_client import METFClient
# Connect to ESP board
api = METFClient(host='192.168.1.100', port=80, timeout=3.0)
# Test connection
api.ping() # Returns 'pong' if successful
Digital I/O
from metf_python_client.boards.nodemcu import D1, D2, D3, INPUT, OUTPUT, INPUT_PULLUP, HIGH, LOW
from metf_python_client import METFClient
api = METFClient("192.168.1.2") # nodemcu active point with METF firmware
# Set pin mode
api.pinMode(D1, OUTPUT)
api.pinMode(D2, INPUT)
api.pinMode(D3, INPUT_PULLUP)
# Write digital value
api.digitalWrite(D1, HIGH)
api.digitalWrite(D1, LOW)
# Read digital value
value = api.digitalRead(D2) # Returns 0 or 1
# Wait for specific value (with timeout)
button_pressed = api.wait_digital(D2, LOW, timeout=5.0)
if button_pressed:
print("Button was pressed within 5 seconds")
I2C Communication
from metf_python_client.boards.nodemcu import D3, D4
# Initialize I2C (default pins)
api.i2c_begin() # Use default SDA/SCL for your board
# OR initialize with custom pins
api.i2c_begin(sda=D3, scl=D4)
# Set clock speed
api.i2c_setClock(400000) # 400kHz
# Set clock stretch limit (ESP8266 specific)
api.i2c_setClockStretchLimit(1500)
# Send message and receive response
slave_address = 0x48
message = 'M' # Send byte 0x4D
response_length = 1
response = api.i2c_ask(slave_address, message, response_length)
print(f"Received: {ord(response[0])}")
# Flush I2C buffer
api.i2c_flush()
Working with Binary Data
from metf_python_client.utils import DataStruct
# Define structure format
fields = [
('version', 'B'), # unsigned char
('value_uint16', 'H'), # unsigned short
('value_uint32', 'L'), # unsigned long
]
# Calculate structure size
header_len = DataStruct.calcsize(fields)
# Read from I2C device
response = api.i2c_ask(slave_address, 'A', header_len)
# Parse binary data
header = DataStruct(fields, response)
print(f"Version: {header.version}")
print(f"Value 16: {header.value_uint16}")
print(f"Value 32: {header.value_uint32}")
Serial Port Sniffing
# Initialize serial port
api.serial_begin(baudrate=115200)
# Read available data
data = api.serial_read() # Returns string or empty string
if data:
print(f"Received: {data}")
# Read lines with timeout and parsing
lines = api.serial_readlines(
wait=5000, # Wait up to 5000ms
delimiter='\n', # Split by newline
prefix='00:' # Merge lines starting with this prefix
)
for line in lines:
print(f"Line: {line}")
# Flush serial buffer
api.serial_flush()
# Check the log ring before trusting what was read (METF 5+)
stat = api.serial_stat()
assert stat['dropped'] == 0, f"log lost {stat['dropped']} lines"
serial_stat() returns the state of the ring buffer the board keeps the log in:
| field | meaning |
|---|---|
lines |
lines waiting to be read |
dropped |
lines evicted since the last flush because the ring was full |
baud |
current speed of the serial port |
capacity |
how many lines the ring holds |
line_len |
characters per line, including the terminator |
bytes |
total size of the ring |
dropped > 0 means the ring overflowed before anyone read it: serial_read()
returns a shorter log, not an error, so a test written on top of it passes for
the wrong reason. Either read more often, or build the firmware with a larger
ASB_BUFFER_BYTES.
RGB LED Control
# Initialize RGB LED
api.rgb_begin()
# Set brightness (0-255)
api.rgb_brightness(128)
# Set color (hex string)
api.rgb_color('FF0000') # Red
api.rgb_color('00FF00') # Green
api.rgb_color('0000FF') # Blue
api.rgb_color('FFFFFF') # White
Utility Functions
# Blink LED
api.blynk(
pin=LED_BUILTIN,
duration=1000, # Duration in milliseconds
invert=False, # Invert logic (for active-low LEDs)
low=0, # Low value
high=1 # High value
)
# Delay
api.delay(1000) # Delay 1000ms (1 second)
Board Pin Definitions
Import board-specific pin mappings and constants:
NodeMCU
from metf_python_client.boards.nodemcu import (
D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10,
LED_BUILTIN, LED_BUILTIN_AUX,
HIGH, LOW,
INPUT, OUTPUT, INPUT_PULLUP
)
WeMos D1 Mini
from metf_python_client.boards.wemos import (
D0, D1, D2, D3, D4, D5, D6, D7, D8,
LED_BUILTIN,
HIGH, LOW,
INPUT, OUTPUT, INPUT_PULLUP
)
ESP32C6 Super Mini
from metf_python_client.boards.esp32c6_super_mini import (
IO0, IO1, IO2, IO3, IO4, IO5, IO6, IO7, IO8, IO9, IO10,
IO11, IO12, IO13, IO15, IO18, IO19, IO20, IO21, IO22, IO23,
LED_BUILTIN,
HIGH, LOW,
INPUT, OUTPUT, INPUT_PULLUP
)
Examples
Complete examples are available in the examples directory:
Example 1: Blink LED
from metf_python_client import METFClient
from metf_python_client.boards.nodemcu import LED_BUILTIN_AUX, OUTPUT
api = METFClient('192.168.1.100')
api.pinMode(LED_BUILTIN_AUX, OUTPUT)
# Blink LED
api.blynk(LED_BUILTIN_AUX, duration=1000)
Example 2: Check Button Press
from metf_python_client import METFClient
from metf_python_client.boards.nodemcu import D5, INPUT_PULLUP, LOW
api = METFClient('192.168.1.100')
api.pinMode(D5, INPUT_PULLUP)
# Wait for button press (3 second timeout)
if api.wait_digital(D5, LOW, 3.0):
print("Button pressed!")
else:
print("Timeout - button not pressed")
Example 3: I2C Communication
from metf_python_client import METFClient
from metf_python_client.boards.nodemcu import D3, D4
api = METFClient('192.168.1.100')
api.i2c_begin(D3, D4)
# Send command and receive response
slave_address = 0x48
message = '\x00' # Read register 0x00
response = api.i2c_ask(slave_address, message, 2)
print(f"Received {len(response)} bytes")
Example 4: RGB LED
from metf_python_client import METFClient
api = METFClient('192.168.1.100')
api.rgb_begin()
api.rgb_brightness(100)
# Cycle through colors
colors = ['FF0000', '00FF00', '0000FF', 'FFFF00', 'FF00FF', '00FFFF']
for color in colors:
api.rgb_color(color)
api.delay(1000)
More examples:
- 01-Blynk.py - Blink onboard LED
- 02-CheckButton.py - Button press detection
- 03-BlynkUnitTest.py - Unit test for LED
- 04-ButtonUnitTest.py - Unit test for button
- 05-i2c.py - I2C communication
- 06-RGBLed.py - RGB LED control
Use Cases
Hardware CI/CD Pipeline
Integrate hardware tests into your continuous integration workflow:
import unittest
from metf_python_client import METFClient
from metf_python_client.boards.nodemcu import D1, D2, INPUT, OUTPUT, HIGH, LOW
class HardwareTests(unittest.TestCase):
def setUp(self):
self.api = METFClient('192.168.1.100')
def test_power_led(self):
"""Test that power LED is on"""
self.api.pinMode(D1, INPUT)
self.assertEqual(self.api.digitalRead(D1), HIGH)
def test_button_press(self):
"""Test button press detection"""
self.api.pinMode(D2, INPUT_PULLUP)
pressed = self.api.wait_digital(D2, LOW, timeout=5.0)
self.assertTrue(pressed, "Button was not pressed")
def test_i2c_sensor(self):
"""Test I2C sensor communication"""
self.api.i2c_begin()
response = self.api.i2c_ask(0x48, '\x00', 2)
self.assertEqual(len(response), 2, "Sensor not responding")
if __name__ == '__main__':
unittest.main()
Interactive Hardware Testing
Test hardware configurations interactively:
from metf_python_client import METFClient
def test_voltage_levels():
"""Test device at different voltage levels"""
voltages = [3.0, 3.3, 5.0]
api = METFClient('192.168.1.100')
for voltage in voltages:
input(f"Set power supply to {voltage}V and press Enter...")
api.i2c_begin()
response = api.i2c_ask(0x48, '\x00', 2)
if len(response) == 2:
print(f"✓ {voltage}V: PASS")
else:
print(f"✗ {voltage}V: FAIL")
test_voltage_levels()
Production Testing
Automated testing during manufacturing:
from metf_python_client import METFClient
from metf_python_client.boards.nodemcu import *
def production_test(board_id):
"""Run complete production test suite"""
api = METFClient('192.168.1.100')
results = {}
# Test 1: Power-on self-test
api.pinMode(LED_BUILTIN, OUTPUT)
api.digitalWrite(LED_BUILTIN, HIGH)
results['led'] = api.digitalRead(LED_BUILTIN) == HIGH
# Test 2: I2C sensors
api.i2c_begin()
try:
response = api.i2c_ask(0x48, '\x00', 2)
results['sensor'] = len(response) == 2
except:
results['sensor'] = False
# Test 3: Serial communication
api.serial_begin(115200)
api.serial_flush()
lines = api.serial_readlines(wait=2000, delimiter='\n')
results['serial'] = len(lines) > 0
# Generate report
print(f"\n=== Board {board_id} Test Results ===")
for test, passed in results.items():
status = "PASS" if passed else "FAIL"
print(f"{test}: {status}")
return all(results.values())
# Test multiple boards
for board_num in range(1, 11):
if production_test(board_num):
print(f"Board {board_num}: ✓ PASSED\n")
else:
print(f"Board {board_num}: ✗ FAILED - CHECK HARDWARE\n")
Requirements
- Python: 3.7 or higher
- Dependencies: requests>=2.22.0
- Hardware: ESP8266/ESP32 board with METF firmware
Supported ESP Boards
- NodeMCU (ESP8266)
- WeMos D1 Mini (ESP8266)
- ESP32C6 Super Mini
ESP Firmware
The ESP board runs METF firmware, which provides an HTTP API for hardware control. The firmware is based on ESPAsyncWebServer.
Get the firmware: https://github.com/dontsovcmc/metf
Firmware Features
- HTTP REST API for GPIO, I2C, Serial control
- Supports multiple ESP8266 and ESP32 boards
- Async web server for fast response times
- Easy configuration via web interface
Development
Install Development Dependencies
# Option 1: Install package with dev dependencies
pip install metf-python-client[dev]
# Option 2: Install from source with requirements_dev.txt (recommended for contributors)
git clone https://github.com/dontsovcmc/metf-python-client
cd metf-python-client
pip install -r requirements_dev.txt
pip install -e .
# Option 3: Install from source with dev dependencies
git clone https://github.com/dontsovcmc/metf-python-client
cd metf-python-client
pip install -e .[dev]
Note: requirements_dev.txt includes all tools needed for development, testing, code quality checks, and publishing to PyPI.
Run Tests
pytest tests/
Code Style
# Format code
black metf_python_client/
# Check style
flake8 metf_python_client/
# Type checking
mypy metf_python_client/
Build and Publish
# Build package
python3 -m build
# Check build
twine check dist/*
# Publish to PyPI (maintainers only)
twine upload dist/*
For detailed publishing instructions, see CONTRIBUTING.md.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
How to Contribute
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests and ensure code quality
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Guidelines
- Follow PEP 8 style guide
- Add tests for new features
- Update documentation as needed
- Keep commits atomic and well-described
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Evgeny Dontsov
- Email: don-and-home@mail.ru
- GitHub: @dontsovcmc
Links
- PyPI: https://pypi.org/project/metf-python-client/
- GitHub Repository: https://github.com/dontsovcmc/metf-python-client
- ESP Firmware: https://github.com/dontsovcmc/metf
- Issue Tracker: https://github.com/dontsovcmc/metf-python-client/issues
- Changelog: CHANGELOG.md
Acknowledgments
- Based on ESPAsyncWebServer
- Inspired by the need for hardware CI/CD in embedded development
FAQ
Q: Can I use this with other ESP boards?
A: Yes! As long as you flash the METF firmware, any ESP8266 or ESP32 board should work. You may need to create custom pin definitions for your specific board.
Q: How do I find the IP address of my ESP board?
A: Check your router's DHCP client list, use a network scanner, or configure the ESP with a static IP in the firmware.
Q: Can I control multiple ESP boards from one script?
A: Yes! Just create multiple METFClient instances with different IP addresses.
esp1 = METFClient('192.168.1.100')
esp2 = METFClient('192.168.1.101')
Q: What's the difference between METF and direct ESP programming?
A: METF allows you to control hardware remotely via HTTP, making it perfect for automated testing and CI/CD. No need to flash new firmware for each test - just write Python scripts!
Q: Can I use this in production devices?
A: METF is designed for testing and development. For production, consider direct firmware implementation for better security and performance.
Made with ❤️ for hardware testing automation
Metadata
Release files for metf-python-client 0.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| metf_python_client-0.4.tar.gz | 21.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| metf_python_client-0.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.6 kB
Release files / metf_python_client-0.4.tar.gz
| Download URL | metf_python_client-0.4.tar.gz |
|---|---|
| Size | 21.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7d113050a4f414049af2c9ceb12fd1154341d75af0aaf1e6923885e029873baf
|
|
BLAKE2b-256 checksum How to use checksums |
1657d2e64ccb37bfe2dfabac7e6adabd71b5de5f80e19e4c6ba24471eb20bb2f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|
Release files / metf_python_client-0.4-py3-none-any.whl
| Download URL | metf_python_client-0.4-py3-none-any.whl |
|---|---|
| Size | 18.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
57fe5c1934abf4d1d4e2a790feace3576a14ccd93c88d9c8bf1a4f1ccd0d7638
|
|
BLAKE2b-256 checksum How to use checksums |
73bc55558415ace578c15e729381ae040620527e129e7036004438db8bb641a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.13
|