Add configuration for semaphore signals and implement railway signal control
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
import machine
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
try:
|
||||
import micropython
|
||||
except ImportError: # CPython / non-MicroPython
|
||||
micropython = None # type: ignore
|
||||
|
||||
LAMP_COLORS = {
|
||||
# Имена цветов -> RGBA (для RGBW ленты)
|
||||
"OFF": (0, 0, 0, 0),
|
||||
"RED": (100, 0, 0, 0),
|
||||
"YELLOW": (100, 45, 0, 0),
|
||||
"GREEN": (0, 100, 0, 0),
|
||||
"BLUE": (0, 0, 100, 0),
|
||||
"WHITE": (0, 0, 0, 100),
|
||||
"MOON_WHITE": (0, 0, 0, 100),
|
||||
}
|
||||
|
||||
|
||||
class Lamp:
|
||||
"""Одна лампа светофора (один пиксель).
|
||||
|
||||
3 режима:
|
||||
- off: выключено
|
||||
- on: постоянно горит
|
||||
- blink: мигает (переключение ON/OFF с периодом period_ms)
|
||||
"""
|
||||
|
||||
MODE_OFF = "off"
|
||||
MODE_ON = "on"
|
||||
MODE_BLINK = "blink"
|
||||
|
||||
def __init__(self, signal, lamp_id: int, pixel_index: int, rgba):
|
||||
self._signal = signal
|
||||
self.id = lamp_id
|
||||
self.pixel = pixel_index
|
||||
self.rgba = rgba
|
||||
|
||||
self.mode = Lamp.MODE_OFF
|
||||
self._blink_period_ms = 500
|
||||
self._blink_on = False
|
||||
self._next_toggle_ms = 0
|
||||
|
||||
def off(self):
|
||||
self._signal._set_lamp_mode(self, Lamp.MODE_OFF)
|
||||
|
||||
def on(self):
|
||||
self._signal._set_lamp_mode(self, Lamp.MODE_ON)
|
||||
|
||||
def blink(self, period_ms: int = 500):
|
||||
self._signal._set_lamp_mode(self, Lamp.MODE_BLINK, period_ms=period_ms)
|
||||
|
||||
|
||||
class RailwaySignal:
|
||||
"""Контейнер ламп светофора.
|
||||
|
||||
Хранит NeoPixel и набор ламп (каждая лампа — один пиксель).
|
||||
Мигающие лампы обслуживаются общим Timer.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pin: int,
|
||||
num_leds: int,
|
||||
*,
|
||||
bpp: int = 4,
|
||||
timing: int = 1,
|
||||
colors: dict | None = None,
|
||||
timer_id: int = -1,
|
||||
timer_tick_ms: int = 50,
|
||||
brightness: float = 0.5,
|
||||
):
|
||||
self.pin = pin
|
||||
self.num_leds = num_leds
|
||||
self.bpp = bpp
|
||||
self.timing = timing
|
||||
|
||||
self.colors = colors if colors is not None else LAMP_COLORS
|
||||
|
||||
self.brightness = float(brightness)
|
||||
if self.brightness < 0.0:
|
||||
self.brightness = 0.0
|
||||
if self.brightness > 1.0:
|
||||
self.brightness = 1.0
|
||||
|
||||
self._timer_id = timer_id
|
||||
self._timer_tick_ms = int(timer_tick_ms)
|
||||
self._timer = None
|
||||
self._timer_running = False
|
||||
self._scheduled = False
|
||||
|
||||
self._lamps_by_id = {}
|
||||
|
||||
self._np = neopixel.NeoPixel(
|
||||
machine.Pin(pin),
|
||||
num_leds,
|
||||
bpp=bpp,
|
||||
timing=timing,
|
||||
)
|
||||
|
||||
self.clear()
|
||||
|
||||
def set_brightness(self, brightness: float):
|
||||
"""Устанавливает глобальный коэффициент яркости (0.0..1.0)."""
|
||||
|
||||
self.brightness = float(brightness)
|
||||
if self.brightness < 0.0:
|
||||
self.brightness = 0.0
|
||||
if self.brightness > 1.0:
|
||||
self.brightness = 1.0
|
||||
|
||||
# Перерисовываем текущее состояние ламп
|
||||
for lamp in self._lamps_by_id.values():
|
||||
self._apply_lamp(lamp)
|
||||
self.show()
|
||||
|
||||
def _scale(self, value: int) -> int:
|
||||
value = int(value)
|
||||
if value <= 0:
|
||||
return 0
|
||||
scaled = int(value * self.brightness)
|
||||
if scaled < 0:
|
||||
return 0
|
||||
if scaled > 255:
|
||||
return 255
|
||||
return scaled
|
||||
|
||||
def _normalize_rgba(self, rgba: tuple | list):
|
||||
"""Приводит цвет к формату (r,g,b,w) с учётом self.bpp.
|
||||
|
||||
- для bpp=4: всегда (r,g,b,w)
|
||||
- для bpp=3: W-канал маппится в RGB только если RGB=0 (белый)
|
||||
"""
|
||||
|
||||
if len(rgba) == 3:
|
||||
r, g, b = rgba
|
||||
w = 0
|
||||
else:
|
||||
r, g, b, w = rgba
|
||||
|
||||
r = int(r)
|
||||
g = int(g)
|
||||
b = int(b)
|
||||
w = int(w)
|
||||
|
||||
if self.bpp == 3:
|
||||
# На RGB-ленте нет W-канала. Самый ожидаемый кейс — "WHITE"/"MOON_WHITE",
|
||||
# которые заданы как (0,0,0,w). Маппим их в (w,w,w).
|
||||
if r == 0 and g == 0 and b == 0 and w > 0:
|
||||
r = w
|
||||
g = w
|
||||
b = w
|
||||
w = 0
|
||||
|
||||
return (r, g, b, w)
|
||||
|
||||
def add_lamp(self, lamp_id: int, pixel_index: int, color: str | tuple | list):
|
||||
if pixel_index < 0 or pixel_index >= self.num_leds:
|
||||
raise ValueError("pixel_index out of range")
|
||||
|
||||
rgba = None
|
||||
if isinstance(color, str):
|
||||
rgba = self.colors.get(color.upper())
|
||||
elif isinstance(color, (tuple, list)):
|
||||
rgba = color
|
||||
|
||||
if rgba is None:
|
||||
raise ValueError("Unknown color")
|
||||
|
||||
rgba = self._normalize_rgba(rgba)
|
||||
|
||||
lamp = Lamp(self, lamp_id, pixel_index, rgba)
|
||||
self._lamps_by_id[lamp_id] = lamp
|
||||
lamp.off()
|
||||
return lamp
|
||||
|
||||
def lamp(self, lamp_id: int):
|
||||
return self._lamps_by_id.get(lamp_id)
|
||||
|
||||
def _stop_timer(self):
|
||||
if self._timer is None or not self._timer_running:
|
||||
return
|
||||
try:
|
||||
self._timer.deinit()
|
||||
except Exception:
|
||||
pass
|
||||
self._timer_running = False
|
||||
|
||||
def _ensure_timer(self):
|
||||
if self._timer_running:
|
||||
return
|
||||
|
||||
try:
|
||||
self._timer = machine.Timer(self._timer_id)
|
||||
except Exception:
|
||||
# Some ports require Timer(0/1/2...), some allow -1.
|
||||
self._timer = machine.Timer(0)
|
||||
|
||||
try:
|
||||
self._timer.init(period=self._timer_tick_ms, mode=machine.Timer.PERIODIC, callback=self._timer_cb)
|
||||
self._timer_running = True
|
||||
except Exception:
|
||||
self._timer_running = False
|
||||
|
||||
def _now_ms(self) -> int:
|
||||
ticks_ms = getattr(time, "ticks_ms", None)
|
||||
if ticks_ms is not None:
|
||||
return int(ticks_ms())
|
||||
return int(time.time() * 1000)
|
||||
|
||||
def _ticks_diff(self, a: int, b: int) -> int:
|
||||
ticks_diff = getattr(time, "ticks_diff", None)
|
||||
if ticks_diff is not None:
|
||||
return int(ticks_diff(a, b))
|
||||
return int(a - b)
|
||||
|
||||
def show(self):
|
||||
self._np.write()
|
||||
|
||||
def clear(self):
|
||||
self._stop_timer()
|
||||
self._scheduled = False
|
||||
if self.bpp == 3:
|
||||
self._np.fill((0, 0, 0))
|
||||
else:
|
||||
self._np.fill((0, 0, 0, 0))
|
||||
self.show()
|
||||
|
||||
def set_pixel_rgba(self, index: int, r: int, g: int, b: int, w: int = 0):
|
||||
if self.bpp == 3:
|
||||
self._np[index] = (self._scale(r), self._scale(g), self._scale(b))
|
||||
else:
|
||||
self._np[index] = (
|
||||
self._scale(r),
|
||||
self._scale(g),
|
||||
self._scale(b),
|
||||
self._scale(w),
|
||||
)
|
||||
|
||||
def fill_rgba(self, r: int, g: int, b: int, w: int = 0):
|
||||
if self.bpp == 3:
|
||||
self._np.fill((self._scale(r), self._scale(g), self._scale(b)))
|
||||
else:
|
||||
self._np.fill((self._scale(r), self._scale(g), self._scale(b), self._scale(w)))
|
||||
|
||||
def _apply_lamp(self, lamp: Lamp):
|
||||
if lamp.mode == Lamp.MODE_ON:
|
||||
r, g, b, w = lamp.rgba
|
||||
self.set_pixel_rgba(lamp.pixel, r, g, b, w)
|
||||
elif lamp.mode == Lamp.MODE_BLINK:
|
||||
if lamp._blink_on:
|
||||
r, g, b, w = lamp.rgba
|
||||
self.set_pixel_rgba(lamp.pixel, r, g, b, w)
|
||||
else:
|
||||
self.set_pixel_rgba(lamp.pixel, 0, 0, 0, 0)
|
||||
else:
|
||||
self.set_pixel_rgba(lamp.pixel, 0, 0, 0, 0)
|
||||
|
||||
def _any_blinking(self) -> bool:
|
||||
for lamp in self._lamps_by_id.values():
|
||||
if lamp.mode == Lamp.MODE_BLINK:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _set_lamp_mode(self, lamp: Lamp, mode: str, *, period_ms: int = 500):
|
||||
if mode == Lamp.MODE_BLINK:
|
||||
lamp.mode = Lamp.MODE_BLINK
|
||||
lamp._blink_period_ms = int(period_ms)
|
||||
lamp._blink_on = False # стартуем с OFF как раньше
|
||||
lamp._next_toggle_ms = self._now_ms() + lamp._blink_period_ms
|
||||
elif mode == Lamp.MODE_ON:
|
||||
lamp.mode = Lamp.MODE_ON
|
||||
lamp._blink_on = False
|
||||
else:
|
||||
lamp.mode = Lamp.MODE_OFF
|
||||
lamp._blink_on = False
|
||||
|
||||
self._apply_lamp(lamp)
|
||||
self.show()
|
||||
|
||||
if self._any_blinking():
|
||||
self._ensure_timer()
|
||||
else:
|
||||
self._stop_timer()
|
||||
|
||||
def _timer_cb(self, _t):
|
||||
# IRQ context: keep it tiny.
|
||||
if not self._any_blinking():
|
||||
return
|
||||
|
||||
if micropython is not None:
|
||||
if self._scheduled:
|
||||
return
|
||||
try:
|
||||
self._scheduled = True
|
||||
micropython.schedule(self._scheduled_tick, 0)
|
||||
except Exception:
|
||||
self._scheduled = False
|
||||
|
||||
def _scheduled_tick(self, _arg):
|
||||
self._scheduled = False
|
||||
|
||||
now_ms = self._now_ms()
|
||||
dirty = False
|
||||
|
||||
for lamp in self._lamps_by_id.values():
|
||||
if lamp.mode != Lamp.MODE_BLINK:
|
||||
continue
|
||||
if self._ticks_diff(now_ms, lamp._next_toggle_ms) < 0:
|
||||
continue
|
||||
|
||||
lamp._blink_on = not lamp._blink_on
|
||||
lamp._next_toggle_ms = now_ms + lamp._blink_period_ms
|
||||
self._apply_lamp(lamp)
|
||||
dirty = True
|
||||
|
||||
if dirty:
|
||||
self.show()
|
||||
|
||||
if not self._any_blinking():
|
||||
self._stop_timer()
|
||||
Reference in New Issue
Block a user