board/board-pyportal.go

258 lines
6.1 KiB
Go

//go:build pyportal
package board
import (
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/ili9341"
"tinygo.org/x/drivers/pixel"
"tinygo.org/x/drivers/touch/resistive"
)
const (
Name = "pyportal"
)
var (
Power = dummyBattery{state: NoBattery}
Sensors = baseSensors{} // TODO: light, temperature
Display = mainDisplay{}
Buttons = noButtons{}
)
type mainDisplay struct{}
var display *ili9341.Device
func (d mainDisplay) Configure() Displayer[pixel.RGB565BE] {
// Initialize backlight and disable at startup.
backlight := machine.TFT_BACKLIGHT
backlight.Configure(machine.PinConfig{Mode: machine.PinOutput})
backlight.Low()
// Enable and configure display.
display = ili9341.NewParallel(
machine.LCD_DATA0,
machine.TFT_WR,
machine.TFT_DC,
machine.TFT_CS,
machine.TFT_RESET,
machine.TFT_RD,
)
display.Configure(ili9341.Config{
Rotation: ili9341.Rotation270,
})
// Enable the TE ("tearing effect") pin to read vblank status.
te := machine.TFT_TE
te.Configure(machine.PinConfig{Mode: machine.PinInput})
display.EnableTEOutput(true)
return display
}
func (d mainDisplay) MaxBrightness() int {
return 1
}
func (d mainDisplay) SetBrightness(level int) {
machine.TFT_BACKLIGHT.Set(level > 0)
}
func (d mainDisplay) WaitForVBlank(defaultInterval time.Duration) {
// Wait until the display has finished updating.
// TODO: wait for a pin interrupt instead of blocking.
for machine.TFT_TE.Get() == true {
}
for machine.TFT_TE.Get() == false {
}
}
func (d mainDisplay) PPI() int {
return 166 // appears to be the same size/resolution as the Gopher Badge and the MCH2022 badge
}
// Configure the resistive touch input on this display.
func (d mainDisplay) ConfigureTouch() TouchInput {
machine.InitADC()
resistiveTouch.Configure(&resistive.FourWireConfig{
YP: machine.TOUCH_YD,
YM: machine.TOUCH_YU,
XP: machine.TOUCH_XR,
XM: machine.TOUCH_XL,
})
return touchInput{}
}
var resistiveTouch resistive.FourWire
var touchPoints [1]TouchPoint
type touchInput struct{}
var touchID uint32
// State associated with the touch input.
var (
medianFilterX, medianFilterY medianFilter
iirFilterX, iirFilterY iirFilter
lastPosX, lastPosY int
)
func (input touchInput) ReadTouch() []TouchPoint {
// Values calibrated on the PyPortal I have. Other boards might have
// slightly different values.
// TODO: make this configurable?
const (
xmin = 54000
xmax = 16000
ymin = 48000
ymax = 22000
)
point := resistiveTouch.ReadTouchPoint()
if point.Z > 8192 {
medianFilterX.add(point.X)
medianFilterY.add(point.Y)
var posX, posY int
if touchPoints[0].ID == 0 {
// First touch on the touch screen.
touchID++
touchPoints[0].ID = touchID
for i := 0; i < 4; i++ {
// Initialize the median filter at this point with some more
// samples, so that the entire median filter is filled.
point := resistiveTouch.ReadTouchPoint()
medianFilterX.add(point.X)
medianFilterY.add(point.Y)
}
// Reset the IIR filter, and use the position as-is.
iirFilterX.add(medianFilterX.value(), true)
iirFilterY.add(medianFilterY.value(), true)
posX = iirFilterX.value()
posY = iirFilterY.value()
} else {
// New touch value while we were touching before.
// Add the value to the IIR filter.
iirFilterX.add(medianFilterX.value(), false)
iirFilterY.add(medianFilterY.value(), false)
// Use some hysteresis to avoid moving the point when it didn't
// actually move.
posX = lastPosX
posY = lastPosY
const diff = 400 // arbitrary value that appears to work well
if iirFilterX.value() > lastPosX+diff {
posX = iirFilterX.value() - diff
}
if iirFilterX.value() < lastPosX-diff {
posX = iirFilterX.value() + diff
}
if iirFilterY.value() > lastPosY+diff {
posY = iirFilterY.value() - diff
}
if iirFilterY.value() < lastPosY-diff {
posY = iirFilterY.value() + diff
}
}
lastPosX = posX
lastPosY = posY
x := int16(clamp(posX, ymin, ymax, 0, 239))
y := int16(clamp(posY, xmin, xmax, 0, 319))
if display != nil {
// Adjust for screen rotation.
switch display.Rotation() {
case drivers.Rotation90:
x, y = y, 239-x
case drivers.Rotation180:
x = 239 - x
y = 319 - y
case drivers.Rotation270:
x, y = 319-y, x
}
}
touchPoints[0].Y = y
touchPoints[0].X = x
return touchPoints[:1]
} else {
touchPoints[0].ID = 0
}
return nil
}
// Map and clamp an input value to an output range.
func clamp(value, lowIn, highIn, lowOut, highOut int) int {
rangeIn := highIn - lowIn
rangeOut := highOut - lowOut
valueOut := (value - lowIn) * rangeOut / rangeIn
if valueOut > highOut {
valueOut = highOut
}
if valueOut < lowOut {
valueOut = lowOut
}
return valueOut
}
// Touch screen filtering has been implemented using the description in this
// article:
// https://dlbeer.co.nz/articles/tsf.html
// It works a lot better than the rather naive algorithm I implemented before.
type medianFilter [5]int
func (f *medianFilter) add(n int) {
// Shift the value into the array.
f[0] = f[1]
f[1] = f[2]
f[2] = f[3]
f[3] = f[4]
f[4] = n
}
func (f *medianFilter) value() int {
// Optimal sorting algorithm.
// It is based on the sorting algorithm described here:
// https://bertdobbelaere.github.io/sorting_networks.html
sorted := *f
compareSwap := func(a, b *int) {
if *a > *b {
*b, *a = *a, *b
}
}
compareSwap(&sorted[1], &sorted[4])
compareSwap(&sorted[0], &sorted[3])
compareSwap(&sorted[1], &sorted[3])
compareSwap(&sorted[0], &sorted[2])
compareSwap(&sorted[2], &sorted[4])
compareSwap(&sorted[0], &sorted[1])
compareSwap(&sorted[1], &sorted[2])
compareSwap(&sorted[3], &sorted[4])
compareSwap(&sorted[2], &sorted[3])
// Return the median value.
return sorted[2]
}
// Infinite impulse response filter, to smooth the input values somewhat.
type iirFilter struct {
state int
}
func (f *iirFilter) add(x int, reset bool) {
if reset {
f.state = x
}
// For every update, the new value is half of x and half of the old value,
// added together:
// f.state = f.state*0.5 + x*0.5
f.state = (f.state + x + 1) / 2
}
func (f *iirFilter) value() int {
return f.state
}