first commit
This commit is contained in:
parent
18e4c61270
commit
ada908e6f4
516
ft5xx6_controller.py
Normal file
516
ft5xx6_controller.py
Normal 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
48
pyft5xx6/.gitignore
vendored
Normal 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
72
pyft5xx6/INSTALL.md
Normal 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
9
pyft5xx6/LICENSE
Normal 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
4
pyft5xx6/MANIFEST.in
Normal file
@ -0,0 +1,4 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include INSTALL.md
|
||||
recursive-include examples *.py
|
||||
136
pyft5xx6/README.md
Normal file
136
pyft5xx6/README.md
Normal 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.
|
||||
53
pyft5xx6/examples/basic_usage.py
Normal file
53
pyft5xx6/examples/basic_usage.py
Normal 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()
|
||||
91
pyft5xx6/examples/interrupt_mode.py
Normal file
91
pyft5xx6/examples/interrupt_mode.py
Normal 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()
|
||||
18
pyft5xx6/pyft5xx6/__init__.py
Normal file
18
pyft5xx6/pyft5xx6/__init__.py
Normal 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'
|
||||
516
pyft5xx6/pyft5xx6/controller.py
Normal file
516
pyft5xx6/pyft5xx6/controller.py
Normal 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
3
pyft5xx6/pyproject.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
29
pyft5xx6/setup.py
Normal file
29
pyft5xx6/setup.py
Normal 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",
|
||||
)
|
||||
Loading…
x
Reference in New Issue
Block a user