Raspberry Pi, GfxLCD and XPT2046 touch panel

Last time we ignored a touch panel. But without it, our Waveshare shield is half-functional. So let’s do something about it: )
We have an XPT2046 panel that is compatible with AD7843 and it is compatible with AD7846 🙂 XPT has additionally a pressure sensor.

Planning

So what’s the plan?
Get some readings, calibrate the touch and add rotate. Additionally, we will use pressure to filter noise. So only touch greater than 10 will be read.

Driver

It is our second driver for touch panel so it is a time to create an abstract class. We require only 3 functions so abstract looks like this:

"""Touch panel interface"""
import abc


class Touch(metaclass=abc.ABCMeta):
    """Touch class"""

    @abc.abstractmethod
    def init(self):
        """some additional init"""
        return

    @abc.abstractmethod
    def get_position(self):
        """returns pressed position"""
        return

    @abc.abstractmethod
    def close(self):
        """close functions"""
        return

This driver differ from AD in function get_position, we are reading pressure strength and trigger reads only when it is above the threshold.
Additionally, we need another variable to drop us out of a loop if we get a false trigger. Variable fuse will break a loop after 40 iterations.

    def get_position(self):
        """get touch coords"""
        buffer = []
        fuse = 40
        while len(buffer) < 20 and fuse > 0:
            if self.cs_pin:
                RPi.GPIO.output(self.cs_pin, 0)

            self.spi.xfer2([0x80 | 0x08 | 0x30])
            recv = self.spi.readbytes(1)
            tc_rz = recv[0] & 0x7f

            self.spi.xfer2([0x80 | 0x08 | 0x40])
            recv = self.spi.readbytes(1)
            tc_rz += (255-recv[0] & 0x7f)

            self.spi.xfer2([0x80 | 0x10])
            recv = self.spi.readbytes(2)
            tc_rx = 1023-((recv[0] << 2)|(recv[1] >> 6))

            self.spi.xfer2([0x80 | 0x50])
            recv = self.spi.readbytes(2)
            tc_ry = ((recv[0] << 2)|(recv[1] >> 6))

            if self.cs_pin:
                RPi.GPIO.output(self.cs_pin, 1)
            if tc_rz > 10:
                pos_x = self.get_x(tc_rx)
                pos_y = self.get_y(tc_ry)
                if 0 <= pos_x <= self.width and 0 <= pos_y <= self.height:
                    buffer.append((pos_x, pos_y))
            fuse -= 1

        return self._calculate_avr(buffer)

Time to calibrate the panel. After few tries I got

        self.correction = {
            'x': 540,
            'y': 50,
            'ratio_x': 0.94,
            'ratio_y': 1.26,
        }

To calibrate, we need to add print in get_position and set correction (x,y) to 0 and (ratio_x, ratio_y) to 1.
After getting extremes we can calculate offset and ratio.
But there is a funny thing. My screen has reverted y so without additional correction we have bad readings. And to add some more work 🙂 touch panel is 480×320 and not 320×480. We have reversed dimensions from the screen ones.
So it is time for:

Rotation

Before swapping let’s do some refactoring. Currently, we have two separate functions for getting (x,y) but this can be done in one and it is easier to write any swapping conditions.

Correction for 0 and 180 require only subtracting but 90 and 270 require more changes. We are comparing variables with width and height but in horizontal mode, they are swapped.

    def _get_xy(self, offset_x, offset_y):
        """correct x and y"""
        if self.rotate == 0:
            return int((offset_x - self.correction['x']) / self.correction['ratio_x']), \
                self.height - int((offset_y - self.correction['y']) / self.correction['ratio_y'])

        if self.rotate == 90:
            return int((offset_y - self.correction['y']) / self.correction['ratio_y']), \
                int((offset_x - self.correction['x']) / self.correction['ratio_x']),

        if self.rotate == 180:
            return self.width - int((offset_x - self.correction['x']) / self.correction['ratio_x']), \
                int((offset_y - self.correction['y']) / self.correction['ratio_y'])

        if self.rotate == 270:
            return self.height - int((offset_y - self.correction['y']) / self.correction['ratio_y']), \
                self.width - int((offset_x - self.correction['x']) / self.correction['ratio_x'])
(..)
           if tc_rz > 10:
                pos_x, pos_y = self._get_xy(tc_rx, tc_ry)
                if self._in_bounds(pos_x, pos_y):
                    buffer.append((pos_x, pos_y))
(..)
    def _in_bounds(self, pos_x, pos_y):
        """checks if point is in range"""
        if self.rotate == 0 or self.rotate == 280:
            return 0 <= pos_x <= self.width and 0 <= pos_y <= self.height
        else:
            return 0 <= pos_y <= self.width and 0 <= pos_x <= self.height

Now we can set the desired rotation:

touch = XPT2046(480, 320, 17, callback, 7)
touch.rotate = 270

touch.init()

This is for XPT2046, now I need to swap to second RPi and add rotation for AD7843.

Refactoring

We have many common parts with both drivers. It is time to move them to Touch class. For sure we can cut&paste functions: init, _interrupt, close, _calculate_avr, _in_bounds.
And we can extract common variables from the  __init__.

Summary

Yes, another driver 🙂
We support XPT2046 / AD7846. Additionally, we can now set a rotation.
Finally, we made a refactor and extracted Touch abstract class.

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s