MicroPython/RP2350ZERO/utils.py

402 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import utime
from machine import Pin, PWM
from neopixel import NeoPixel
class MTS102:
"""MTS102三脚二档钮子开关"""
def __init__(self, pin, pull_down=False):
"""
:param pin: 引脚编号
:param pull_down: 引脚下拉电阻断开为低电平接通为高电平默认为False。若使用下拉电阻因钮子接通后断开可能存留电荷/漏电故需在引脚和GND并联1K10K欧电阻
"""
import utime
from machine import Pin
if not isinstance(pull_down, bool):
raise TypeError("pull_down数据类型应为布尔")
self.pull_down = pull_down
# 尝试初始化引脚
try:
self.pin = Pin(
pin, Pin.IN, Pin.PULL_DOWN if self.pull_down else Pin.PULL_UP
)
except Exception as exception:
raise RuntimeError(f"初始化引脚发生异常,{str(exception)}") from exception
# 初始化上一次检测时间
self._last_time = utime.ticks_ms()
# 初始化上一次检测状态
self._last_state = self._get_state()
def _get_state(self):
"""获取状态0为断开1为接通"""
return self.pin.value() ^ (not self.pull_down)
def _debounce(self):
"""去抖"""
current_time = utime.ticks_ms()
current_state = self._get_state()
# 若当前与上一次检测间隔大于等于防抖时间则更新时间,又若状态不同则更新状态
if utime.ticks_diff(current_time, self._last_time) >= 20:
if current_state != self._last_state:
self._last_time = current_time
self._last_state = current_state
return self._last_state
@property
def switched(self):
"""是否接通"""
return self._debounce() == 1
"""
使用示例
引脚0→LED→220R→GND
引脚1→MST102→GND
import utime
from machine import Pin
from utils import MTS102
led = Pin(0, Pin.OUT)
toggle = MTS102(pin=1)
while True:
if toggle.switched:
led.on()
else:
led.off()
utime.sleep_ms(200)
"""
class WS2812:
"""WS2812可编程 RGB LED"""
def __init__(self, pin=16, led_beads=1):
"""
:param pin: 引脚编号RP2350ZERO板载WS2812使用引脚16
:param led_beads: LED灯珠数
"""
import utime
from machine import Pin
from neopixel import NeoPixel
if not isinstance(led_beads, int):
raise TypeError("led_beads数据类型应为整数")
if not led_beads >= 1:
raise ValueError("led_beads应大于等于1")
self.led_beads = led_beads
# 尝试初始化LED
try:
self.led = NeoPixel(Pin(pin, Pin.OUT), self.led_beads)
except Exception as exception:
raise RuntimeError(f"初始化LED发生异常{str(exception)}") from exception
def set_colors(self, colors):
"""
设置灯珠颜色
:param colors: 包含每颗灯珠RGB的颜色列表
"""
if not isinstance(colors, list):
raise TypeError("colors数据类型应为列表")
if len(colors) != self.led_beads:
raise ValueError("colors列表长度应与LED灯珠数一致")
for bead_idx, bead_color in enumerate(colors):
if not isinstance(bead_color, tuple):
raise TypeError("灯珠颜色数据类型应为元组")
if len(bead_color) != 3:
raise ValueError("灯珠颜色长度应为3")
for channel_idx, channel_value in enumerate(bead_color):
if not isinstance(channel_value, int):
raise TypeError("通道数据数据类型应为整数")
if not 0 <= channel_value <= 255:
raise ValueError("通道数据应在0255范围内")
for bead_idx, bead_color in enumerate(colors):
# 将RGB转为GRB并设置灯珠颜色
self.led[bead_idx] = (bead_color[1], bead_color[0], bead_color[2])
# LED刷新
self.led.write()
def iridesce(self, hue_offset=20):
"""通过HSV颜色空间生成渐变炫彩效果
:param hue_offset: 色相偏移角度
"""
if not isinstance(hue_offset, int):
raise TypeError("hue_offset数据类型应为整数")
if not 0 <= hue_offset <= 60:
raise ValueError("hue_offset应在060范围内")
# 初始化HSV颜色空间参数其中hue为色相saturation为饱和度value为亮度
hue, saturation, value = 0, 255, 128
while True:
# 初始化颜色列表
colors = []
for bead_idx in range(self.led_beads):
# 色相
hue = (hue + bead_idx * hue_offset) % 360
# 色域
region = hue // 60
# 色域内相对位置
remainder = (hue % 60) * 255 // 60
# RGB颜色通道中的最小值分量
primary = (value * (255 - saturation)) // 255
# RGB颜色通道中的过渡分量
tertiary = (value * remainder) // 255
# 根据色域将HSV转为RGB
if region == 0:
color = (value, tertiary, primary) # 由红色渐变为黄色
elif region == 1:
color = (value - tertiary, value, primary) # 由黄色渐变为绿色
elif region == 2:
color = (primary, value, tertiary) # 由绿色渐变为青色
elif region == 3:
color = (primary, value - tertiary, value) # 由青色渐变为蓝色
elif region == 4:
color = (tertiary, primary, value) # 由蓝色渐变为洋红色
else:
color = (value, primary, value - tertiary) # 由洋红色渐变为红色
colors.append(color)
self.set_colors(colors)
# 根据色域动态调整色相步长
hue = hue + [1, 3, 3, 3, 3, 1][region]
utime.sleep_ms(20)
"""
使用示例
RP2350ZERP 使用板载WS2812其占用引脚16
import utime
from utils import WS2812
rgb = WS2812()
while True:
rgb.iridesce()
utime.sleep_ms(20)
"""
class Servo:
"""舵机基类"""
class EasingObj:
"""缓动曲线对象,用于封装缓动曲线函数、转动单位角度对应的步数和平均速率"""
def __init__(self, function, steps_per_degree):
"""
:param function: 缓动曲线函数X轴为进程、Y轴为速率
:param steps_per_degree: 转动单位角度对应的步数,单位为步
"""
self.function = function
self.steps_per_degree = steps_per_degree
# 平均速率
self.average_speed = self._calculate_average_speed()
def _calculate_average_speed(self, intervals=100):
"""
基于复核中点矩形法计算平均速率
:param intervals: 积分间隔数
"""
return sum(self.function((2 * i + 1) / (2 * intervals))[1] for i in range(intervals)) / intervals
# 缓动曲线
EASINGS = {
"uniform": EasingObj(lambda x: (x, 1), 1.0), # 匀速
"ease_in": EasingObj(lambda x: (x ** 2, 2 * x), 1.5), # 慢→快
"ease_out": EasingObj(lambda x: (1 - (1 - x) ** 2, 2 * (1 - x)), 1.5), # 快→慢
"ease_in_out": EasingObj(lambda x: (3 * x ** 2 - 2 * x ** 3, 6 * x * (1 - x)), 1.65), # 慢→快→慢
}
def __init__(self, pin, frequency=50, min_angle=0, max_angle=180, min_pulsewidth=500, max_pulsewidth=2500, dead_zone=8, duration_per_degree=0.13):
"""
:param pin: 引脚编号
:param frequency: 频率,单位为赫兹
:param min_angle: 最小转动角度,单位为度
:param max_angle: 最大转动角度,单位为度
:param min_pulsewidth: 最小转动角度对应的脉宽,单位为微秒
:param max_pulsewidth: 最大转动角度对应的脉宽,单位为微秒
:param dead_zone: 死区,单位为微秒
:param duration_per_degree: 转动单位角度对应的时长,单位为秒/六十度
"""
import utime
from machine import Pin, PWM
# 尝试初始化PWM
try:
self.pwm = PWM(Pin(pin, Pin.OUT))
# 设置频率
self.pwm.freq(frequency)
self.pwm.duty_ns(0) # 关闭PWM信号输出
except Exception as exception:
raise RuntimeError(f"初始化PWM发生异常{str(exception)}") from exception
self.min_angle, self.max_angle = min_angle, max_angle
# 脉宽单位由微秒转为纳秒(默认脉宽单位为纳秒)
self.min_pulsewidth, self.max_pulsewidth = min_pulsewidth * 1000, max_pulsewidth * 1000
# 转动单位角度对应的脉宽,单位为纳秒/度
self.pulsewidth_per_degree = (self.max_pulsewidth - self.min_pulsewidth) / (self.max_angle - self.min_angle)
# 死区单位由微秒转为纳秒
self.dead_zone = dead_zone * 1000
# 转动单位角度对应的时长单位由秒/六十度转为毫秒/度(默认转动、延迟单位为毫秒)
self.duration_per_degree = duration_per_degree * 1000 / 60
# 初始化标记为停止转动
self.rotating = False
# 初始化目标转动角度
self.target_angle = self.min_angle
# 初始化目标转动角度对应的脉宽
self.target_pulsewidth = self._angle_to_pulsewidth(self.target_angle)
# 初始化当前转动角度
self.current_angle = self.min_angle
# 初始化当前转动角度对应的脉宽
self.current_pulsewidth = self._angle_to_pulsewidth(self.current_angle)
# 初始化缓动曲线
self.easing = None
# 初始化转动步骤生成器
self.rotation_steps = None
def set_angle(self, target_angle, easing="ease_in_out"):
"""
设置旋转角度
:param target_angle: 目标转动角度,单位为度
:param easing: 缓动函数名称
"""
if not isinstance(target_angle, (int, float)):
raise TypeError("target_angle数据类型应为整数或浮点")
# 限制目标转动角度在最小转动角度至最大转动角度范围之内
target_angle = max(self.min_angle, min(self.max_angle, target_angle))
if not isinstance(easing, str):
raise TypeError("easing数据类型应为字符")
# 限制缓动曲线在指定范围之内
easing = self.EASINGS.get(easing.lower(), self.EASINGS["ease_in_out"])
# 若为旋转中则返回设置旋转角度失败
if self.rotating:
return False
# 目标转动角度对应的脉宽
target_pulsewidth = self._angle_to_pulsewidth(target_angle)
# 若目标转动角度对应的脉宽和当前转动角度对应的脉宽之差小于死区则返回设置旋转角度成功
if abs(target_pulsewidth - self.current_pulsewidth) < self.dead_zone:
# 更新目标转动角度
self.target_angle = target_angle
# 更新目标转动角度对应的脉宽
self.target_pulsewidth = target_pulsewidth
return True
# 标记为旋转中
self.rotating = True
# 更新目标转动角度
self.target_angle = target_angle
# 更新目标转动角度对应的脉宽
self.target_pulsewidth = target_pulsewidth
# 更新缓动曲线
self.easing = easing
# 初始化转动步骤生成器
self.rotation_steps = self._generate_rotation_steps()
return True
def _generate_rotation_steps(self):
"""生成转动步骤"""
# 起始转动角度
start_angle = self.current_angle
# 目标转动角度和起始转动角度之差
angle_difference = self.target_angle - start_angle
# 转动步数,单位为步
steps = max(int(round(abs(angle_difference) * self.easing.steps_per_degree)), max(10, int(round(abs(angle_difference) / 2))))
# 每步延迟,单位为毫秒
step_delay = max(5, min(50, int(round((abs(angle_difference) * self.duration_per_degree / self.easing.average_speed) / steps))))
for step in range(steps + 1):
# 当前转动角度
self.current_angle = start_angle + angle_difference * self.easing.function(step / steps)[0]
# 当前转动角度对应的脉宽
self.current_pulsewidth = self._angle_to_pulsewidth(self.current_angle)
# 输出PWM信号更新当前转动角度
self.pwm.duty_ns(self.current_pulsewidth)
if step < steps: # 最后一步无需等待
yield
utime.sleep_ms(step_delay)
# 兜底:确保最后一步达到目标转动角度对应的脉宽
self.current_angle = self.target_angle
self.current_pulsewidth = self.target_pulsewidth
self.pwm.duty_ns(self.current_pulsewidth)
self._stop()
yield
def _angle_to_pulsewidth(self, angle):
"""将转动角度转为脉宽"""
pulsewidth = self.min_pulsewidth + (angle - self.min_angle) * self.pulsewidth_per_degree
return max(self.min_pulsewidth, min(self.max_pulsewidth, int(round(pulsewidth))))
def rotate(self):
"""转动:需在主循环调用,执行转动步骤"""
if self.rotating and self.rotation_steps:
try:
next(self.rotation_steps)
# 生成器耗尽,正常停止
except StopIteration:
self._stop()
except Exception as exception:
self._stop()
raise RuntimeError(f"转动时发生异常,{str(exception)}") from exception
def _stop(self):
"""停止转动"""
# 标记为停止转动
self.rotating = False
self.rotation_steps = None
self.pwm.duty_ns(0)
class SG90(Servo):
"""SG90适用于SG90和MG90S等舵机"""
def __init__(self, pin):
super().__init__(
pin=pin,
)