Skip to content

Instantly share code, notes, and snippets.

@kmatch98
Last active January 28, 2021 22:45
Show Gist options
  • Save kmatch98/31fb5469b461d94012122fea78c5bf69 to your computer and use it in GitHub Desktop.
Save kmatch98/31fb5469b461d94012122fea78c5bf69 to your computer and use it in GitHub Desktop.
Widget: Round, horizontal switch for CircuitPython using displayio and adafruit_display_shapes. Includes a demo for the PyPortal.
# This is a trial of the switch_round_horizontal
# for use on the PyPortal
#
# To do:
# Allow color handling for both RGB tuples (255, 255, 255) or hex values (0xFFFFFF)
#
import time
#import random
import board
import displayio
from adafruit_display_shapes.circle import Circle
#from adafruit_display_shapes.roundrect import RoundRect
from adafruit_display_shapes.rect import Rect
from switch_round_horizontal import SwitchRoundHorizontal
import adafruit_touchscreen
from adafruit_pyportal import PyPortal
if "DISPLAY" not in dir(board):
# Setup the LCD display with driver
# You may need to change this to match the display driver for the chipset
# used on your display
from adafruit_ili9341 import ILI9341
displayio.release_displays()
# setup the SPI bus
spi = board.SPI()
tft_cs = board.D9 # arbitrary, pin not used
tft_dc = board.D10
tft_backlight = board.D12
tft_reset = board.D11
while not spi.try_lock():
spi.configure(baudrate=32000000)
spi.unlock()
display_bus = displayio.FourWire(
spi,
command=tft_dc,
chip_select=tft_cs,
reset=tft_reset,
baudrate=32000000,
polarity=1,
phase=1,
)
print("spi.frequency: {}".format(spi.frequency))
# Number of pixels in the display
DISPLAY_WIDTH = 320
DISPLAY_HEIGHT = 240
# create the display
display = ILI9341(
display_bus,
width=DISPLAY_WIDTH,
height=DISPLAY_HEIGHT,
rotation=180, # The rotation can be adjusted to match your configuration.
auto_refresh=True,
native_frames_per_second=90,
)
# reset the display to show nothing.
display.show(None)
else:
# built-in display
display = board.DISPLAY
screen_width = 320
screen_height = 240
ts = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR,
board.TOUCH_YD, board.TOUCH_YU,
calibration=((5200, 59000),
(5800, 57000)),
size=(screen_width, screen_height))
# Create an on-off switch
# structural variables
# --------------------
# input parameters:
# x,y: position: upper left corner pixel position
# radius: (pixel diameter of button) *****
# width: (default is calculate based on radius)
# switch_fill_color_off: RGB
# switch_fill_color_on: RGB
# switch_outline_color_off: RGB
# switch_outline_color_on: RGB
# background_color_off: RGB
# background_color_on: RGB
# background_outline_color_off: RGB
# background_outline_color_on: RGB
#
# display_text: Boolean
# animation_time: in seconds
#
# touch_padding: additional pixels outside of the bounding_box to accept touch input, affects touch_boundary
#
#
# other instance variables:
# bounding_box: [x, y, width, height] where x,y are the upper left corner
# touch_boundary: area that responds to touch
# value: Boolean (upon setter change, perform the animation)
# add getter/setter
#
# animation triggers
# selected(x,y): changes from current state to the other state
# setter only
#
# other functions:
# contains(x,y): check for touch input inside of touch_boundary
#
# Options to consider
# -------------------
# rotation?
# text on switch: (0/1)
#
#test_circle=Circle(x0=0, y0=0, r=50)
#print("test_circle.x: {}, .y: {}".format(test_circle.x, test_circle.y))
def color_fade(start_color, end_color, total_steps, step_number):
if step_number >= total_steps-1:
return end_color
if step_number <= 0:
return start_color
else:
faded_color = [0,0,0]
for i in range(3):
faded_color[i] = start_color[i] - int((step_number * (start_color[i]-end_color[i]))/total_steps)
return faded_color
my_group = displayio.Group(max_size=3)
# Group elements:
# 1. switch_roundrect: The switch background
# 2. switch_circle: The switch button
# 3. text_0 or text_1: The text on the switch button
switch_x = 30
switch_y = 30
switch_radius = 20
switch_fill_color_off = (66, 44, 66)
switch_fill_color_on = (0, 100, 0)
switch_outline_color_off = (30, 30, 30)
switch_outline_color_on = (0, 60, 0)
background_color_off = (255, 255, 255)
background_color_on = (90, 255, 90)
background_outline_color_off = background_color_off
background_outline_color_on = background_color_on
switch_width=4*switch_radius # This is a good aspect ratio to start with
switch_stroke = 2 # Width of the outlines (in pixels)
text_stroke = switch_stroke # width of text lines
touch_padding = 0 # Additional boundary around widget that will accept touch input
animation_time = 0.2 # time for switch to display change (in seconds). 0.15 is a good starting point
display_text = True # show the text (0/1)
# initialize state variables
switch_value=False
switch_value=True
my_switch=SwitchRoundHorizontal(x=switch_x, y=switch_y,
height=switch_radius*2,
fill_color_off=switch_fill_color_off,
fill_color_on=switch_fill_color_on,
outline_color_off=switch_outline_color_off,
outline_color_on=switch_outline_color_on,
background_color_off=background_color_off,
background_color_on=background_color_on,
background_outline_color_off=background_outline_color_off,
background_outline_color_on=background_outline_color_on,
switch_stroke=switch_stroke,
display_button_text=display_text,
touch_padding=10,
animation_time=animation_time,
value=False)
my_switch2=SwitchRoundHorizontal(x=switch_x+100, y=switch_y,
height=switch_radius*2,
fill_color_off=switch_fill_color_off,
fill_color_on=switch_fill_color_on,
outline_color_off=switch_outline_color_off,
outline_color_on=switch_outline_color_on,
background_color_off=background_color_off,
background_color_on=background_color_on,
background_outline_color_off=background_outline_color_off,
background_outline_color_on=background_outline_color_on,
switch_stroke=switch_stroke,
display_button_text=False,
touch_padding=touch_padding,
animation_time=animation_time,
value=False)
my_switch3=SwitchRoundHorizontal(x=switch_x, y=switch_y+55,
height=switch_radius*2,
fill_color_off=(255, 0, 0),
fill_color_on=switch_fill_color_on,
outline_color_off=(80, 0, 0),
outline_color_on=switch_outline_color_on,
background_color_off=(150, 0, 0),
background_color_on=background_color_on,
background_outline_color_off=(30, 0, 0),
background_outline_color_on=background_outline_color_on,
switch_stroke=switch_stroke,
display_button_text=True,
touch_padding=touch_padding,
animation_time=animation_time,
value=False)
my_switch4=SwitchRoundHorizontal(x=switch_x+100, y=switch_y+55,
height=switch_radius*2,
fill_color_off=(255, 0, 0),
fill_color_on=switch_fill_color_on,
outline_color_off=(80, 0, 0),
outline_color_on=switch_outline_color_on,
background_color_off=(150, 0, 0),
background_color_on=background_color_on,
background_outline_color_off=(30, 0, 0),
background_outline_color_on=background_outline_color_on,
switch_stroke=switch_stroke,
display_button_text=False,
touch_padding=touch_padding,
animation_time=animation_time,
value=False)
my_switch5=SwitchRoundHorizontal(x=0, y=0,
height=switch_radius*4,
fill_color_off=switch_fill_color_off,
fill_color_on=switch_fill_color_on,
outline_color_off=switch_outline_color_off,
outline_color_on=switch_outline_color_on,
background_color_off=background_color_off,
background_color_on=background_color_on,
background_outline_color_off=background_outline_color_off,
background_outline_color_on=background_outline_color_on,
switch_stroke=switch_stroke,
display_button_text=True,
touch_padding=10,
animation_time=0.3, # for larger button, may want to extend the animation time
text_stroke=6, ## Add a wider text stroke
value=False)
# demonstrate relocation of a switch position using anchor_point, anchored_position
#my_switch.x=0
#my_switch.y=0
my_switch5.anchor_point=(1,1)
my_switch5.anchored_position=(305, 225)
# visually verify the bounding_box and touch_boundary
this_switch=my_switch5
rect=Rect(x=this_switch.x, y=this_switch.y,
width=this_switch.bounding_box[2],
height=this_switch.bounding_box[3],
fill=0x777777,
outline=0x000000,
stroke=1)
rect2=Rect(x=this_switch.x+this_switch.touch_boundary[0],
y=this_switch.y+this_switch.touch_boundary[1],
width=this_switch.touch_boundary[2],
height=this_switch.touch_boundary[3],
fill=0xAAAAAA,
outline=0xFFFFFF, stroke=1)
my_group=displayio.Group(max_size=8)
#my_group.append(rect2)
#my_group.append(rect)
my_group.append(my_switch)
my_group.append(my_switch2)
my_group.append(my_switch3)
my_group.append(my_switch4)
my_group.append(my_switch5)
# Add my_group to the display
display.show(my_group)
display.refresh(target_frames_per_second=60, minimum_frames_per_second=30)
## Robotic switching demo
# sleep_time=0.5
# fake_touch_point=[0,0,0]
# time.sleep(sleep_time)
# my_switch.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch2.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch2.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch3.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch4.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch3.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch4.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch5.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch5.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch5.selected(fake_touch_point)
# time.sleep(sleep_time)
# my_switch5.selected(fake_touch_point)
# Start the main loop
while True:
p = ts.touch_point
#print("touch_point p: {}".format(p))
if p:
#this_switch=my_switch5
#print("this_switch x,y: {},{}, touch_boundary: {}".format(this_switch.x, this_switch.y, this_switch.touch_boundary))
#print("bounding_box: {}".format(this_switch.bounding_box))
if my_switch.contains(p):
my_switch.selected(p)
elif my_switch2.contains(p):
my_switch2.selected(p)
elif my_switch3.contains(p):
my_switch3.selected(p)
elif my_switch4.contains(p):
my_switch4.selected(p)
elif my_switch5.contains(p):
#print("switch5 touched")
my_switch5.selected(p)
time.sleep(0.05) # touch responce is more accurate with a small delayed added
# The MIT License (MIT)
#
# Copyright (c) 2021 Kevin Matocha (kmatch98)
#
# 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.
#
################################
# A round switch widget for CircuitPython, using displayio and adafruit_display_shapes
#
# Features:
# - Color grading as the switch animates between the off and on states
# - Option to display 0 or 1 to confirm the switch state (display_button_text=True)
# - Provides setting for animation_time (approximate), and adapts redraw rate based on real time.
#
#
# Open items: ******
# - Clarify x,y placement for widgets. Should this correspond to the upper-left corner or the center?
# - Update color handling for either RGB tuples or RGB hex codes
# - For `selected` function, make `touch_point` input as optional
# - Update documentation
#
#
# Structural parameters
# --------------------
# - x,y: position: upper left corner pixel position
# - width: (default width is calculated based on radius)
# - height: (pixel height of button, may be rounded up by 1)
# - name: string
# - switch_fill_color_off: RGB
# - switch_fill_color_on: RGB
# - switch_outline_color_off: RGB
# - switch_outline_color_on: RGB
# - background_color_off: RGB
# - background_color_on: RGB
# - background_outline_color_off: RGB
# - background_outline_color_on: RGB
#
# - display_button_text: Boolean to select if the 0/1 is displayed on the switch
# - animation_time: in seconds, approximate duration of the animation
#
# - touch_padding: additional pixels outside of the bounding_box to accept touch input, affects `touch_boundary`
#
#
# other instance variables:
# -------------------------
# - value: Boolean getter/setter (upon a state change, this will perform the switch animation)
# - bounding_box: [x, y, width, height] where x,y is the upper left corner offset from self.x, self.y
# - touch_boundary: [x, y, width, height] touch area, where x,y is the upper left corner offset from self.x, self.y
# Note that the `touch_boundary` can be different from the bounding_box
#
# animation trigger:
# ------------------
# - selected(touch_point): flips the switch, setter only
#
# other functions:
# ----------------
# - contains(touch_point): check for touch input inside of `touch_boundary`
#
# Future options to consider:
# ---------------------------
# rotation?
#
import displayio
import time
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.roundrect import RoundRect
from adafruit_display_shapes.rect import Rect
def _color_fade(start_color, end_color, fraction):
if fraction >= 1:
return end_color
if fraction <= 0:
return start_color
else:
faded_color = [0,0,0]
for i in range(3):
faded_color[i] = start_color[i] - int((start_color[i]-end_color[i])* fraction)
return faded_color
class SwitchRoundHorizontal(displayio.Group):
def __init__(
self,
*,
x=0, # Placement of upper left corner pixel position on the display
y=0,
width=None, # defaults to the 4*radius
height=30,
name="",
value=False,
touch_padding=0,
anchor_point=None,
anchored_position=None,
fill_color_off=(66, 44, 66),
fill_color_on=(0,100,0),
outline_color_off=(30,30,30),
outline_color_on=(0,60,0),
background_color_off=(255,255,255),
background_color_on=(0,60,0),
background_outline_color_off=None, # default to background_color_off
background_outline_color_on=None, # default to background_color_on
switch_stroke=2,
text_stroke=None, # default to switch_stroke
display_button_text=True,
animation_time=0.2, # animation duration (in seconds)
):
super().__init__(x=x, y=y, max_size=3)
# Group elements:
# 1. switch_roundrect: The switch background
# 2. switch_circle: The switch button
# 3. text_0 or text_1: The text on the switch button
self.name = name
self._radius = height//2
switch_x = self._radius
switch_y = self._radius
if width is None:
self._width = 4 * self._radius
else:
self._width = width
self._name = name # button name
if background_outline_color_off is None:
background_outline_color_off = background_color_off
if background_outline_color_on is None:
background_outline_color_on = background_color_on
self._fill_color_off = fill_color_off
self._fill_color_on = fill_color_on
self._outline_color_off = outline_color_off
self._outline_color_on = outline_color_on
self._background_color_off = background_color_off
self._background_color_on = background_color_on
self._background_outline_color_off = background_outline_color_off
self._background_outline_color_on = background_outline_color_on
self._switch_stroke=switch_stroke
if text_stroke is None:
text_stroke = switch_stroke # width of text lines
self._text_stroke = text_stroke
self._display_button_text = display_button_text # state variable whether text (0/1) is displayed
self._touch_padding = touch_padding
self._animation_time = animation_time
self._value = value
self._text_0_on = not value # controls which text value is displayed (0 or 1)
self._anchor_point = anchor_point
self._anchored_position = anchored_position
# initialize the display elements
self._switch_circle = Circle(x0=switch_x, y0=switch_y,
r=self._radius,
fill=self._fill_color_off,
outline=self._outline_color_off,
stroke=self._switch_stroke)
self._switch_roundrect = RoundRect(x=switch_x-self._radius, y=switch_y-self._radius,
r=self._radius,
width=self._width, height=2*self._radius+1,
fill=self._background_color_off,
outline=self._background_outline_color_off, stroke=self._switch_stroke)
self._bounding_box = [self._switch_circle.x,
self._switch_circle.y,
self._width,
2*self._radius+1]
# bounding_box defines the "local" x and y.
# Must be offset by self.x and self.y to get the raw display coordinates
self._touch_boundary = [self._bounding_box[0]-self._touch_padding,
self._bounding_box[1]-self._touch_padding,
self._bounding_box[2]+2*self._touch_padding,
self._bounding_box[3]+2*self._touch_padding]
# The 0 text circle
self._text_0 = Circle(x0=switch_x, y0=switch_y,
r=self._radius//2,
fill=self._fill_color_off,
outline=self._outline_color_off,
stroke=self._text_stroke)
# The 1 text rectangle
self._text_1 = Rect(x=switch_x-self._switch_stroke+1, y=switch_y-self._radius//2,
height=self._radius,
width=self._text_stroke,
fill=self._fill_color_off,
outline=self._outline_color_off,
stroke=self._text_stroke)
# Store initial positions of the moving parts
self._switch_initial_x=self._switch_circle.x
self._text_0_initial_x=self._text_0.x
self._text_1_initial_x=self._text_1.x
# Set the initial switch position based on the starting value
if value:
self._draw_position(1)
else:
self._draw_position(0)
# Add the display elements to the self group
self.append(self._switch_roundrect)
self.append(self._switch_circle)
# Add the correct text element, if display_button_text is True
if display_button_text:
if (self._text_0_on):
self.append(self._text_0)
else:
self.append(self._text_1)
# update the position, if required
self._update_position
def _update_position(self):
# reposition self group based on anchor_point and anchored_position
if (self._anchor_point is not None) and (self._anchored_position is not None):
self.x=self._anchored_position[0]-int(self._anchor_point[0]*self._bounding_box[2]) - self._bounding_box[0]
self.y=self._anchored_position[1]-int(self._anchor_point[1]*self._bounding_box[3]) - self._bounding_box[1]
def _draw_position(self, position):
# To deal with any integer rounding errors
if position <= 0: # left-end position
self._switch_circle.x = self._switch_initial_x
self._text_0.x=self._text_0_initial_x
self._text_1.x=self._text_1_initial_x
elif position >= 1: # right-end position
self._switch_circle.x = self._switch_initial_x + self._width-2*self._radius-1
self._text_0.x = self._text_0_initial_x + self._width-2*self._radius-1
self._text_1.x = self._text_1_initial_x + self._width-2*self._radius-1
else: # somewhere in the middle
self._switch_circle.x = self._switch_initial_x + int((self._width - (2 * self._radius) - 1)*position)
self._text_0.x = self._text_0_initial_x + int((self._width - (2 * self._radius) - 1)*position)
self._text_1.x = self._text_1_initial_x + int((self._width - (2 * self._radius) - 1)*position)
# Set the color to the correct fade
self._switch_circle.fill = _color_fade(self._fill_color_off, self._fill_color_on, position)
self._switch_circle.outline = _color_fade(self._outline_color_off, self._outline_color_on, position)
self._switch_roundrect.fill = _color_fade(self._background_color_off, self._background_color_on, position)
self._switch_roundrect.outline = _color_fade(self._background_outline_color_off, self._background_outline_color_on, position)
self._text_0.fill = self._switch_circle.fill
self._text_1.fill = self._switch_circle.fill
self._text_0.outline = self._switch_circle.outline
self._text_1.outline = self._switch_circle.outline
if (self._display_button_text and position > 0.5 and self._text_0_on):
self.pop()
self.append(self._text_1)
self._text_0_on = False
elif (self._display_button_text and position < 0.5 and not self._text_0_on):
self.pop()
self.append(self._text_0)
self._text_0_on = True
def selected(self, touch_point): # requires passing display to allow auto_refresh off when redrawing
# touch_point is a tuple: (x, y, pressure)
start_time=time.monotonic()
while True:
if self._value:
position = 1 - (time.monotonic() - start_time)/self._animation_time # fraction from 0 to 1
else:
position = (time.monotonic() - start_time)/self._animation_time # fraction from 0 to 1
self._draw_position(position) # update the switch position
if ((position >= 1) and not self._value): # ensures that the final position is drawn
self._value=True
break
if ((position <= 0) and self._value): # ensures that the final position is drawn
self._value=False
break
def contains(self, touch_point):
"""Returns True if the touch_point is within the widget's touch_boundary."""
touch_x = touch_point[0] - self.x # adjust touch position for the local position
touch_y = touch_point[1] - self.y
if ( (self._touch_boundary[0] <= touch_x <= (self._touch_boundary[0]+self._touch_boundary[2])) and
(self._touch_boundary[1] <= touch_y <= (self._touch_boundary[1]+self._touch_boundary[3])) ):
return True
return False
@property
def bounding_box(self):
"""The boundary of the widget. Defined as a [x, y, width, height] where x,y are
offset from self.x and self.y."""
return self._bounding_box
@property
def touch_boundary(self):
"""The region that responds to touch using the `contain(touch_point)` function.
Defined as a [x, y, width, height] where x,y are offset from self.x and self.y."""
return self._touch_boundary
@touch_boundary.setter
def touch_boundary(self, new_touch_boundary):
self._touch_boundary = new_touch_boundary
@property
def value(self):
"""The current Boolean switch value."""
return self._value
@value.setter
def value(self, new_value):
if (new_value != self._value):
fake_touch_point=[0,0,0]
self.selected(fake_touch_point)
@property
def anchor_point(self):
"""The anchor point for positioning the switch, works in concert with `anchored_position`."""
return self._anchor_point
@anchor_point.setter
def anchor_point(self, new_anchor_point):
self._anchor_point = new_anchor_point
self._update_position()
@property
def anchored_position(self):
"""The anchored position for positioning the switch, works in concert with `anchor_point`."""
return self._anchored_position
@anchored_position.setter
def anchored_position(self, new_anchored_position):
self._anchored_position = new_anchored_position
self._update_position()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment