move changes to new folder leaving original as it was

This commit is contained in:
RetiredWizard 2025-08-15 16:34:35 -04:00
parent 21ce0d8ae1
commit 3f902f837a
4 changed files with 815 additions and 385 deletions

View file

@ -16,26 +16,12 @@ Licensed under the MIT license.
All text above must be included in any redistribution.
"""
import array
import gc
import time
import os
import supervisor
import board
import displayio
import adafruit_logging as logging
try:
import usb.core
usb_available = True
except ImportError:
usb_available = False
try:
from adafruit_fruitjam.peripherals import request_display_config
from adafruit_fruitjam.peripherals import VALID_DISPLAY_SIZES
except ImportError:
request_display_config = None
try:
import adafruit_touchscreen
except ImportError:
@ -105,9 +91,9 @@ class TouchscreenPoller(object):
if p is not None:
self._cursor_grp.x = p[0] - self._x_offset
self._cursor_grp.y = p[1] - self._y_offset
return True, False, p
return True, p
else:
return False, False, None
return False, None
def poke(self, location=None):
"""Force a bitmap refresh."""
@ -157,7 +143,7 @@ class CursorPoller(object):
self._mouse_cursor.x + self._x_offset,
self._mouse_cursor.y + self._y_offset,
)
return a_button, False, location
return a_button, location
def poke(self, x=None, y=None):
"""Force a bitmap refresh."""
@ -175,281 +161,9 @@ class CursorPoller(object):
################################################################################
class MousePoller(object):
"""Get 'pressed' and location updates from a USB mouse."""
def __init__(self, splash, cursor_bmp, screen_width, screen_height):
self._logger = logging.getLogger("Paint")
if not self._logger.hasHandlers():
self._logger.addHandler(logging.StreamHandler())
self._logger.debug("Creating a MousePoller")
self._display_grp = splash
self._cursor_grp = displayio.Group()
self._cur_palette = displayio.Palette(3)
self._cur_palette.make_transparent(0)
self._cur_palette[1] = 0xFFFFFF
self._cur_palette[2] = 0x0000
self._cur_sprite = displayio.TileGrid(
cursor_bmp, pixel_shader=self._cur_palette
)
self._cursor_grp.append(self._cur_sprite)
self._display_grp.append(self._cursor_grp)
self._x_offset = cursor_bmp.width // 2
self._y_offset = cursor_bmp.height // 2
self.SCREEN_WIDTH = screen_width
self.SCREEN_HEIGHT = screen_height
# Mouse state
self.last_left_button_state = 0
self.last_right_button_state = 0
self.left_button_pressed = False
self.right_button_pressed = False
self.mouse = None
self.buf = None
self.in_endpoint = None
# Mouse position
self.mouse_x = screen_width // 2
self.mouse_y = screen_height // 2
mouse_found = False
if self.find_mouse():
mouse_found = True
else:
self._logger.debug("WARNING: Mouse not found after multiple attempts.")
self._logger.debug("The application will run, but mouse control may not work.")
def find_mouse(self):
"""Find the mouse device with multiple retry attempts"""
MAX_ATTEMPTS = 5
RETRY_DELAY = 1 # seconds
if not usb_available:
self._logger.debug("USB library not available; cannot find mouse.")
return False
for attempt in range(MAX_ATTEMPTS):
try:
self._logger.debug(f"Mouse detection attempt {attempt+1}/{MAX_ATTEMPTS}")
# Constants for USB control transfers
DIR_OUT = 0
# DIR_IN = 0x80 # Unused variable
REQTYPE_CLASS = 1 << 5
REQREC_INTERFACE = 1 << 0
HID_REQ_SET_PROTOCOL = 0x0B
# Find all USB devices
devices_found = False
for device in usb.core.find(find_all=True):
devices_found = True
self._logger.debug(f"Found device: {device.idVendor:04x}:{device.idProduct:04x}")
try:
# Try to get device info
try:
manufacturer = device.manufacturer
product = device.product
except Exception: # pylint: disable=broad-except
manufacturer = "Unknown"
product = "Unknown"
# Just use whatever device we find
self.mouse = device
# Try to detach kernel driver
try:
has_kernel_driver = hasattr(device, 'is_kernel_driver_active')
if has_kernel_driver and device.is_kernel_driver_active(0):
device.detach_kernel_driver(0)
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error detaching kernel driver: {e}")
# Set configuration
try:
device.set_configuration()
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error setting configuration: {e}")
continue # Try next device
# Just assume endpoint 0x81 (common for mice)
self.in_endpoint = 0x81
self._logger.debug(f"Using mouse: {manufacturer}, {product}")
# Set to report protocol mode
try:
bmRequestType = DIR_OUT | REQTYPE_CLASS | REQREC_INTERFACE
bRequest = HID_REQ_SET_PROTOCOL
wValue = 1 # 1 = report protocol
wIndex = 0 # First interface
buf = bytearray(1)
device.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, buf)
self._logger.debug("Set to report protocol mode")
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Could not set protocol: {e}")
# Buffer for reading data
self.buf = array.array("B", [0] * 4)
self._logger.debug("Created 4-byte buffer for mouse data")
# Verify mouse works by reading from it
try:
# Try to read some data with a short timeout
data = device.read(self.in_endpoint, self.buf, timeout=100)
self._logger.debug(f"Mouse test read successful: {data} bytes")
return True
except usb.core.USBTimeoutError:
# Timeout is normal if mouse isn't moving
self._logger.debug("Mouse connected but not sending data (normal)")
return True
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Mouse test read failed: {e}")
# Continue to try next device or retry
self.mouse = None
self.in_endpoint = None
continue
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error initializing device: {e}")
continue
if not devices_found:
self._logger.debug("No USB devices found")
# If we get here without returning, no suitable mouse was found
self._logger.debug(f"No working mouse found on attempt {attempt+1}, retrying...")
gc.collect()
time.sleep(RETRY_DELAY)
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error during mouse detection: {e}")
gc.collect()
time.sleep(RETRY_DELAY)
self._logger.debug("Failed to find a working mouse after multiple attempts")
return False
def poll(self):
"""Check for input. Returns contact (a bool), False (no button B),
and it's location ((x,y) or None)"""
if self._process_mouse_input():
self._cursor_grp.x = self.mouse_x - self._x_offset
self._cursor_grp.y = self.mouse_y - self._y_offset
return self.left_button_pressed, self.right_button_pressed, \
(self.mouse_x, self.mouse_y)
else:
return False, False, None
def poke(self, location=None):
"""Force a bitmap refresh."""
self._display_grp.remove(self._cursor_grp)
if location is not None:
self._cursor_grp.x = location[0] - self._x_offset
self._cursor_grp.y = location[1] - self._y_offset
self._display_grp.append(self._cursor_grp)
def set_cursor_bitmap(self, bmp):
"""Update the cursor bitmap.
:param bmp: the new cursor bitmap
"""
self._cursor_grp.remove(self._cur_sprite)
self._cur_sprite = displayio.TileGrid(bmp, pixel_shader=self._cur_palette)
self._cursor_grp.append(self._cur_sprite)
self.poke()
def _process_mouse_input(self):
"""Process mouse input - simplified version without wheel support"""
try:
# Attempt to read data from the mouse (10ms timeout)
count = self.mouse.read(self.in_endpoint, self.buf, timeout=10)
except usb.core.USBTimeoutError:
# Timeout is normal if mouse isn't moving
return False
except usb.core.USBError as e:
# Handle timeouts silently
if e.errno == 110: # Operation timed out
return False
# Handle disconnections
if e.errno == 19: # No such device
self._logger.debug("Mouse disconnected")
self.mouse = None
self.in_endpoint = None
gc.collect()
return False
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error reading mouse: {type(e).__name__}")
return False
if count >= 3: # We need at least buttons, X and Y
# Extract mouse button states
buttons = self.buf[0]
x = self.buf[1]
y = self.buf[2]
# Convert to signed values if needed
if x > 127:
x = x - 256
if y > 127:
y = y - 256
# Extract button states
current_left_button_state = buttons & 0x01
current_right_button_state = (buttons & 0x02) >> 1
# Detect button presses
if current_left_button_state == 1 and self.last_left_button_state == 0:
self.left_button_pressed = True
elif current_left_button_state == 0 and self.last_left_button_state == 1:
self.left_button_pressed = False
if current_right_button_state == 1 and self.last_right_button_state == 0:
self.right_button_pressed = True
elif current_right_button_state == 0 and self.last_right_button_state == 1:
self.right_button_pressed = False
# Update button states
self.last_left_button_state = current_left_button_state
self.last_right_button_state = current_right_button_state
# Update position
self.mouse_x += x
self.mouse_y += y
# Ensure position stays within bounds
self.mouse_x = max(0, min(self.SCREEN_WIDTH - 1, self.mouse_x))
self.mouse_y = max(0, min(self.SCREEN_HEIGHT - 1, self.mouse_y))
return True
return False
################################################################################
class Paint(object):
def __init__(self, display=None):
if display is None:
if hasattr(board, "DISPLAY"):
display = board.DISPLAY
else:
if request_display_config is not None:
request_display_config(320, 240)
display = supervisor.runtime.display
else:
raise RuntimeError("No display found.")
def __init__(self, display=board.DISPLAY):
self._logger = logging.getLogger("Paint")
if not self._logger.hasHandlers():
self._logger.addHandler(logging.StreamHandler())
@ -470,7 +184,7 @@ class Paint(object):
)
self._splash.append(self._bg_sprite)
self._palette_bitmap = displayio.Bitmap(self._w, self._h, 8)
self._palette_bitmap = displayio.Bitmap(self._w, self._h, 5)
self._palette_palette = displayio.Palette(len(Color.colors))
for i, c in enumerate(Color.colors):
self._palette_palette[i] = c
@ -479,11 +193,10 @@ class Paint(object):
)
self._splash.append(self._palette_sprite)
self._fg_bitmap = displayio.Bitmap(self._w, self._h, 9)
self._fg_palette = displayio.Palette(len(Color.colors)+1)
self._fg_bitmap = displayio.Bitmap(self._w, self._h, 5)
self._fg_palette = displayio.Palette(len(Color.colors))
for i, c in enumerate(Color.colors):
self._fg_palette[i] = c
self._fg_palette[8] = Color.PINK # Marker for filled areas
self._fg_sprite = displayio.TileGrid(
self._fg_bitmap, pixel_shader=self._fg_palette, x=0, y=0
)
@ -513,24 +226,18 @@ class Paint(object):
self._poller = TouchscreenPoller(self._splash, self._cursor_bitmaps[0])
elif hasattr(board, "BUTTON_CLOCK"):
self._poller = CursorPoller(self._splash, self._cursor_bitmaps[0])
elif usb_available:
self._poller = MousePoller(self._splash, self._cursor_bitmaps[0], self._w, self._h)
if not self._poller.mouse:
raise RuntimeError("No mouse found. Please connect a USB mouse.")
else:
raise AttributeError("PyPaint requires a touchscreen or cursor.")
self._a_pressed = False
self._last_a_pressed = False
self._b_pressed = False
self._last_b_pressed = False
self._location = None
self._last_location = None
self._pencolor = 7
def _make_palette(self):
self._palette_bitmap = displayio.Bitmap(self._w // 10, self._h, 8)
self._palette_bitmap = displayio.Bitmap(self._w // 10, self._h, 5)
self._palette_palette = displayio.Palette(len(Color.colors))
for i, c in enumerate(Color.colors):
self._palette_palette[i] = c
@ -657,73 +364,6 @@ class Paint(object):
else:
x0 += 1
def _fill(self, x, y, c):
"""Fill an area with the current color.
:param x: x coordinate to start filling from
:param y: y coordinate to start filling from
:param c: color to fill with
"""
MARKER = 8 # Marker for filled areas
self._logger.debug("Filling at (%d, %d) with color %d", x, y, c)
if self._fg_bitmap[x, y] != c:
blank_color = self._fg_bitmap[x, y]
self._fg_bitmap[x, y] = MARKER
done = False
min_row = 0
max_row = self._h - 1
min_col = (self._w // 10) + 1
max_col = self._w - 1
while not done:
newmin_row = self._h - 1
newmax_row = 0
newmin_col = self._w - 1
newmax_col = (self._w // 10) + 1
done = True
#self._logger.debug("Rows: %d to %d Cols: %d to %d" , min_row, max_row, min_col, max_col)
for i in range(min_row,max_row):
for j in range(min_col,max_col):
if self._fg_bitmap[j, i] == MARKER:
newmarker = False
if j > self._w // 10 and self._fg_bitmap[j - 1, i] == blank_color:
self._fg_bitmap[j - 1, i] = MARKER
newmarker = True
if j < self._w - 1 and self._fg_bitmap[j + 1, i] == blank_color:
self._fg_bitmap[j + 1, i] = MARKER
newmarker = True
if i > 0 and self._fg_bitmap[j, i - 1] == blank_color:
self._fg_bitmap[j, i - 1] = MARKER
newmarker = True
if i < self._h - 1 and self._fg_bitmap[j, i + 1] == blank_color:
self._fg_bitmap[j, i + 1] = MARKER
newmarker = True
if newmarker:
done = False
if i < newmin_row:
newmin_row = i
if i > newmax_row:
newmax_row = i
if j < newmin_col:
newmin_col = j
if j > newmax_col:
newmax_col = j
max_row = min(newmax_row + 2, self._h - 1)
min_row = max(newmin_row - 2, 0)
max_col = min(newmax_col + 2, self._w - 1)
min_col = max(newmin_col - 2, (self._w // 10) + 1)
self._poller.poke()
for i in range(self._h):
for j in range(self._w):
if self._fg_bitmap[j, i] == MARKER:
self._fg_bitmap[j, i] = c
self._poller.poke()
def _handle_palette_selection(self, location):
selected = location[1] // self._swatch_height
if selected >= self._number_of_palette_options:
@ -752,12 +392,6 @@ class Paint(object):
def _handle_a_release(self, location):
self._logger.debug("A Released!")
def _handle_b_release(self, location):
self._logger.debug("B Released!")
if location[0] >= self._w // 10: # not in color picker
self._fill(location[0], location[1], self._pencolor)
self._poller.poke()
@property
def _was_a_just_pressed(self):
return self._a_pressed and not self._last_a_pressed
@ -766,10 +400,6 @@ class Paint(object):
def _was_a_just_released(self):
return not self._a_pressed and self._last_a_pressed
@property
def _was_b_just_released(self):
return not self._b_pressed and self._last_b_pressed
@property
def _did_move(self):
if self._location is not None and self._last_location is not None:
@ -780,11 +410,8 @@ class Paint(object):
return False
def _update(self):
self._last_a_pressed = self._a_pressed
self._last_b_pressed = self._b_pressed
if self._location is not None:
self._last_location = self._location
self._a_pressed, self._b_pressed, self._location = self._poller.poll()
self._last_a_pressed, self._last_location = self._a_pressed, self._location
self._a_pressed, self._location = self._poller.poll()
def run(self):
"""Run the painting program."""
@ -794,8 +421,6 @@ class Paint(object):
self._handle_a_press(self._location)
elif self._was_a_just_released:
self._handle_a_release(self._location)
if self._was_b_just_released:
self._handle_b_release(self._last_location)
if self._did_move and self._a_pressed:
self._handle_motion(self._last_location, self._location)
time.sleep(0.1)

View file

@ -0,0 +1,805 @@
# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Paint for PyPortal, PyBadge, PyGamer, and the like.
Adafruit invests time and resources providing this open source code.
Please support Adafruit and open source hardware by purchasing
products from Adafruit!
Written by Dave Astels for Adafruit Industries
Copyright (c) 2019 Adafruit Industries
Licensed under the MIT license.
All text above must be included in any redistribution.
"""
import array
import gc
import time
import os
import supervisor
import board
import displayio
import adafruit_logging as logging
try:
import usb.core
usb_available = True
except ImportError:
usb_available = False
try:
from adafruit_fruitjam.peripherals import request_display_config
from adafruit_fruitjam.peripherals import VALID_DISPLAY_SIZES
except ImportError:
request_display_config = None
try:
import adafruit_touchscreen
except ImportError:
pass
try:
from adafruit_cursorcontrol.cursorcontrol import Cursor
from adafruit_cursorcontrol.cursorcontrol_cursormanager import DebouncedCursorManager
except ImportError:
pass
class Color(object):
"""Standard colors"""
WHITE = 0xFFFFFF
BLACK = 0x000000
RED = 0xFF0000
ORANGE = 0xFFA500
YELLOW = 0xFFFF00
GREEN = 0x00FF00
BLUE = 0x0000FF
PURPLE = 0x800080
PINK = 0xFFC0CB
colors = (BLACK, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, WHITE)
def __init__(self):
pass
################################################################################
class TouchscreenPoller(object):
"""Get 'pressed' and location updates from a touch screen device."""
def __init__(self, splash, cursor_bmp):
logger = logging.getLogger("Paint")
if not logger.hasHandlers():
logger.addHandler(logging.StreamHandler())
logger.debug("Creating a TouchscreenPoller")
self._display_grp = splash
self._touchscreen = adafruit_touchscreen.Touchscreen(
board.TOUCH_XL, board.TOUCH_XR,
board.TOUCH_YD, board.TOUCH_YU,
calibration=((9000, 59000), (8000, 57000)),
size=(320, 240),
)
self._cursor_grp = displayio.Group()
self._cur_palette = displayio.Palette(3)
self._cur_palette.make_transparent(0)
self._cur_palette[1] = 0xFFFFFF
self._cur_palette[2] = 0x0000
self._cur_sprite = displayio.TileGrid(
cursor_bmp, pixel_shader=self._cur_palette
)
self._cursor_grp.append(self._cur_sprite)
self._display_grp.append(self._cursor_grp)
self._x_offset = cursor_bmp.width // 2
self._y_offset = cursor_bmp.height // 2
def poll(self):
"""Check for input. Returns contact (a bool), False (no button B),
and it's location ((x,y) or None)"""
p = self._touchscreen.touch_point
if p is not None:
self._cursor_grp.x = p[0] - self._x_offset
self._cursor_grp.y = p[1] - self._y_offset
return True, False, p
else:
return False, False, None
def poke(self, location=None):
"""Force a bitmap refresh."""
self._display_grp.remove(self._cursor_grp)
if location is not None:
self._cursor_grp.x = location[0] - self._x_offset
self._cursor_grp.y = location[1] - self._y_offset
self._display_grp.append(self._cursor_grp)
def set_cursor_bitmap(self, bmp):
"""Update the cursor bitmap.
:param bmp: the new cursor bitmap
"""
self._cursor_grp.remove(self._cur_sprite)
self._cur_sprite = displayio.TileGrid(bmp, pixel_shader=self._cur_palette)
self._cursor_grp.append(self._cur_sprite)
self.poke()
################################################################################
class CursorPoller(object):
"""Get 'pressed' and location updates from a D-Pad/joystick device."""
def __init__(self, splash, cursor_bmp):
self._logger = logging.getLogger("Paint")
if not self._logger.hasHandlers():
self._logger.addHandler(logging.StreamHandler())
self._logger.debug("Creating a CursorPoller")
self._mouse_cursor = Cursor(
board.DISPLAY, display_group=splash, bmp=cursor_bmp, cursor_speed=2
)
self._x_offset = cursor_bmp.width // 2
self._y_offset = cursor_bmp.height // 2
self._cursor = DebouncedCursorManager(self._mouse_cursor)
def poll(self):
"""Check for input. Returns press of A (a bool), B,
and the cursor location ((x,y) or None)"""
location = None
self._cursor.update()
a_button = self._cursor.held
if a_button:
location = (
self._mouse_cursor.x + self._x_offset,
self._mouse_cursor.y + self._y_offset,
)
return a_button, False, location
def poke(self, x=None, y=None):
"""Force a bitmap refresh."""
self._mouse_cursor.hide()
self._mouse_cursor.show()
def set_cursor_bitmap(self, bmp):
"""Update the cursor bitmap.
:param bmp: the new cursor bitmap
"""
self._mouse_cursor.cursor_bitmap = bmp
self.poke()
################################################################################
class MousePoller(object):
"""Get 'pressed' and location updates from a USB mouse."""
def __init__(self, splash, cursor_bmp, screen_width, screen_height):
self._logger = logging.getLogger("Paint")
if not self._logger.hasHandlers():
self._logger.addHandler(logging.StreamHandler())
self._logger.debug("Creating a MousePoller")
self._display_grp = splash
self._cursor_grp = displayio.Group()
self._cur_palette = displayio.Palette(3)
self._cur_palette.make_transparent(0)
self._cur_palette[1] = 0xFFFFFF
self._cur_palette[2] = 0x0000
self._cur_sprite = displayio.TileGrid(
cursor_bmp, pixel_shader=self._cur_palette
)
self._cursor_grp.append(self._cur_sprite)
self._display_grp.append(self._cursor_grp)
self._x_offset = cursor_bmp.width // 2
self._y_offset = cursor_bmp.height // 2
self.SCREEN_WIDTH = screen_width
self.SCREEN_HEIGHT = screen_height
# Mouse state
self.last_left_button_state = 0
self.last_right_button_state = 0
self.left_button_pressed = False
self.right_button_pressed = False
self.mouse = None
self.buf = None
self.in_endpoint = None
# Mouse position
self.mouse_x = screen_width // 2
self.mouse_y = screen_height // 2
mouse_found = False
if self.find_mouse():
mouse_found = True
else:
self._logger.debug("WARNING: Mouse not found after multiple attempts.")
self._logger.debug("The application will run, but mouse control may not work.")
def find_mouse(self):
"""Find the mouse device with multiple retry attempts"""
MAX_ATTEMPTS = 5
RETRY_DELAY = 1 # seconds
if not usb_available:
self._logger.debug("USB library not available; cannot find mouse.")
return False
for attempt in range(MAX_ATTEMPTS):
try:
self._logger.debug(f"Mouse detection attempt {attempt+1}/{MAX_ATTEMPTS}")
# Constants for USB control transfers
DIR_OUT = 0
# DIR_IN = 0x80 # Unused variable
REQTYPE_CLASS = 1 << 5
REQREC_INTERFACE = 1 << 0
HID_REQ_SET_PROTOCOL = 0x0B
# Find all USB devices
devices_found = False
for device in usb.core.find(find_all=True):
devices_found = True
self._logger.debug(f"Found device: {device.idVendor:04x}:{device.idProduct:04x}")
try:
# Try to get device info
try:
manufacturer = device.manufacturer
product = device.product
except Exception: # pylint: disable=broad-except
manufacturer = "Unknown"
product = "Unknown"
# Just use whatever device we find
self.mouse = device
# Try to detach kernel driver
try:
has_kernel_driver = hasattr(device, 'is_kernel_driver_active')
if has_kernel_driver and device.is_kernel_driver_active(0):
device.detach_kernel_driver(0)
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error detaching kernel driver: {e}")
# Set configuration
try:
device.set_configuration()
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error setting configuration: {e}")
continue # Try next device
# Just assume endpoint 0x81 (common for mice)
self.in_endpoint = 0x81
self._logger.debug(f"Using mouse: {manufacturer}, {product}")
# Set to report protocol mode
try:
bmRequestType = DIR_OUT | REQTYPE_CLASS | REQREC_INTERFACE
bRequest = HID_REQ_SET_PROTOCOL
wValue = 1 # 1 = report protocol
wIndex = 0 # First interface
buf = bytearray(1)
device.ctrl_transfer(bmRequestType, bRequest, wValue, wIndex, buf)
self._logger.debug("Set to report protocol mode")
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Could not set protocol: {e}")
# Buffer for reading data
self.buf = array.array("B", [0] * 4)
self._logger.debug("Created 4-byte buffer for mouse data")
# Verify mouse works by reading from it
try:
# Try to read some data with a short timeout
data = device.read(self.in_endpoint, self.buf, timeout=100)
self._logger.debug(f"Mouse test read successful: {data} bytes")
return True
except usb.core.USBTimeoutError:
# Timeout is normal if mouse isn't moving
self._logger.debug("Mouse connected but not sending data (normal)")
return True
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Mouse test read failed: {e}")
# Continue to try next device or retry
self.mouse = None
self.in_endpoint = None
continue
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error initializing device: {e}")
continue
if not devices_found:
self._logger.debug("No USB devices found")
# If we get here without returning, no suitable mouse was found
self._logger.debug(f"No working mouse found on attempt {attempt+1}, retrying...")
gc.collect()
time.sleep(RETRY_DELAY)
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error during mouse detection: {e}")
gc.collect()
time.sleep(RETRY_DELAY)
self._logger.debug("Failed to find a working mouse after multiple attempts")
return False
def poll(self):
"""Check for input. Returns contact (a bool), False (no button B),
and it's location ((x,y) or None)"""
if self._process_mouse_input():
self._cursor_grp.x = self.mouse_x - self._x_offset
self._cursor_grp.y = self.mouse_y - self._y_offset
return self.left_button_pressed, self.right_button_pressed, \
(self.mouse_x, self.mouse_y)
else:
return False, False, None
def poke(self, location=None):
"""Force a bitmap refresh."""
self._display_grp.remove(self._cursor_grp)
if location is not None:
self._cursor_grp.x = location[0] - self._x_offset
self._cursor_grp.y = location[1] - self._y_offset
self._display_grp.append(self._cursor_grp)
def set_cursor_bitmap(self, bmp):
"""Update the cursor bitmap.
:param bmp: the new cursor bitmap
"""
self._cursor_grp.remove(self._cur_sprite)
self._cur_sprite = displayio.TileGrid(bmp, pixel_shader=self._cur_palette)
self._cursor_grp.append(self._cur_sprite)
self.poke()
def _process_mouse_input(self):
"""Process mouse input - simplified version without wheel support"""
try:
# Attempt to read data from the mouse (10ms timeout)
count = self.mouse.read(self.in_endpoint, self.buf, timeout=10)
except usb.core.USBTimeoutError:
# Timeout is normal if mouse isn't moving
return False
except usb.core.USBError as e:
# Handle timeouts silently
if e.errno == 110: # Operation timed out
return False
# Handle disconnections
if e.errno == 19: # No such device
self._logger.debug("Mouse disconnected")
self.mouse = None
self.in_endpoint = None
gc.collect()
return False
except Exception as e: # pylint: disable=broad-except
self._logger.debug(f"Error reading mouse: {type(e).__name__}")
return False
if count >= 3: # We need at least buttons, X and Y
# Extract mouse button states
buttons = self.buf[0]
x = self.buf[1]
y = self.buf[2]
# Convert to signed values if needed
if x > 127:
x = x - 256
if y > 127:
y = y - 256
# Extract button states
current_left_button_state = buttons & 0x01
current_right_button_state = (buttons & 0x02) >> 1
# Detect button presses
if current_left_button_state == 1 and self.last_left_button_state == 0:
self.left_button_pressed = True
elif current_left_button_state == 0 and self.last_left_button_state == 1:
self.left_button_pressed = False
if current_right_button_state == 1 and self.last_right_button_state == 0:
self.right_button_pressed = True
elif current_right_button_state == 0 and self.last_right_button_state == 1:
self.right_button_pressed = False
# Update button states
self.last_left_button_state = current_left_button_state
self.last_right_button_state = current_right_button_state
# Update position
self.mouse_x += x
self.mouse_y += y
# Ensure position stays within bounds
self.mouse_x = max(0, min(self.SCREEN_WIDTH - 1, self.mouse_x))
self.mouse_y = max(0, min(self.SCREEN_HEIGHT - 1, self.mouse_y))
return True
return False
################################################################################
class Paint(object):
def __init__(self, display=None):
if display is None:
if hasattr(board, "DISPLAY"):
display = board.DISPLAY
else:
if request_display_config is not None:
request_display_config(320, 240)
display = supervisor.runtime.display
else:
raise RuntimeError("No display found.")
self._logger = logging.getLogger("Paint")
if not self._logger.hasHandlers():
self._logger.addHandler(logging.StreamHandler())
self._logger.setLevel(logging.DEBUG)
self._display = display
self._w = self._display.width
self._h = self._display.height
self._x = self._w // 2
self._y = self._h // 2
self._splash = displayio.Group()
self._bg_bitmap = displayio.Bitmap(self._w, self._h, 1)
self._bg_palette = displayio.Palette(1)
self._bg_palette[0] = Color.BLACK
self._bg_sprite = displayio.TileGrid(
self._bg_bitmap, pixel_shader=self._bg_palette, x=0, y=0
)
self._splash.append(self._bg_sprite)
self._palette_bitmap = displayio.Bitmap(self._w, self._h, 8)
self._palette_palette = displayio.Palette(len(Color.colors))
for i, c in enumerate(Color.colors):
self._palette_palette[i] = c
self._palette_sprite = displayio.TileGrid(
self._palette_bitmap, pixel_shader=self._palette_palette, x=0, y=0
)
self._splash.append(self._palette_sprite)
self._fg_bitmap = displayio.Bitmap(self._w, self._h, 9)
self._fg_palette = displayio.Palette(len(Color.colors)+1)
for i, c in enumerate(Color.colors):
self._fg_palette[i] = c
self._fg_palette[8] = Color.PINK # Marker for filled areas
self._fg_sprite = displayio.TileGrid(
self._fg_bitmap, pixel_shader=self._fg_palette, x=0, y=0
)
self._splash.append(self._fg_sprite)
self._number_of_palette_options = len(Color.colors) + 2
self._swatch_height = self._h // self._number_of_palette_options
self._swatch_width = self._w // 10
self._logger.debug("Height: %d", self._h)
self._logger.debug("Swatch height: %d", self._swatch_height)
self._palette = self._make_palette()
self._splash.append(self._palette)
self._display.root_group = self._splash
try:
gc.collect()
self._display.refresh(target_frames_per_second=60)
except AttributeError:
self._display.refresh_soon()
gc.collect()
self._display.wait_for_frame()
self._brush = 0
self._cursor_bitmaps = [self._cursor_bitmap_1(), self._cursor_bitmap_3()]
if hasattr(board, "TOUCH_XL"):
self._poller = TouchscreenPoller(self._splash, self._cursor_bitmaps[0])
elif hasattr(board, "BUTTON_CLOCK"):
self._poller = CursorPoller(self._splash, self._cursor_bitmaps[0])
elif usb_available:
self._poller = MousePoller(self._splash, self._cursor_bitmaps[0], self._w, self._h)
if not self._poller.mouse:
raise RuntimeError("No mouse found. Please connect a USB mouse.")
else:
raise AttributeError("PyPaint requires a touchscreen or cursor.")
self._a_pressed = False
self._last_a_pressed = False
self._b_pressed = False
self._last_b_pressed = False
self._location = None
self._last_location = None
self._pencolor = 7
def _make_palette(self):
self._palette_bitmap = displayio.Bitmap(self._w // 10, self._h, 8)
self._palette_palette = displayio.Palette(len(Color.colors))
for i, c in enumerate(Color.colors):
self._palette_palette[i] = c
for y in range(self._swatch_height):
for x in range(self._swatch_width):
self._palette_bitmap[x, self._swatch_height * i + y] = i
swatch_x_offset = (self._swatch_width - 9) // 2
swatch_y_offset = (self._swatch_height - 9) // 2
swatch_y = self._swatch_height * len(Color.colors) + swatch_y_offset
for i in range(9):
self._palette_bitmap[swatch_x_offset + 4, swatch_y + i] = 1
self._palette_bitmap[swatch_x_offset + i, swatch_y + 4] = 1
self._palette_bitmap[swatch_x_offset + 4, swatch_y + 4] = 0
swatch_y += self._swatch_height
for i in range(9):
self._palette_bitmap[swatch_x_offset + 3, swatch_y + i] = 1
self._palette_bitmap[swatch_x_offset + 4, swatch_y + i] = 1
self._palette_bitmap[swatch_x_offset + 5, swatch_y + i] = 1
self._palette_bitmap[swatch_x_offset + i, swatch_y + 3] = 1
self._palette_bitmap[swatch_x_offset + i, swatch_y + 4] = 1
self._palette_bitmap[swatch_x_offset + i, swatch_y + 5] = 1
for i in range(swatch_x_offset + 3, swatch_x_offset + 6):
for j in range(swatch_y + 3, swatch_y + 6):
self._palette_bitmap[i, j] = 0
for i in range(self._h):
self._palette_bitmap[self._swatch_width - 1, i] = 7
return displayio.TileGrid(
self._palette_bitmap, pixel_shader=self._palette_palette, x=0, y=0
)
def _cursor_bitmap_1(self):
bmp = displayio.Bitmap(9, 9, 3)
for i in range(9):
bmp[4, i] = 1
bmp[i, 4] = 1
bmp[4, 4] = 0
return bmp
def _cursor_bitmap_3(self):
bmp = displayio.Bitmap(9, 9, 3)
for i in range(9):
bmp[3, i] = 1
bmp[4, i] = 1
bmp[5, i] = 1
bmp[i, 3] = 1
bmp[i, 4] = 1
bmp[i, 5] = 1
for i in range(3, 6):
for j in range(3, 6):
bmp[i, j] = 0
return bmp
def _plot(self, x, y, c):
if self._brush == 0:
r = [0]
else:
r = [-1, 0, 1]
for i in r:
for j in r:
try:
self._fg_bitmap[int(x + i), int(y + j)] = c
except IndexError:
pass
def _draw_line(self, start, end):
"""Draw a line from the previous position to the current one.
:param start: a tuple of (x, y) coordinatess to fram from
:param end: a tuple of (x, y) coordinates to draw to
"""
x0 = start[0]
y0 = start[1]
x1 = end[0]
y1 = end[1]
self._logger.debug("* GoTo from (%d, %d) to (%d, %d)", x0, y0, x1, y1)
steep = abs(y1 - y0) > abs(x1 - x0)
rev = False
dx = x1 - x0
if steep:
x0, y0 = y0, x0
x1, y1 = y1, x1
dx = x1 - x0
if x0 > x1:
rev = True
dx = x0 - x1
dy = abs(y1 - y0)
err = dx / 2
ystep = -1
if y0 < y1:
ystep = 1
while (not rev and x0 <= x1) or (rev and x1 <= x0):
if steep:
try:
self._plot(int(y0), int(x0), self._pencolor)
except IndexError:
pass
self._x = y0
self._y = x0
self._poller.poke((int(y0), int(x0)))
time.sleep(0.003)
else:
try:
self._plot(int(x0), int(y0), self._pencolor)
except IndexError:
pass
self._x = x0
self._y = y0
self._poller.poke((int(x0), int(y0)))
time.sleep(0.003)
err -= dy
if err < 0:
y0 += ystep
err += dx
if rev:
x0 -= 1
else:
x0 += 1
def _fill(self, x, y, c):
"""Fill an area with the current color.
:param x: x coordinate to start filling from
:param y: y coordinate to start filling from
:param c: color to fill with
"""
MARKER = 8 # Marker for filled areas
self._logger.debug("Filling at (%d, %d) with color %d", x, y, c)
if self._fg_bitmap[x, y] != c:
blank_color = self._fg_bitmap[x, y]
self._fg_bitmap[x, y] = MARKER
done = False
min_row = 0
max_row = self._h - 1
min_col = (self._w // 10) + 1
max_col = self._w - 1
while not done:
newmin_row = self._h - 1
newmax_row = 0
newmin_col = self._w - 1
newmax_col = (self._w // 10) + 1
done = True
#self._logger.debug("Rows: %d to %d Cols: %d to %d" , min_row, max_row, min_col, max_col)
for i in range(min_row,max_row):
for j in range(min_col,max_col):
if self._fg_bitmap[j, i] == MARKER:
newmarker = False
if j > self._w // 10 and self._fg_bitmap[j - 1, i] == blank_color:
self._fg_bitmap[j - 1, i] = MARKER
newmarker = True
if j < self._w - 1 and self._fg_bitmap[j + 1, i] == blank_color:
self._fg_bitmap[j + 1, i] = MARKER
newmarker = True
if i > 0 and self._fg_bitmap[j, i - 1] == blank_color:
self._fg_bitmap[j, i - 1] = MARKER
newmarker = True
if i < self._h - 1 and self._fg_bitmap[j, i + 1] == blank_color:
self._fg_bitmap[j, i + 1] = MARKER
newmarker = True
if newmarker:
done = False
if i < newmin_row:
newmin_row = i
if i > newmax_row:
newmax_row = i
if j < newmin_col:
newmin_col = j
if j > newmax_col:
newmax_col = j
max_row = min(newmax_row + 2, self._h - 1)
min_row = max(newmin_row - 2, 0)
max_col = min(newmax_col + 2, self._w - 1)
min_col = max(newmin_col - 2, (self._w // 10) + 1)
self._poller.poke()
for i in range(self._h):
for j in range(self._w):
if self._fg_bitmap[j, i] == MARKER:
self._fg_bitmap[j, i] = c
self._poller.poke()
def _handle_palette_selection(self, location):
selected = location[1] // self._swatch_height
if selected >= self._number_of_palette_options:
return
self._logger.debug("Palette selection: %d", selected)
if selected < len(Color.colors):
self._pencolor = selected
else:
self._brush = selected - len(Color.colors)
self._poller.set_cursor_bitmap(self._cursor_bitmaps[self._brush])
def _handle_motion(self, start, end):
self._logger.debug(
"Moved: (%d, %d) -> (%d, %d)", start[0], start[1], end[0], end[1]
)
self._draw_line(start, end)
def _handle_a_press(self, location):
self._logger.debug("A Pressed!")
if location[0] < self._w // 10: # in color picker
self._handle_palette_selection(location)
else:
self._plot(location[0], location[1], self._pencolor)
self._poller.poke()
def _handle_a_release(self, location):
self._logger.debug("A Released!")
def _handle_b_release(self, location):
self._logger.debug("B Released!")
if location[0] >= self._w // 10: # not in color picker
self._fill(location[0], location[1], self._pencolor)
self._poller.poke()
@property
def _was_a_just_pressed(self):
return self._a_pressed and not self._last_a_pressed
@property
def _was_a_just_released(self):
return not self._a_pressed and self._last_a_pressed
@property
def _was_b_just_released(self):
return not self._b_pressed and self._last_b_pressed
@property
def _did_move(self):
if self._location is not None and self._last_location is not None:
x_changed = self._location[0] != self._last_location[0]
y_changed = self._location[1] != self._last_location[1]
return x_changed or y_changed
else:
return False
def _update(self):
self._last_a_pressed = self._a_pressed
self._last_b_pressed = self._b_pressed
if self._location is not None:
self._last_location = self._location
self._a_pressed, self._b_pressed, self._location = self._poller.poll()
def run(self):
"""Run the painting program."""
while True:
self._update()
if self._was_a_just_pressed:
self._handle_a_press(self._location)
elif self._was_a_just_released:
self._handle_a_release(self._location)
if self._was_b_just_released:
self._handle_b_release(self._last_location)
if self._did_move and self._a_pressed:
self._handle_motion(self._last_location, self._location)
time.sleep(0.1)
painter = Paint()
painter.run()

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB