adds rotary encoder+SPI based Display: ili9341

This commit is contained in:
Robert Schauklies 2025-08-23 18:55:01 +02:00
parent dd2a5bf637
commit a8f8586756
11 changed files with 2184 additions and 269 deletions

View file

@ -1,299 +1,128 @@
from machine import Pin, I2C, Timer, UART
import uasyncio
import json
# test of printing multiple fonts to the ILI9341 on an M5Stack using H/W SP
# MIT License; Copyright (c) 2017 Jeffrey N. Magee
import ssd1306
from ili934xnew import ILI9341, color565
from machine import Pin, SPI,Timer
import m5stack
import tt14
import glcdfont
import tt14
import tt24
import tt32
from rotary_irq_esp import RotaryIRQ
from dshot import Dshot
from pid import PID
def splash():
# display.fill(0)
# display.fill_rect(0, 0, 32, 32, 1)
# display.fill_rect(2, 2, 28, 28, 0)
# display.vline(9, 8, 22, 1)
# display.vline(16, 2, 22, 1)
# display.vline(23, 8, 22, 1)
# display.fill_rect(26, 24, 2, 4, 1)
# display.text("MicroPython", 40, 0, 1)
# display.text("SSD1306", 40, 12, 1)
# display.text("OLED 128x64", 40, 24, 1)
display.fill(0)
display.text("SPIN", 34, 4, 1)
display.text("COATER", 50, 14, 1)
display.show()
def start_view(state, rotary):
display.text("SPIN", 34, 4, 1)
display.text("COATER", 50, 14, 1)
before = "> " if rotary.value() == 0 else " "
display.text(before + "Edit", 12, 36, 1)
before = "> " if rotary.value() == 1 else " "
display.text(before + "Start", 12, 46, 1)
def draw_edit_menu(state, rotary):
display.text("Deposit speed:", 0, 0, 1)
display.text("{: >{w}} RPM".format(config["deposit_rpm"], w=5), 56, 10, 1)
display.text("Coating speed:", 0, 21, 1)
display.text("{: >{w}} RPM".format(config["coating_rpm"], w=5), 56, 31, 1)
display.text("Coating time:", 0, 42, 1)
display.text("{: >{w}} sec".format(config["coating_time"], w=5), 56, 52, 1)
def edit_deposit_view(state, rotary):
config["deposit_rpm"] = rotary.value() * 100
draw_edit_menu(state, rotary)
display.text(">", 40, 10, 1)
def edit_coating_rpm_view(state, rotary):
config["coating_rpm"] = rotary.value() * 100
draw_edit_menu(state, rotary)
display.text(">", 40, 32, 1)
def edit_coating_time_view(state, rotary):
config["coating_time"] = rotary.value()
draw_edit_menu(state, rotary)
display.text(">", 40, 54, 1)
def draw_rpm(rpm):
display.text("RPM:{: >{w}.0f}".format(rpm, w=5), 30, 27, 1)
def deposit_view(state, rotary):
display.fill_rect(0, 0, 127, 14, 1)
display.text("Deposit", 36, 3, 0)
draw_rpm(state["rpm"])
display.text("Press to", 32, 42, 1)
display.text("continue", 32, 52, 1)
def coating_view(state, rotary):
display.fill_rect(0, 0, 127, 14, 1)
display.text("Coating", 36, 3, 0)
draw_rpm(state["rpm"])
display.text("{: >{w}} sec".format(state["timer"], w=4), 30, 48, 1)
def decode_ESC_telemetry(data, motor_poles=14):
if len(data) > 10:
# use latest telemetry
data = data[-10:]
temperature = int(data[0]) # degrees Celsius
voltage = int((data[1] << 8) | data[2]) * 0.01 # Volt
current = (
int((data[3] << 8) | data[4]) * 0.01
) # Amps, only available if the ESC has a current meter
consumption = int(
(data[5] << 8) | data[6]
) # mAh, only available if the ESC has a current meter
erpm = int((data[7] << 8) | data[8]) * 100
rpm = erpm / (motor_poles / 2)
crc = data[9]
# print(" Temp (C):", temperature)
# print(" Voltage (V):", voltage)
# print(" Current (A):", current)
# print("Consumption (mAh):", consumption)
# print(" Erpm:", erpm)
# print(" RPM:", rpm)
# print(" CRC:", crc)
# print()
return temperature, voltage, current, consumption, erpm, rpm
async def update_display():
global state
global rotary
while True:
display.fill(0)
state["view"](state, rotary)
display.show()
await uasyncio.sleep_ms(33)
async def update_motor():
global state
dshot = Dshot(pin=Pin(18))
rpm_pid = PID(
Kp=config["PID"]["Kp"],
Ki=config["PID"]["Ki"],
Kd=config["PID"]["Kd"],
setpoint=0,
sample_time=None,
output_limits=(0.0, 1.0),
# proportional_on_measurement=True,
)
while True:
rpm_pid.setpoint = state["target_rpm"]
# read ESC telemetry
if uart.any() >= 10:
telemetry = decode_ESC_telemetry(uart.read())
if telemetry is not None:
state["rpm"] = telemetry[5]
throttle = rpm_pid(state["rpm"])
# print(
# "Throttle:",
# throttle,
# "pid components:",
# rpm_pid.components,
# "RPM:",
# state["rpm"],
# )
if state["target_rpm"] == 0 and state["rpm"] < 1000:
throttle = 0
rpm_pid.reset()
dshot.set_throttle(throttle)
await uasyncio.sleep_ms(1)
fonts = [glcdfont,tt14,tt24,tt32]
def debounce_button(p):
p.irq(trigger=Pin.IRQ_FALLING, handler=None) # remove irq
timer0 = Timer(0)
print("Button debounced!")
timer0.init(period=20, mode=Timer.ONE_SHOT, callback=lambda t: on_button_press(p))
def on_button_press(p):
p.irq(trigger=Pin.IRQ_FALLING, handler=debounce_button) # restore irq
print("Button pressed")
if p.value() == 1: # debounce
return
global state
global config
global rotary
if state["view"] == start_view:
if rotary.value() == 0:
state["view"] = edit_deposit_view
rotary.set(
min_val=0,
max_val=1000,
range_mode=RotaryIRQ.RANGE_BOUNDED,
value=int(0.01 * config["deposit_rpm"]),
)
return
if rotary.value() == 1:
state["view"] = deposit_view
state["target_rpm"] = config["deposit_rpm"]
return
if state["view"] == edit_deposit_view:
state["view"] = edit_coating_rpm_view
rotary.set(
min_val=0,
max_val=1000,
range_mode=RotaryIRQ.RANGE_BOUNDED,
value=int(0.01 * config["coating_rpm"]),
)
return
if state["view"] == edit_coating_rpm_view:
state["view"] = edit_coating_time_view
rotary.set(
min_val=0,
max_val=9999,
range_mode=RotaryIRQ.RANGE_BOUNDED,
value=config["coating_time"],
)
return
if state["view"] == edit_coating_time_view:
save_config()
rotary.set(min_val=0, max_val=1, range_mode=RotaryIRQ.RANGE_BOUNDED, value=0)
state["view"] = start_view
return
if state["view"] == deposit_view:
state["view"] = coating_view
start_coating(state)
return
if state["view"] == coating_view:
stop_coating()
return
# return
# global state
# global config
# global rotary
# if state["view"] == start_view:
# if rotary.value() == 0:
# state["view"] = edit_deposit_view
# rotary.set(
# min_val=0,
# max_val=1000,
# range_mode=RotaryIRQ.RANGE_BOUNDED,
# value=int(0.01 * config["deposit_rpm"]),
# )
# return
# if rotary.value() == 1:
# state["view"] = deposit_view
# state["target_rpm"] = config["deposit_rpm"]
# return
# if state["view"] == edit_deposit_view:
# state["view"] = edit_coating_rpm_view
# rotary.set(
# min_val=0,
# max_val=1000,
# range_mode=RotaryIRQ.RANGE_BOUNDED,
# value=int(0.01 * config["coating_rpm"]),
# )
# return
# if state["view"] == edit_coating_rpm_view:
# state["view"] = edit_coating_time_view
# rotary.set(
# min_val=0,
# max_val=9999,
# range_mode=RotaryIRQ.RANGE_BOUNDED,
# value=config["coating_time"],
# )
# return
# if state["view"] == edit_coating_time_view:
# save_config()
# rotary.set(min_val=0, max_val=1, range_mode=RotaryIRQ.RANGE_BOUNDED, value=0)
# state["view"] = start_view
# return
# if state["view"] == deposit_view:
# state["view"] = coating_view
# start_coating(state)
# return
# if state["view"] == coating_view:
# stop_coating()
# return
def start_coating(state):
global timer1
global timer2
state["timer"] = config["coating_time"]
text = 'Now is the time for all good men to come to the aid of the party.'
timer1.init(
period=config["coating_time"] * 1000,
mode=Timer.ONE_SHOT,
callback=lambda t: stop_coating(),
)
def decrement_timer(t):
state["timer"] -= 1
timer2.init(period=1000, mode=Timer.PERIODIC, callback=decrement_timer)
# state["throttle"] = 0.10
state["target_rpm"] = config["coating_rpm"]
power = Pin(m5stack.TFT_LED_PIN, Pin.OUT)
power.value(1)
def stop_coating():
global state
global rotary
global timer1
global timer2
timer1.deinit()
timer2.deinit()
state["target_rpm"] = 0
rotary.set(min_val=0, max_val=1, range_mode=RotaryIRQ.RANGE_BOUNDED, value=0)
state["view"] = start_view
# No need to change the software. It's just a matter of different names.. Use this translation:
# SDO(MISO) <not used>
# LED BL
# SCK CLK/SCK
# SDI(MOSI) DIN/SDA
# DC D/C (A0)
# RESET RST
# CS CS
# GND GND
# VCC VCC
print("BOOOOOOTTTT")
spi = SPI(
2,
baudrate=40000000,
miso=Pin(m5stack.TFT_MISO_PIN),
mosi=Pin(m5stack.TFT_MOSI_PIN),
sck=Pin(m5stack.TFT_CLK_PIN))
def save_config():
global config
with open("config.json", "w") as f:
json.dump(config, f)
# using default address 0x3c
i2c = I2C(1, sda=Pin(21), scl=Pin(22))
display = ssd1306.SSD1306_I2C(128, 64, i2c)
display.rotate(0)
timer1 = Timer(1)
timer2 = Timer(2)
splash()
display = ILI9341(
spi,
cs=Pin(m5stack.TFT_CS_PIN),
dc=Pin(m5stack.TFT_DC_PIN),
rst=Pin(m5stack.TFT_RST_PIN),
w=128,
h=160,
r=3)
rotary = RotaryIRQ(
pin_num_clk=14,
pin_num_clk=25,
pin_num_dt=13,
min_val=0,
max_val=1,
range_mode=RotaryIRQ.RANGE_BOUNDED,
pull_up=True,
)
button = Pin(19, Pin.IN, Pin.PULL_UP)
button = Pin(15, Pin.IN, Pin.PULL_UP)
button.irq(trigger=Pin.IRQ_FALLING, handler=on_button_press)
uart = UART(1, baudrate=115200, rx=5) # to receive ESC telemetry
state = {
"view": start_view,
"rpm": 0,
"target_rpm": 0,
"timer": 0,
}
display.erase()
display.set_pos(0,0)
for ff in fonts:
display.set_font(ff)
display.print(text)
with open("config.json", "r") as f:
config = json.load(f)
event_loop = uasyncio.get_event_loop()
event_loop.create_task(update_display())
event_loop.create_task(update_motor())
event_loop.run_forever()