Update learn guide files for use with Fruit Jam OS

This commit is contained in:
RetiredWizard 2025-08-10 00:05:19 -04:00
parent e804e240a6
commit 5b29480e9f
3 changed files with 392 additions and 10 deletions

View file

@ -16,12 +16,26 @@ 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:
@ -91,9 +105,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, p
return True, False, p
else:
return False, None
return False, False, None
def poke(self, location=None):
"""Force a bitmap refresh."""
@ -143,7 +157,7 @@ class CursorPoller(object):
self._mouse_cursor.x + self._x_offset,
self._mouse_cursor.y + self._y_offset,
)
return a_button, location
return a_button, False, location
def poke(self, x=None, y=None):
"""Force a bitmap refresh."""
@ -161,9 +175,281 @@ 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):
logger = logging.getLogger("Paint")
if not logger.hasHandlers():
logger.addHandler(logging.StreamHandler())
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:
print("WARNING: Mouse not found after multiple attempts.")
print("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:
print("USB library not available; cannot find mouse.")
return False
for attempt in range(MAX_ATTEMPTS):
try:
print(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
print(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
print(f"Error detaching kernel driver: {e}")
# Set configuration
try:
device.set_configuration()
except Exception as e: # pylint: disable=broad-except
print(f"Error setting configuration: {e}")
continue # Try next device
# Just assume endpoint 0x81 (common for mice)
self.in_endpoint = 0x81
print(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)
print("Set to report protocol mode")
except Exception as e: # pylint: disable=broad-except
print(f"Could not set protocol: {e}")
# Buffer for reading data
self.buf = array.array("B", [0] * 4)
print("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)
print(f"Mouse test read successful: {data} bytes")
return True
except usb.core.USBTimeoutError:
# Timeout is normal if mouse isn't moving
print("Mouse connected but not sending data (normal)")
return True
except Exception as e: # pylint: disable=broad-except
print(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
print(f"Error initializing device: {e}")
continue
if not devices_found:
print("No USB devices found")
# If we get here without returning, no suitable mouse was found
print(f"No working mouse found on attempt {attempt+1}, retrying...")
gc.collect()
time.sleep(RETRY_DELAY)
except Exception as e: # pylint: disable=broad-except
print(f"Error during mouse detection: {e}")
gc.collect()
time.sleep(RETRY_DELAY)
print("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
print("Mouse disconnected")
self.mouse = None
self.in_endpoint = None
gc.collect()
return False
except Exception as e: # pylint: disable=broad-except
print(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=board.DISPLAY):
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())
@ -184,7 +470,7 @@ class Paint(object):
)
self._splash.append(self._bg_sprite)
self._palette_bitmap = displayio.Bitmap(self._w, self._h, 5)
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
@ -193,10 +479,11 @@ class Paint(object):
)
self._splash.append(self._palette_sprite)
self._fg_bitmap = displayio.Bitmap(self._w, self._h, 5)
self._fg_palette = displayio.Palette(len(Color.colors))
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
)
@ -226,18 +513,24 @@ 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, 5)
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
@ -364,6 +657,76 @@ 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):
newmarker = False
if self._fg_bitmap[j, i] == MARKER:
if j > self._w // 10 and self._fg_bitmap[j - 1, i] == blank_color:
self._fg_bitmap[j - 1, i] = MARKER
done = False
newmarker = True
if j < self._w - 1 and self._fg_bitmap[j + 1, i] == blank_color:
self._fg_bitmap[j + 1, i] = MARKER
done = False
newmarker = True
if i > 0 and self._fg_bitmap[j, i - 1] == blank_color:
self._fg_bitmap[j, i - 1] = MARKER
done = False
newmarker = True
if i < self._h - 1 and self._fg_bitmap[j, i + 1] == blank_color:
self._fg_bitmap[j, i + 1] = MARKER
done = False
newmarker = True
if newmarker:
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:
@ -392,6 +755,12 @@ 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
@ -400,6 +769,10 @@ 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:
@ -410,8 +783,11 @@ class Paint(object):
return False
def _update(self):
self._last_a_pressed, self._last_location = self._a_pressed, self._location
self._a_pressed, self._location = self._poller.poll()
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."""
@ -421,6 +797,8 @@ 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,4 @@
{
"title": "PyPaint",
"icon": "icon.bmp"
}