first commit

This commit is contained in:
Duncan Tourolle 2025-05-24 12:44:45 +01:00
parent 18e4c61270
commit ada908e6f4
12 changed files with 1495 additions and 0 deletions

516
ft5xx6_controller.py Normal file
View File

@ -0,0 +1,516 @@
#!/usr/bin/env python3
"""
Python library for control of FT5xx6 Capacitive Touch panels (CTPs).
Based on Arduino library by Owen Lyke and original work by Helge Langehaug.
Ported to Python for Raspberry Pi usage.
"""
import time
import threading
from enum import Enum, IntEnum
from typing import List, Optional, Callable, Union, Tuple
try:
import RPi.GPIO as GPIO
except ImportError:
# For development/testing without RPi.GPIO
print("Warning: RPi.GPIO not available. Using mock implementation.")
class GPIO:
BCM = 'BCM'
FALLING = 'FALLING'
IN = 'IN'
PUD_UP = 'PUD_UP'
@staticmethod
def setmode(mode): pass
@staticmethod
def setup(pin, direction, pull_up_down=None): pass
@staticmethod
def add_event_detect(pin, edge, callback=None): pass
@staticmethod
def remove_event_detect(pin): pass
try:
from smbus2 import SMBus
except ImportError:
# For development/testing without smbus2
print("Warning: smbus2 not available. Using mock implementation.")
class SMBus:
def __init__(self, bus): pass
def read_byte_data(self, addr, reg): return 0
def write_byte_data(self, addr, reg, val): pass
def read_i2c_block_data(self, addr, reg, length): return [0] * length
# Constants
FT5XX6_UNUSED_PIN = 0xFF
# Enums
class I2CAddress(IntEnum):
"""Known I2C addresses for the touch controllers"""
UNKNOWN = 0x03
FT5316 = 0x38
class RegisterAddresses(IntEnum):
"""Register addresses for the FT5xx6 controllers"""
DEV_MODE = 0x00
GEST_ID = 0x01
TD_STATUS = 0x02
T1_XH = 0x03
T1_XL = 0x04
T1_YH = 0x05
T1_YL = 0x06
T2_XH = 0x09
T2_XL = 0x0A
T2_YH = 0x0B
T2_YL = 0x0C
T3_XH = 0x0F
T3_XL = 0x10
T3_YH = 0x11
T3_YL = 0x12
T4_XH = 0x15
T4_XL = 0x16
T4_YH = 0x17
T4_YL = 0x18
T5_XH = 0x1B
T5_XL = 0x1C
T5_YH = 0x1D
T5_YL = 0x1E
class Status(Enum):
"""Status codes for function returns"""
NOMINAL = 0
ERROR = 1
NOT_ENOUGH_MEMORY = 2
class Gestures(IntEnum):
"""Gesture IDs from the touch controller"""
NO_GESTURE = 0x00
MOVE_UP = 0x10
MOVE_LEFT = 0x14
MOVE_DOWN = 0x18
MOVE_RIGHT = 0x1C
ZOOM_IN = 0x48
ZOOM_OUT = 0x49
class Mode(Enum):
"""Operation modes for the touch controller"""
INTERRUPT = 0
POLLING = 1
# Data structures
class TouchRecord:
"""Stores data for a touch event"""
def __init__(self):
self.num_touches = 0
self.t1x = 0
self.t1y = 0
self.t2x = 0
self.t2y = 0
self.t3x = 0
self.t3y = 0
self.t4x = 0
self.t4y = 0
self.t5x = 0
self.t5y = 0
self.gesture = Gestures.NO_GESTURE
self.timestamp = 0
def __eq__(self, other):
"""Compare two TouchRecord objects"""
if not isinstance(other, TouchRecord):
return False
if self.num_touches != other.num_touches:
return False
if self.t1x != other.t1x or self.t1y != other.t1y:
return False
if self.t2x != other.t2x or self.t2y != other.t2y:
return False
if self.t3x != other.t3x or self.t3y != other.t3y:
return False
if self.t4x != other.t4x or self.t4y != other.t4y:
return False
if self.t5x != other.t5x or self.t5y != other.t5y:
return False
if self.gesture != other.gesture:
return False
return True
def __ne__(self, other):
"""Not equal operator"""
return not (self == other)
class FT5xx6:
"""Base class for FT5xx6 capacitive touch panel controllers"""
def __init__(self, address: int = I2CAddress.UNKNOWN):
"""
Initialize the touch controller
Args:
address: I2C address of the touch controller
"""
self._has_interrupts = False
self._mode = Mode.POLLING
self._i2c_bus = None
self._addr = address
self._int_pin = FT5XX6_UNUSED_PIN
self._touch_record_buffer = None
self._write_offset = 0
self._read_offset = 0
self._record_depth = 0
self._records_available = 0
self._write_ok = False
self._read_ok = False
self._buffer_was_allocated = False
self.new_touch = False
self.new_data = False
self.last_update = 0
self.last_touch = TouchRecord()
# Lock for thread safety
self._lock = threading.Lock()
def begin(self, i2c_bus: int = 1, int_pin: int = FT5XX6_UNUSED_PIN,
user_isr: Optional[Callable] = None) -> Status:
"""
Initialize the touch controller
Args:
i2c_bus: I2C bus number
int_pin: Interrupt pin (BCM numbering)
user_isr: User interrupt service routine
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
if int_pin != FT5XX6_UNUSED_PIN and user_isr is not None:
self._has_interrupts = True
self._int_pin = int_pin
self._mode = Mode.INTERRUPT
GPIO.setmode(GPIO.BCM)
GPIO.setup(self._int_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(self._int_pin, GPIO.FALLING, callback=user_isr)
try:
self._i2c_bus = SMBus(i2c_bus)
# Set device mode to normal operation
self._i2c_bus.write_byte_data(self._addr, RegisterAddresses.DEV_MODE, 0)
return Status.NOMINAL
except Exception as e:
print(f"Error initializing FT5xx6: {e}")
return Status.ERROR
def _get_touch_record(self, record: TouchRecord) -> Status:
"""
Read touch data from the controller
Args:
record: TouchRecord to store the data
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
try:
# Read all registers in one go for better performance
registers = self._i2c_bus.read_i2c_block_data(self._addr, 0, 31)
record.timestamp = int(time.time() * 1000) # Milliseconds
record.num_touches = registers[RegisterAddresses.TD_STATUS] & 0x0F
record.gesture = Gestures(registers[RegisterAddresses.GEST_ID])
if record.num_touches > 0:
record.t1x = ((registers[RegisterAddresses.T1_XH] & 0x0F) << 8) | registers[RegisterAddresses.T1_XL]
record.t1y = ((registers[RegisterAddresses.T1_YH] & 0x0F) << 8) | registers[RegisterAddresses.T1_YL]
if record.num_touches > 1:
record.t2x = ((registers[RegisterAddresses.T2_XH] & 0x0F) << 8) | registers[RegisterAddresses.T2_XL]
record.t2y = ((registers[RegisterAddresses.T2_YH] & 0x0F) << 8) | registers[RegisterAddresses.T2_YL]
if record.num_touches > 2:
record.t3x = ((registers[RegisterAddresses.T3_XH] & 0x0F) << 8) | registers[RegisterAddresses.T3_XL]
record.t3y = ((registers[RegisterAddresses.T3_YH] & 0x0F) << 8) | registers[RegisterAddresses.T3_YL]
if record.num_touches > 3:
record.t4x = ((registers[RegisterAddresses.T4_XH] & 0x0F) << 8) | registers[RegisterAddresses.T4_XL]
record.t4y = ((registers[RegisterAddresses.T4_YH] & 0x0F) << 8) | registers[RegisterAddresses.T4_YL]
if record.num_touches > 4:
record.t5x = ((registers[RegisterAddresses.T5_XH] & 0x0F) << 8) | registers[RegisterAddresses.T5_XL]
record.t5y = ((registers[RegisterAddresses.T5_YH] & 0x0F) << 8) | registers[RegisterAddresses.T5_YL]
return Status.NOMINAL
except Exception as e:
print(f"Error reading touch data: {e}")
return Status.ERROR
def write(self, records: Union[TouchRecord, List[TouchRecord]],
num_records: int = 1) -> Status:
"""
Write touch records to the buffer
Args:
records: TouchRecord or list of TouchRecords to write
num_records: Number of records to write
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
with self._lock:
if records is None or num_records == 0:
return Status.ERROR
# Convert single record to list
if isinstance(records, TouchRecord):
records = [records]
if self._touch_record_buffer is not None and self._record_depth != 0:
for i in range(min(num_records, len(records))):
if self._write_ok:
self._touch_record_buffer[self._write_offset] = records[i]
self._write_offset += 1
self._read_ok = True
self._records_available += 1
if self._write_offset >= self._record_depth:
self._write_offset = 0
if self._write_offset == self._read_offset:
self._write_ok = False
else:
return Status.ERROR
else:
# If no buffer, just update the last_touch
self.last_touch = records[0]
return Status.NOMINAL
def use_buffer(self, depth: int = 0,
touch_records: Optional[List[TouchRecord]] = None) -> Status:
"""
Initialize a buffer for touch records
Args:
depth: Number of records to store
touch_records: Pre-allocated buffer (optional)
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
with self._lock:
if depth > 0:
# Clear buffer without calling remove_buffer() to avoid deadlock
self._clear_buffer_internal()
self._record_depth = 0
self._buffer_was_allocated = False
self._touch_record_buffer = None
self._write_ok = False
self._read_ok = False
if touch_records is None:
try:
self._touch_record_buffer = [TouchRecord() for _ in range(depth)]
self._buffer_was_allocated = True
except MemoryError:
self._touch_record_buffer = None
return Status.NOT_ENOUGH_MEMORY
else:
self._touch_record_buffer = touch_records
self._buffer_was_allocated = False
self._record_depth = depth
self._write_ok = True
self._read_ok = False
return Status.NOMINAL
def _clear_buffer_internal(self):
"""
Internal method to clear the buffer without acquiring the lock
Used by methods that already have the lock
"""
self._records_available = 0
self._write_offset = 0
self._read_offset = 0
self._write_ok = True
self._read_ok = False
def remove_buffer(self) -> Status:
"""
Remove the touch record buffer
Returns:
Status: NOMINAL
"""
with self._lock:
self.clear_buffer()
self._record_depth = 0
self._buffer_was_allocated = False
self._touch_record_buffer = None
self._write_ok = False
self._read_ok = False
return Status.NOMINAL
def available(self) -> int:
"""
Get the number of available touch records
Returns:
Number of available records
"""
return self._records_available
def clear_buffer(self) -> Status:
"""
Clear the touch record buffer
Returns:
Status: NOMINAL
"""
with self._lock:
self._records_available = 0
self._write_offset = 0
self._read_offset = 0
self._write_ok = True
self._read_ok = False
return Status.NOMINAL
def read(self) -> TouchRecord:
"""
Read the oldest touch record from the buffer
Returns:
TouchRecord: The oldest touch record
"""
with self._lock:
if self._touch_record_buffer is not None and self._record_depth != 0:
if self._read_ok:
record = self._touch_record_buffer[self._read_offset]
self._read_offset += 1
if self._read_offset >= self._record_depth:
self._read_offset = 0
if self._read_offset == self._write_offset:
self._read_ok = False
if self._records_available > 0:
self._records_available -= 1
if self._records_available == 0:
self.new_touch = False
self._write_ok = True
return record
self.new_touch = False
return self.last_touch
def peek(self, offset_from_read: int = 0) -> TouchRecord:
"""
Peek at a touch record without removing it from the buffer
Args:
offset_from_read: Offset from the read pointer
Returns:
TouchRecord: The touch record at the specified offset
"""
with self._lock:
if self._touch_record_buffer is None or self._record_depth == 0:
return self.last_touch
offset = (self._read_offset + offset_from_read) % self._record_depth
return self._touch_record_buffer[offset]
def set_mode(self, mode: Mode) -> Status:
"""
Set the operation mode
Args:
mode: INTERRUPT or POLLING
Returns:
Status: NOMINAL
"""
self._mode = mode
return Status.NOMINAL
def update(self) -> Status:
"""
Update touch data (polling mode)
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
new_record = TouchRecord()
result = self._get_touch_record(new_record)
if result != Status.NOMINAL:
return result
self.new_data = False
self.last_update = int(time.time() * 1000) # Milliseconds
if new_record != self.last_touch:
self.write(new_record)
self.new_touch = True
return Status.NOMINAL
def interrupt(self) -> Status:
"""
Handle an interrupt
Returns:
Status: NOMINAL
"""
# Set flag to indicate new data is available
self.new_data = True
# Call the callback if defined
try:
ft5xx6_interrupt_callback(self)
except NameError:
# Callback not defined, ignore
pass
return Status.NOMINAL
class FT5316(FT5xx6):
"""FT5316 touch controller implementation"""
def __init__(self):
"""Initialize the FT5316 touch controller"""
super().__init__(I2CAddress.FT5316)
# Weak callback functions (can be overridden by the user)
def ft5xx6_return_callback(retval: Status, file: str, line: int):
"""Called when a function returns a status code"""
pass
def ft5xx6_interrupt_callback(controller: FT5xx6):
"""Called when an interrupt occurs"""
pass

48
pyft5xx6/.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE specific files
.idea/
.vscode/
*.swp
*.swo

72
pyft5xx6/INSTALL.md Normal file
View File

@ -0,0 +1,72 @@
# Installation Guide for PyFT5xx6
This document provides instructions for installing the PyFT5xx6 package.
## Prerequisites
- Python 3.6 or higher
- pip (Python package installer)
- For Raspberry Pi usage: Enabled I2C interface
## Installation Methods
### From PyPI (once published)
```bash
pip install pyft5xx6
```
### From Source
1. Clone the repository or download the source code:
```bash
git clone https://gitea.tourolle.paris/dtourolle/PyFTtxx6
cd PyFTtxx6
```
2. Install using pip:
```bash
pip install ./pyft5xx6
```
Or for development installation:
```bash
pip install -e ./pyft5xx6
```
### Building from Source
1. Build the package:
```bash
cd pyft5xx6
python -m build
```
2. Install the built package:
```bash
pip install dist/pyft5xx6-0.1.0-py3-none-any.whl
```
## Verifying Installation
After installation, you can verify that the package is installed correctly:
```python
import pyft5xx6
print(pyft5xx6.__version__)
```
## Enabling I2C on Raspberry Pi
If you're using a Raspberry Pi, you need to enable the I2C interface:
1. Run `sudo raspi-config`
2. Navigate to "Interfacing Options" > "I2C"
3. Select "Yes" to enable I2C
4. Reboot your Raspberry Pi: `sudo reboot`
## Dependencies
The package automatically installs the following dependencies:
- smbus2
- RPi.GPIO (only on Linux platforms)

9
pyft5xx6/LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 dtourolle
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

4
pyft5xx6/MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include LICENSE
include README.md
include INSTALL.md
recursive-include examples *.py

136
pyft5xx6/README.md Normal file
View File

@ -0,0 +1,136 @@
# PyFT5xx6
A Python library for interfacing with FT5xx6 Capacitive Touch Panel (CTP) controllers, primarily designed for Raspberry Pi.
## Overview
PyFT5xx6 provides a Python interface for FT5xx6 touch controllers commonly used in touchscreen displays. The library supports:
- Reading touch events and gestures
- Both polling and interrupt-based operation modes
- Multi-touch (up to 5 points)
- Gesture recognition
- Thread-safe operation
This library is based on Arduino library by Owen Lyke and original work by Helge Langehaug, ported to Python for Raspberry Pi usage.
## Installation
### From PyPI
```bash
pip install pyft5xx6
```
### From Source
```bash
git clone https://gitea.tourolle.paris/dtourolle/PyFTtxx6
cd PyFTtxx6/pyft5xx6
pip install -e .
```
## Dependencies
- `RPi.GPIO` (Only required when running on Raspberry Pi)
- `smbus2`
## Usage
### Basic Example
```python
from pyft5xx6 import FT5316, Status, Mode
import time
# Initialize the touch controller
touch = FT5316()
result = touch.begin(i2c_bus=1) # Using I2C bus 1
if result != Status.NOMINAL:
print("Failed to initialize touch controller")
exit(1)
# Set polling mode
touch.set_mode(Mode.POLLING)
# Main loop
try:
while True:
# Update touch data
touch.update()
# Check if there is a new touch event
if touch.new_touch:
record = touch.read()
print(f"Number of touches: {record.num_touches}")
if record.num_touches > 0:
print(f"Touch 1: ({record.t1x}, {record.t1y})")
if record.gesture != 0:
print(f"Gesture: {record.gesture}")
time.sleep(0.01) # 10ms delay
except KeyboardInterrupt:
print("Exiting")
```
### Using Interrupts
```python
from pyft5xx6 import FT5316, Status, Mode
import time
import RPi.GPIO as GPIO
# Interrupt pin (BCM numbering)
INT_PIN = 17
# Interrupt handler
def touch_interrupt(channel):
# The actual touch data will be read in the main loop
print("Touch interrupt detected")
# Initialize the touch controller
touch = FT5316()
result = touch.begin(i2c_bus=1, int_pin=INT_PIN, user_isr=touch_interrupt)
if result != Status.NOMINAL:
print("Failed to initialize touch controller")
exit(1)
# Main loop
try:
while True:
# Check if there is a new touch event
if touch.new_data:
# Update touch data
touch.update()
if touch.new_touch:
record = touch.read()
print(f"Number of touches: {record.num_touches}")
if record.num_touches > 0:
print(f"Touch 1: ({record.t1x}, {record.t1y})")
if record.gesture != 0:
print(f"Gesture: {record.gesture}")
time.sleep(0.01) # 10ms delay
except KeyboardInterrupt:
print("Exiting")
# Clean up GPIO
GPIO.cleanup()
```
## Supported Controllers
- FT5316 (I2C address 0x38)
- Generic FT5xx6 (configurable address)
## License
MIT License - See the LICENSE file for details.

View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""
Basic usage example for the PyFT5xx6 library.
This example demonstrates how to use the FT5316 touch controller in polling mode.
"""
from pyft5xx6 import FT5316, Status, Mode, Gestures
import time
def main():
# Initialize the touch controller
touch = FT5316()
result = touch.begin(i2c_bus=1) # Using I2C bus 1
if result != Status.NOMINAL:
print("Failed to initialize touch controller")
return
# Set polling mode
touch.set_mode(Mode.POLLING)
print("Touch controller initialized. Touch the screen...")
# Allocate a buffer for 10 touch records
touch.use_buffer(10)
# Main loop
try:
while True:
# Update touch data
touch.update()
# Check if there is a new touch event
if touch.new_touch:
record = touch.read()
print(f"Number of touches: {record.num_touches}")
if record.num_touches > 0:
print(f"Touch 1: ({record.t1x}, {record.t1y})")
if record.num_touches > 1:
print(f"Touch 2: ({record.t2x}, {record.t2y})")
if record.gesture != Gestures.NO_GESTURE:
print(f"Gesture: {record.gesture.name}")
time.sleep(0.01) # 10ms delay
except KeyboardInterrupt:
print("Exiting")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""
Interrupt mode example for the PyFT5xx6 library.
This example demonstrates how to use the FT5316 touch controller with interrupt-based operation.
"""
from pyft5xx6 import FT5316, Status, Mode, Gestures
import time
import signal
import sys
try:
import RPi.GPIO as GPIO
except ImportError:
print("RPi.GPIO not available. This example requires a Raspberry Pi.")
sys.exit(1)
# Interrupt pin (BCM numbering)
INT_PIN = 17 # Change to match your wiring
# Flag for cleanup on exit
cleanup_done = False
# Interrupt handler
def touch_interrupt(channel):
"""Called when a touch event triggers the interrupt pin"""
# Just set the controller's interrupt flag
# The actual data reading is done in the main loop
touch.interrupt()
print("Touch interrupt detected")
# Signal handler for clean exit
def signal_handler(sig, frame):
global cleanup_done
print("Exiting...")
if not cleanup_done:
GPIO.cleanup()
cleanup_done = True
sys.exit(0)
def main():
global touch
# Register signal handler
signal.signal(signal.SIGINT, signal_handler)
# Initialize the touch controller
touch = FT5316()
result = touch.begin(i2c_bus=1, int_pin=INT_PIN, user_isr=touch_interrupt)
if result != Status.NOMINAL:
print("Failed to initialize touch controller")
GPIO.cleanup()
return
# Allocate a buffer for 10 touch records
touch.use_buffer(10)
print("Touch controller initialized in interrupt mode.")
print("Touch the screen to trigger interrupt...")
# Main loop
try:
while True:
# Check if new data is available (set by the interrupt handler)
if touch.new_data:
# Read touch data
if touch.new_touch:
record = touch.read()
print(f"Number of touches: {record.num_touches}")
if record.num_touches > 0:
print(f"Touch 1: ({record.t1x}, {record.t1y})")
if record.num_touches > 1:
print(f"Touch 2: ({record.t2x}, {record.t2y})")
if record.gesture != Gestures.NO_GESTURE:
print(f"Gesture: {record.gesture.name}")
time.sleep(0.01) # 10ms delay
except KeyboardInterrupt:
# This should be caught by the signal handler
pass
finally:
if not cleanup_done:
GPIO.cleanup()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,18 @@
"""
Python library for control of FT5xx6 Capacitive Touch panels (CTPs).
"""
from .controller import (
FT5xx6,
FT5316,
TouchRecord,
Status,
Mode,
Gestures,
I2CAddress,
RegisterAddresses,
ft5xx6_interrupt_callback,
ft5xx6_return_callback
)
__version__ = '0.1.0'

View File

@ -0,0 +1,516 @@
#!/usr/bin/env python3
"""
Python library for control of FT5xx6 Capacitive Touch panels (CTPs).
Based on Arduino library by Owen Lyke and original work by Helge Langehaug.
Ported to Python for Raspberry Pi usage.
"""
import time
import threading
from enum import Enum, IntEnum
from typing import List, Optional, Callable, Union, Tuple
try:
import RPi.GPIO as GPIO
except ImportError:
# For development/testing without RPi.GPIO
print("Warning: RPi.GPIO not available. Using mock implementation.")
class GPIO:
BCM = 'BCM'
FALLING = 'FALLING'
IN = 'IN'
PUD_UP = 'PUD_UP'
@staticmethod
def setmode(mode): pass
@staticmethod
def setup(pin, direction, pull_up_down=None): pass
@staticmethod
def add_event_detect(pin, edge, callback=None): pass
@staticmethod
def remove_event_detect(pin): pass
try:
from smbus2 import SMBus
except ImportError:
# For development/testing without smbus2
print("Warning: smbus2 not available. Using mock implementation.")
class SMBus:
def __init__(self, bus): pass
def read_byte_data(self, addr, reg): return 0
def write_byte_data(self, addr, reg, val): pass
def read_i2c_block_data(self, addr, reg, length): return [0] * length
# Constants
FT5XX6_UNUSED_PIN = 0xFF
# Enums
class I2CAddress(IntEnum):
"""Known I2C addresses for the touch controllers"""
UNKNOWN = 0x03
FT5316 = 0x38
class RegisterAddresses(IntEnum):
"""Register addresses for the FT5xx6 controllers"""
DEV_MODE = 0x00
GEST_ID = 0x01
TD_STATUS = 0x02
T1_XH = 0x03
T1_XL = 0x04
T1_YH = 0x05
T1_YL = 0x06
T2_XH = 0x09
T2_XL = 0x0A
T2_YH = 0x0B
T2_YL = 0x0C
T3_XH = 0x0F
T3_XL = 0x10
T3_YH = 0x11
T3_YL = 0x12
T4_XH = 0x15
T4_XL = 0x16
T4_YH = 0x17
T4_YL = 0x18
T5_XH = 0x1B
T5_XL = 0x1C
T5_YH = 0x1D
T5_YL = 0x1E
class Status(Enum):
"""Status codes for function returns"""
NOMINAL = 0
ERROR = 1
NOT_ENOUGH_MEMORY = 2
class Gestures(IntEnum):
"""Gesture IDs from the touch controller"""
NO_GESTURE = 0x00
MOVE_UP = 0x10
MOVE_LEFT = 0x14
MOVE_DOWN = 0x18
MOVE_RIGHT = 0x1C
ZOOM_IN = 0x48
ZOOM_OUT = 0x49
class Mode(Enum):
"""Operation modes for the touch controller"""
INTERRUPT = 0
POLLING = 1
# Data structures
class TouchRecord:
"""Stores data for a touch event"""
def __init__(self):
self.num_touches = 0
self.t1x = 0
self.t1y = 0
self.t2x = 0
self.t2y = 0
self.t3x = 0
self.t3y = 0
self.t4x = 0
self.t4y = 0
self.t5x = 0
self.t5y = 0
self.gesture = Gestures.NO_GESTURE
self.timestamp = 0
def __eq__(self, other):
"""Compare two TouchRecord objects"""
if not isinstance(other, TouchRecord):
return False
if self.num_touches != other.num_touches:
return False
if self.t1x != other.t1x or self.t1y != other.t1y:
return False
if self.t2x != other.t2x or self.t2y != other.t2y:
return False
if self.t3x != other.t3x or self.t3y != other.t3y:
return False
if self.t4x != other.t4x or self.t4y != other.t4y:
return False
if self.t5x != other.t5x or self.t5y != other.t5y:
return False
if self.gesture != other.gesture:
return False
return True
def __ne__(self, other):
"""Not equal operator"""
return not (self == other)
class FT5xx6:
"""Base class for FT5xx6 capacitive touch panel controllers"""
def __init__(self, address: int = I2CAddress.UNKNOWN):
"""
Initialize the touch controller
Args:
address: I2C address of the touch controller
"""
self._has_interrupts = False
self._mode = Mode.POLLING
self._i2c_bus = None
self._addr = address
self._int_pin = FT5XX6_UNUSED_PIN
self._touch_record_buffer = None
self._write_offset = 0
self._read_offset = 0
self._record_depth = 0
self._records_available = 0
self._write_ok = False
self._read_ok = False
self._buffer_was_allocated = False
self.new_touch = False
self.new_data = False
self.last_update = 0
self.last_touch = TouchRecord()
# Lock for thread safety
self._lock = threading.Lock()
def begin(self, i2c_bus: int = 1, int_pin: int = FT5XX6_UNUSED_PIN,
user_isr: Optional[Callable] = None) -> Status:
"""
Initialize the touch controller
Args:
i2c_bus: I2C bus number
int_pin: Interrupt pin (BCM numbering)
user_isr: User interrupt service routine
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
if int_pin != FT5XX6_UNUSED_PIN and user_isr is not None:
self._has_interrupts = True
self._int_pin = int_pin
self._mode = Mode.INTERRUPT
GPIO.setmode(GPIO.BCM)
GPIO.setup(self._int_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(self._int_pin, GPIO.FALLING, callback=user_isr)
try:
self._i2c_bus = SMBus(i2c_bus)
# Set device mode to normal operation
self._i2c_bus.write_byte_data(self._addr, RegisterAddresses.DEV_MODE, 0)
return Status.NOMINAL
except Exception as e:
print(f"Error initializing FT5xx6: {e}")
return Status.ERROR
def _get_touch_record(self, record: TouchRecord) -> Status:
"""
Read touch data from the controller
Args:
record: TouchRecord to store the data
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
try:
# Read all registers in one go for better performance
registers = self._i2c_bus.read_i2c_block_data(self._addr, 0, 31)
record.timestamp = int(time.time() * 1000) # Milliseconds
record.num_touches = registers[RegisterAddresses.TD_STATUS] & 0x0F
record.gesture = Gestures(registers[RegisterAddresses.GEST_ID])
if record.num_touches > 0:
record.t1x = ((registers[RegisterAddresses.T1_XH] & 0x0F) << 8) | registers[RegisterAddresses.T1_XL]
record.t1y = ((registers[RegisterAddresses.T1_YH] & 0x0F) << 8) | registers[RegisterAddresses.T1_YL]
if record.num_touches > 1:
record.t2x = ((registers[RegisterAddresses.T2_XH] & 0x0F) << 8) | registers[RegisterAddresses.T2_XL]
record.t2y = ((registers[RegisterAddresses.T2_YH] & 0x0F) << 8) | registers[RegisterAddresses.T2_YL]
if record.num_touches > 2:
record.t3x = ((registers[RegisterAddresses.T3_XH] & 0x0F) << 8) | registers[RegisterAddresses.T3_XL]
record.t3y = ((registers[RegisterAddresses.T3_YH] & 0x0F) << 8) | registers[RegisterAddresses.T3_YL]
if record.num_touches > 3:
record.t4x = ((registers[RegisterAddresses.T4_XH] & 0x0F) << 8) | registers[RegisterAddresses.T4_XL]
record.t4y = ((registers[RegisterAddresses.T4_YH] & 0x0F) << 8) | registers[RegisterAddresses.T4_YL]
if record.num_touches > 4:
record.t5x = ((registers[RegisterAddresses.T5_XH] & 0x0F) << 8) | registers[RegisterAddresses.T5_XL]
record.t5y = ((registers[RegisterAddresses.T5_YH] & 0x0F) << 8) | registers[RegisterAddresses.T5_YL]
return Status.NOMINAL
except Exception as e:
print(f"Error reading touch data: {e}")
return Status.ERROR
def write(self, records: Union[TouchRecord, List[TouchRecord]],
num_records: int = 1) -> Status:
"""
Write touch records to the buffer
Args:
records: TouchRecord or list of TouchRecords to write
num_records: Number of records to write
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
with self._lock:
if records is None or num_records == 0:
return Status.ERROR
# Convert single record to list
if isinstance(records, TouchRecord):
records = [records]
if self._touch_record_buffer is not None and self._record_depth != 0:
for i in range(min(num_records, len(records))):
if self._write_ok:
self._touch_record_buffer[self._write_offset] = records[i]
self._write_offset += 1
self._read_ok = True
self._records_available += 1
if self._write_offset >= self._record_depth:
self._write_offset = 0
if self._write_offset == self._read_offset:
self._write_ok = False
else:
return Status.ERROR
else:
# If no buffer, just update the last_touch
self.last_touch = records[0]
return Status.NOMINAL
def use_buffer(self, depth: int = 0,
touch_records: Optional[List[TouchRecord]] = None) -> Status:
"""
Initialize a buffer for touch records
Args:
depth: Number of records to store
touch_records: Pre-allocated buffer (optional)
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
with self._lock:
if depth > 0:
# Clear buffer without calling remove_buffer() to avoid deadlock
self._clear_buffer_internal()
self._record_depth = 0
self._buffer_was_allocated = False
self._touch_record_buffer = None
self._write_ok = False
self._read_ok = False
if touch_records is None:
try:
self._touch_record_buffer = [TouchRecord() for _ in range(depth)]
self._buffer_was_allocated = True
except MemoryError:
self._touch_record_buffer = None
return Status.NOT_ENOUGH_MEMORY
else:
self._touch_record_buffer = touch_records
self._buffer_was_allocated = False
self._record_depth = depth
self._write_ok = True
self._read_ok = False
return Status.NOMINAL
def _clear_buffer_internal(self):
"""
Internal method to clear the buffer without acquiring the lock
Used by methods that already have the lock
"""
self._records_available = 0
self._write_offset = 0
self._read_offset = 0
self._write_ok = True
self._read_ok = False
def remove_buffer(self) -> Status:
"""
Remove the touch record buffer
Returns:
Status: NOMINAL
"""
with self._lock:
self.clear_buffer()
self._record_depth = 0
self._buffer_was_allocated = False
self._touch_record_buffer = None
self._write_ok = False
self._read_ok = False
return Status.NOMINAL
def available(self) -> int:
"""
Get the number of available touch records
Returns:
Number of available records
"""
return self._records_available
def clear_buffer(self) -> Status:
"""
Clear the touch record buffer
Returns:
Status: NOMINAL
"""
with self._lock:
self._records_available = 0
self._write_offset = 0
self._read_offset = 0
self._write_ok = True
self._read_ok = False
return Status.NOMINAL
def read(self) -> TouchRecord:
"""
Read the oldest touch record from the buffer
Returns:
TouchRecord: The oldest touch record
"""
with self._lock:
if self._touch_record_buffer is not None and self._record_depth != 0:
if self._read_ok:
record = self._touch_record_buffer[self._read_offset]
self._read_offset += 1
if self._read_offset >= self._record_depth:
self._read_offset = 0
if self._read_offset == self._write_offset:
self._read_ok = False
if self._records_available > 0:
self._records_available -= 1
if self._records_available == 0:
self.new_touch = False
self._write_ok = True
return record
self.new_touch = False
return self.last_touch
def peek(self, offset_from_read: int = 0) -> TouchRecord:
"""
Peek at a touch record without removing it from the buffer
Args:
offset_from_read: Offset from the read pointer
Returns:
TouchRecord: The touch record at the specified offset
"""
with self._lock:
if self._touch_record_buffer is None or self._record_depth == 0:
return self.last_touch
offset = (self._read_offset + offset_from_read) % self._record_depth
return self._touch_record_buffer[offset]
def set_mode(self, mode: Mode) -> Status:
"""
Set the operation mode
Args:
mode: INTERRUPT or POLLING
Returns:
Status: NOMINAL
"""
self._mode = mode
return Status.NOMINAL
def update(self) -> Status:
"""
Update touch data (polling mode)
Returns:
Status: NOMINAL if successful, ERROR otherwise
"""
new_record = TouchRecord()
result = self._get_touch_record(new_record)
if result != Status.NOMINAL:
return result
self.new_data = False
self.last_update = int(time.time() * 1000) # Milliseconds
if new_record != self.last_touch:
self.write(new_record)
self.new_touch = True
return Status.NOMINAL
def interrupt(self) -> Status:
"""
Handle an interrupt
Returns:
Status: NOMINAL
"""
# Set flag to indicate new data is available
self.new_data = True
# Call the callback if defined
try:
ft5xx6_interrupt_callback(self)
except NameError:
# Callback not defined, ignore
pass
return Status.NOMINAL
class FT5316(FT5xx6):
"""FT5316 touch controller implementation"""
def __init__(self):
"""Initialize the FT5316 touch controller"""
super().__init__(I2CAddress.FT5316)
# Weak callback functions (can be overridden by the user)
def ft5xx6_return_callback(retval: Status, file: str, line: int):
"""Called when a function returns a status code"""
pass
def ft5xx6_interrupt_callback(controller: FT5xx6):
"""Called when an interrupt occurs"""
pass

3
pyft5xx6/pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

29
pyft5xx6/setup.py Normal file
View File

@ -0,0 +1,29 @@
from setuptools import setup, find_packages
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="pyft5xx6",
version="0.1.0",
author="dtourolle",
author_email="duncan@tourolle.paris",
description="Python library for control of FT5xx6 Capacitive Touch panels (CTPs)",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://gitea.tourolle.paris/dtourolle/PyFTtxx6",
packages=find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Topic :: Software Development :: Libraries",
"Topic :: System :: Hardware",
],
python_requires=">=3.6",
install_requires=[
"smbus2",
"RPi.GPIO; platform_system=='Linux'"
],
keywords="raspberry pi, touch panel, capacitive, ft5xx6, i2c",
)