You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Add board Fri3d 2026 (unfinished and untested)
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
# Hardware initialization for Fri3d Camp 2024 Badge
|
||||
from machine import Pin, SPI, SDCard
|
||||
import st7789
|
||||
import lcd_bus
|
||||
import machine
|
||||
import cst816s
|
||||
import i2c
|
||||
import math
|
||||
|
||||
import micropython
|
||||
import gc
|
||||
|
||||
import lvgl as lv
|
||||
import task_handler
|
||||
|
||||
import mpos.ui
|
||||
import mpos.ui.focus_direction
|
||||
|
||||
|
||||
# Pin configuration
|
||||
SPI_BUS = 2
|
||||
SPI_FREQ = 40000000
|
||||
#SPI_FREQ = 20000000 # also works but I guess higher is better
|
||||
LCD_SCLK = 7
|
||||
LCD_MOSI = 6
|
||||
LCD_MISO = 8
|
||||
LCD_DC = 4
|
||||
LCD_CS = 5
|
||||
#LCD_BL = 1 # backlight can't be controlled on this hardware
|
||||
LCD_RST = 48
|
||||
|
||||
TFT_HOR_RES=320
|
||||
TFT_VER_RES=240
|
||||
|
||||
spi_bus = machine.SPI.Bus(
|
||||
host=SPI_BUS,
|
||||
mosi=LCD_MOSI,
|
||||
miso=LCD_MISO,
|
||||
sck=LCD_SCLK
|
||||
)
|
||||
display_bus = lcd_bus.SPIBus(
|
||||
spi_bus=spi_bus,
|
||||
freq=SPI_FREQ,
|
||||
dc=LCD_DC,
|
||||
cs=LCD_CS
|
||||
)
|
||||
|
||||
# lv.color_format_get_size(lv.COLOR_FORMAT.RGB565) = 2 bytes per pixel * 320 * 240 px = 153600 bytes
|
||||
# The default was /10 so 15360 bytes.
|
||||
# /2 = 76800 shows something on display and then hangs the board
|
||||
# /2 = 38400 works and pretty high framerate but camera gets ESP_FAIL
|
||||
# /2 = 19200 works, including camera at 9FPS
|
||||
# 28800 is between the two and still works with camera!
|
||||
# 30720 is /5 and is already too much
|
||||
#_BUFFER_SIZE = const(28800)
|
||||
buffersize = const(28800)
|
||||
fb1 = display_bus.allocate_framebuffer(buffersize, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)
|
||||
fb2 = display_bus.allocate_framebuffer(buffersize, lcd_bus.MEMORY_INTERNAL | lcd_bus.MEMORY_DMA)
|
||||
|
||||
STATE_HIGH = 1
|
||||
STATE_LOW = 0
|
||||
|
||||
# see ./lvgl_micropython/api_drivers/py_api_drivers/frozen/display/display_driver_framework.py
|
||||
mpos.ui.main_display = st7789.ST7789(
|
||||
data_bus=display_bus,
|
||||
frame_buffer1=fb1,
|
||||
frame_buffer2=fb2,
|
||||
display_width=TFT_VER_RES,
|
||||
display_height=TFT_HOR_RES,
|
||||
color_space=lv.COLOR_FORMAT.RGB565,
|
||||
color_byte_order=st7789.BYTE_ORDER_BGR,
|
||||
rgb565_byte_swap=True,
|
||||
reset_pin=LCD_RST, # doesn't seem needed
|
||||
reset_state=STATE_LOW # doesn't seem needed
|
||||
)
|
||||
|
||||
mpos.ui.main_display.init()
|
||||
mpos.ui.main_display.set_power(True)
|
||||
mpos.ui.main_display.set_backlight(100)
|
||||
mpos.ui.main_display.set_color_inversion(False)
|
||||
|
||||
# Touch handling:
|
||||
# touch pad interrupt TP Int is on ESP.IO13
|
||||
i2c_bus = i2c.I2C.Bus(host=I2C_BUS, scl=TP_SCL, sda=TP_SDA, freq=I2C_FREQ, use_locks=False)
|
||||
touch_dev = i2c.I2C.Device(bus=i2c_bus, dev_id=TP_ADDR, reg_bits=TP_REGBITS)
|
||||
indev=cst816s.CST816S(touch_dev,startup_rotation=lv.DISPLAY_ROTATION._180) # button in top left, good
|
||||
|
||||
lv.init()
|
||||
mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._270) # must be done after initializing display and creating the touch drivers, to ensure proper handling
|
||||
mpos.ui.main_display.set_params(0x36, bytearray([0x28]))
|
||||
|
||||
# Button and joystick handling code:
|
||||
from machine import ADC, Pin
|
||||
import time
|
||||
|
||||
btn_x = Pin(38, Pin.IN, Pin.PULL_UP) # X
|
||||
btn_y = Pin(41, Pin.IN, Pin.PULL_UP) # Y
|
||||
btn_a = Pin(39, Pin.IN, Pin.PULL_UP) # A
|
||||
btn_b = Pin(40, Pin.IN, Pin.PULL_UP) # B
|
||||
btn_start = Pin(0, Pin.IN, Pin.PULL_UP) # START
|
||||
btn_menu = Pin(45, Pin.IN, Pin.PULL_UP) # START
|
||||
|
||||
ADC_KEY_MAP = [
|
||||
{'key': 'UP', 'unit': 1, 'channel': 2, 'min': 3072, 'max': 4096},
|
||||
{'key': 'DOWN', 'unit': 1, 'channel': 2, 'min': 0, 'max': 1024},
|
||||
{'key': 'RIGHT', 'unit': 1, 'channel': 0, 'min': 3072, 'max': 4096},
|
||||
{'key': 'LEFT', 'unit': 1, 'channel': 0, 'min': 0, 'max': 1024},
|
||||
]
|
||||
|
||||
# Initialize ADC for the two channels
|
||||
adc_up_down = ADC(Pin(3)) # ADC1_CHANNEL_2 (GPIO 33)
|
||||
adc_up_down.atten(ADC.ATTN_11DB) # 0-3.3V range
|
||||
adc_left_right = ADC(Pin(1)) # ADC1_CHANNEL_0 (GPIO 36)
|
||||
adc_left_right.atten(ADC.ATTN_11DB) # 0-3.3V range
|
||||
|
||||
def read_joystick():
|
||||
# Read ADC values
|
||||
val_up_down = adc_up_down.read()
|
||||
val_left_right = adc_left_right.read()
|
||||
|
||||
# Check each key's range
|
||||
for mapping in ADC_KEY_MAP:
|
||||
adc_val = val_up_down if mapping['channel'] == 2 else val_left_right
|
||||
if mapping['min'] <= adc_val <= mapping['max']:
|
||||
return mapping['key']
|
||||
return None # No key triggered
|
||||
|
||||
# Rotate: UP = 0°, RIGHT = 90°, DOWN = 180°, LEFT = 270°
|
||||
def read_joystick_angle(threshold=0.1):
|
||||
# Read ADC values
|
||||
val_up_down = adc_up_down.read()
|
||||
val_left_right = adc_left_right.read()
|
||||
|
||||
#if time.time() < 60:
|
||||
# print(f"val_up_down: {val_up_down}")
|
||||
# print(f"val_left_right: {val_left_right}")
|
||||
|
||||
# Normalize to [-1, 1]
|
||||
x = (val_left_right - 2048) / 2048 # Positive x = RIGHT
|
||||
y = (val_up_down - 2048) / 2048 # Positive y = UP
|
||||
#if time.time() < 60:
|
||||
# print(f"x,y = {x},{y}")
|
||||
|
||||
# Check if joystick is near center
|
||||
magnitude = math.sqrt(x*x + y*y)
|
||||
#if time.time() < 60:
|
||||
# print(f"magnitude: {magnitude}")
|
||||
if magnitude < threshold:
|
||||
return None # Neutral position
|
||||
|
||||
# Calculate angle in degrees with UP = 0°, clockwise
|
||||
angle_rad = math.atan2(x, y)
|
||||
angle_deg = math.degrees(angle_rad)
|
||||
angle_deg = (angle_deg + 360) % 360 # Normalize to [0, 360)
|
||||
return angle_deg
|
||||
|
||||
# Key repeat configuration
|
||||
# This whole debounce logic is only necessary because LVGL 9.2.2 seems to have an issue where
|
||||
# the lv_keyboard widget doesn't handle PRESSING (long presses) properly, it loses focus.
|
||||
REPEAT_INITIAL_DELAY_MS = 300 # Delay before first repeat
|
||||
REPEAT_RATE_MS = 100 # Interval between repeats
|
||||
last_key = None
|
||||
last_state = lv.INDEV_STATE.RELEASED
|
||||
key_press_start = 0 # Time when key was first pressed
|
||||
last_repeat_time = 0 # Time of last repeat event
|
||||
|
||||
# Read callback
|
||||
# Warning: This gets called several times per second, and if it outputs continuous debugging on the serial line,
|
||||
# that will break tools like mpremote from working properly to upload new files over the serial line, thus needing a reflash.
|
||||
def keypad_read_cb(indev, data):
|
||||
global last_key, last_state, key_press_start, last_repeat_time
|
||||
data.continue_reading = False
|
||||
since_last_repeat = 0
|
||||
|
||||
# Check buttons and joystick
|
||||
current_key = None
|
||||
current_time = time.ticks_ms()
|
||||
|
||||
# Check buttons
|
||||
if btn_x.value() == 0:
|
||||
current_key = lv.KEY.ESC
|
||||
elif btn_y.value() == 0:
|
||||
current_key = ord("Y")
|
||||
elif btn_a.value() == 0:
|
||||
current_key = lv.KEY.ENTER
|
||||
elif btn_b.value() == 0:
|
||||
current_key = ord("B")
|
||||
elif btn_menu.value() == 0:
|
||||
current_key = lv.KEY.HOME
|
||||
elif btn_start.value() == 0:
|
||||
current_key = lv.KEY.END
|
||||
else:
|
||||
# Check joystick
|
||||
angle = read_joystick_angle(0.30) # 0.25-0.27 is right on the edge so 0.30 should be good
|
||||
if angle:
|
||||
if angle > 45 and angle < 135:
|
||||
current_key = lv.KEY.RIGHT
|
||||
elif angle > 135 and angle < 225:
|
||||
current_key = lv.KEY.DOWN
|
||||
elif angle > 225 and angle < 315:
|
||||
current_key = lv.KEY.LEFT
|
||||
elif angle < 45 or angle > 315:
|
||||
current_key = lv.KEY.UP
|
||||
else:
|
||||
print(f"WARNING: unhandled joystick angle {angle}") # maybe we could also handle diagonals?
|
||||
|
||||
# Key repeat logic
|
||||
if current_key:
|
||||
if current_key != last_key:
|
||||
# New key press
|
||||
data.key = current_key
|
||||
data.state = lv.INDEV_STATE.PRESSED
|
||||
last_key = current_key
|
||||
last_state = lv.INDEV_STATE.PRESSED
|
||||
key_press_start = current_time
|
||||
last_repeat_time = current_time
|
||||
else: # same key
|
||||
# Key held: Check for repeat
|
||||
elapsed = time.ticks_diff(current_time, key_press_start)
|
||||
since_last_repeat = time.ticks_diff(current_time, last_repeat_time)
|
||||
if elapsed >= REPEAT_INITIAL_DELAY_MS and since_last_repeat >= REPEAT_RATE_MS:
|
||||
# Send a new PRESSED/RELEASED pair for repeat
|
||||
data.key = current_key
|
||||
data.state = lv.INDEV_STATE.PRESSED if last_state == lv.INDEV_STATE.RELEASED else lv.INDEV_STATE.RELEASED
|
||||
last_state = data.state
|
||||
last_repeat_time = current_time
|
||||
else:
|
||||
# No repeat yet, send RELEASED to avoid PRESSING
|
||||
data.state = lv.INDEV_STATE.RELEASED
|
||||
last_state = lv.INDEV_STATE.RELEASED
|
||||
else:
|
||||
# No key pressed
|
||||
data.key = last_key if last_key else lv.KEY.ENTER
|
||||
data.state = lv.INDEV_STATE.RELEASED
|
||||
last_key = None
|
||||
last_state = lv.INDEV_STATE.RELEASED
|
||||
key_press_start = 0
|
||||
last_repeat_time = 0
|
||||
|
||||
# Handle ESC for back navigation (only on initial PRESSED)
|
||||
if last_state == lv.INDEV_STATE.PRESSED:
|
||||
if current_key == lv.KEY.ESC and since_last_repeat == 0:
|
||||
mpos.ui.back_screen()
|
||||
elif current_key == lv.KEY.RIGHT:
|
||||
mpos.ui.focus_direction.move_focus_direction(90)
|
||||
elif current_key == lv.KEY.LEFT:
|
||||
mpos.ui.focus_direction.move_focus_direction(270)
|
||||
elif current_key == lv.KEY.UP:
|
||||
mpos.ui.focus_direction.move_focus_direction(0)
|
||||
elif current_key == lv.KEY.DOWN:
|
||||
mpos.ui.focus_direction.move_focus_direction(180)
|
||||
|
||||
group = lv.group_create()
|
||||
group.set_default()
|
||||
|
||||
# Create and set up the input device
|
||||
indev = lv.indev_create()
|
||||
indev.set_type(lv.INDEV_TYPE.KEYPAD)
|
||||
indev.set_read_cb(keypad_read_cb)
|
||||
indev.set_group(group) # is this needed? maybe better to move the default group creation to main.py so it's available everywhere...
|
||||
disp = lv.display_get_default() # NOQA
|
||||
indev.set_display(disp) # different from display
|
||||
indev.enable(True) # NOQA
|
||||
|
||||
# Battery voltage ADC measuring
|
||||
# NOTE: GPIO13 is on ADC2, which requires WiFi to be disabled during reading on ESP32-S3.
|
||||
# battery_voltage.py handles this automatically: disables WiFi, reads ADC, reconnects WiFi.
|
||||
import mpos.battery_voltage
|
||||
"""
|
||||
best fit on battery power:
|
||||
2482 is 4.180
|
||||
2470 is 4.170
|
||||
2457 is 4.147
|
||||
# 2444 is 4.12
|
||||
2433 is 4.109
|
||||
2429 is 4.102
|
||||
2393 is 4.044
|
||||
2369 is 4.000
|
||||
2343 is 3.957
|
||||
2319 is 3.916
|
||||
2269 is 3.831
|
||||
2227 is 3.769
|
||||
"""
|
||||
def adc_to_voltage(adc_value):
|
||||
"""
|
||||
Convert raw ADC value to battery voltage using calibrated linear function.
|
||||
Calibration data shows linear relationship: voltage = -0.0016237 * adc + 8.2035
|
||||
This is ~10x more accurate than simple scaling (error ~0.01V vs ~0.1V).
|
||||
"""
|
||||
return (0.001651* adc_value + 0.08709)
|
||||
|
||||
mpos.battery_voltage.init_adc(13, adc_to_voltage)
|
||||
|
||||
import mpos.sdcard
|
||||
mpos.sdcard.init(spi_bus, cs_pin=14)
|
||||
|
||||
# === AUDIO HARDWARE ===
|
||||
from machine import PWM, Pin
|
||||
from mpos import AudioFlinger
|
||||
|
||||
# Initialize buzzer (GPIO 46)
|
||||
buzzer = PWM(Pin(46), freq=550, duty=0)
|
||||
|
||||
# I2S pin configuration for audio output (DAC) and input (microphone)
|
||||
# Note: I2S is created per-stream, not at boot (only one instance can exist)
|
||||
# The DAC uses BCK (bit clock) on GPIO 2, while the microphone uses SCLK on GPIO 17
|
||||
# See schematics: DAC has BCK=2, WS=47, SD=16; Microphone has SCLK=17, WS=47, DIN=15
|
||||
i2s_pins = {
|
||||
# Output (DAC/speaker) pins
|
||||
'sck': 2, # BCK - Bit Clock for DAC output
|
||||
'ws': 47, # Word Select / LRCLK (shared between DAC and mic)
|
||||
'sd': 16, # Serial Data OUT (speaker/DAC)
|
||||
# Input (microphone) pins
|
||||
'sck_in': 17, # SCLK - Serial Clock for microphone input
|
||||
'sd_in': 15, # DIN - Serial Data IN (microphone)
|
||||
}
|
||||
|
||||
# Initialize AudioFlinger with I2S and buzzer
|
||||
AudioFlinger(i2s_pins=i2s_pins, buzzer_instance=buzzer)
|
||||
|
||||
# === LED HARDWARE ===
|
||||
import mpos.lights as LightsManager
|
||||
|
||||
# Initialize 5 NeoPixel LEDs (GPIO 12)
|
||||
LightsManager.init(neopixel_pin=12, num_leds=5)
|
||||
|
||||
# === SENSOR HARDWARE ===
|
||||
import mpos.sensor_manager as SensorManager
|
||||
|
||||
# Create I2C bus for IMU (different pins from display)
|
||||
from machine import I2C
|
||||
imu_i2c = I2C(0, sda=Pin(9), scl=Pin(18))
|
||||
SensorManager.init(imu_i2c, address=0x6B, mounted_position=SensorManager.FACING_EARTH)
|
||||
|
||||
print("Fri3d hardware: Audio, LEDs, and sensors initialized")
|
||||
|
||||
# === STARTUP "WOW" EFFECT ===
|
||||
import time
|
||||
import _thread
|
||||
|
||||
def startup_wow_effect():
|
||||
"""
|
||||
Epic startup effect with rainbow LED chase and upbeat startup jingle.
|
||||
Runs in background thread to avoid blocking boot.
|
||||
"""
|
||||
try:
|
||||
# Startup jingle: Happy upbeat sequence (ascending scale with flourish)
|
||||
startup_jingle = "Startup:d=8,o=6,b=200:c,d,e,g,4c7,4e,4c7"
|
||||
#startup_jingle = "ShortBeeps:d=32,o=5,b=320:c6,c7"
|
||||
|
||||
# Start the jingle
|
||||
AudioFlinger.play_rtttl(
|
||||
startup_jingle,
|
||||
stream_type=AudioFlinger.STREAM_NOTIFICATION,
|
||||
volume=60
|
||||
)
|
||||
|
||||
# Rainbow colors for the 5 LEDs
|
||||
rainbow = [
|
||||
(255, 0, 0), # Red
|
||||
(255, 128, 0), # Orange
|
||||
(255, 255, 0), # Yellow
|
||||
(0, 255, 0), # Green
|
||||
(0, 0, 255), # Blue
|
||||
]
|
||||
|
||||
# Rainbow sweep effect (3 passes, getting faster)
|
||||
for pass_num in range(3):
|
||||
for i in range(5):
|
||||
# Light up LEDs progressively
|
||||
for j in range(i + 1):
|
||||
LightsManager.set_led(j, *rainbow[j])
|
||||
LightsManager.write()
|
||||
time.sleep_ms(80 - pass_num * 20) # Speed up each pass
|
||||
|
||||
# Flash all LEDs bright white
|
||||
LightsManager.set_all(255, 255, 255)
|
||||
LightsManager.write()
|
||||
time.sleep_ms(150)
|
||||
|
||||
# Rainbow finale
|
||||
for i in range(5):
|
||||
LightsManager.set_led(i, *rainbow[i])
|
||||
LightsManager.write()
|
||||
time.sleep_ms(300)
|
||||
|
||||
# Fade out
|
||||
LightsManager.clear()
|
||||
LightsManager.write()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Startup effect error: {e}")
|
||||
|
||||
_thread.stack_size(mpos.apps.good_stack_size()) # default stack size won't work, crashes!
|
||||
_thread.start_new_thread(startup_wow_effect, ())
|
||||
|
||||
print("fri3d_2024.py finished")
|
||||
@@ -23,6 +23,8 @@ elif sys.platform == "esp32":
|
||||
i2c0 = I2C(0, sda=Pin(9), scl=Pin(18))
|
||||
if {0x6B} <= set(i2c0.scan()): # IMU (plus possibly the Communicator's LANA TNY at 0x38)
|
||||
board = "fri3d_2024"
|
||||
elif {0x6A} <= set(i2c0.scan()): # IMU (plus a few others, to be added later, but this should work)
|
||||
board = "fri3d_2026"
|
||||
else:
|
||||
print("Unable to identify board, defaulting...")
|
||||
board = "fri3d_2024" # default fallback
|
||||
|
||||
Reference in New Issue
Block a user