From cf13f025082a7b8fd09031ec3beb634fa9f87086 Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Thu, 27 Nov 2025 16:35:33 +0000 Subject: [PATCH 1/7] applet.video.ws2812_output: Add support for BGR pixel format Light curtains on Aliexpress seem to follow this standard. --- software/glasgow/applet/video/ws2812_output/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index 5a5d3d989..608e9bdfd 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -145,6 +145,7 @@ class VideoWS2812OutputApplet(GlasgowApplet): pixel_formats = { # in-out in size out size format_func "RGB-BRG": ( 3, 3, lambda r,g,b: Cat(b,r,g) ), + "RGB-BGR": ( 3, 3, lambda r,g,b: Cat(b,g,r) ), "RGB-xBRG": ( 3, 4, lambda r,g,b: Cat(Const(0, unsigned(8)),b,r,g) ), "RGBW-WBRG": ( 4, 4, lambda r,g,b,w: Cat(w,b,r,g) ), } From b7cbe1063ecafb20be2ce7c4f57a76255a5e61f1 Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Fri, 28 Nov 2025 00:30:10 +0000 Subject: [PATCH 2/7] applet.video.ws2812_output: port to V2 --- .../applet/video/ws2812_output/__init__.py | 156 +++++++++++------- .../applet/video/ws2812_output/test.py | 4 +- 2 files changed, 101 insertions(+), 59 deletions(-) diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index 608e9bdfd..957d8542e 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -1,15 +1,18 @@ import logging import asyncio + from amaranth import * -from amaranth.lib import io +from amaranth.lib import io, wiring, stream +from amaranth.lib.wiring import In -from ....support.endpoint import * -from ....gateware.pll import * -from ... import * +from glasgow.abstract import AbstractAssembly, GlasgowPin, PortGroup +from glasgow.applet import GlasgowAppletV2 +from glasgow.support.endpoint import * +from glasgow.gateware.pll import * class VideoWS2812Output(Elaboratable): - def __init__(self, ports): + def __init__(self, ports: PortGroup): self.ports = ports self.out = Signal(len(ports.out)) @@ -22,14 +25,19 @@ def elaborate(self, platform): return m -class VideoWS2812OutputSubtarget(Elaboratable): - def __init__(self, ports, count, pix_in_size, pix_out_size, pix_format_func, out_fifo): - self.ports = ports - self.count = count - self.pix_in_size = pix_in_size - self.pix_out_size = pix_out_size +class VideoWS2812OutputComponent(wiring.Component): + i_stream: In(stream.Signature(8)) + + def __init__( + self, ports: PortGroup, count: int, pix_in_size: int, pix_out_size: int, pix_format_func + ): + self.ports = ports + self.count = count + self.pix_in_size = pix_in_size + self.pix_out_size = pix_out_size self.pix_format_func = pix_format_func - self.out_fifo = out_fifo + + super().__init__() def elaborate(self, platform): # Safe timings: @@ -54,29 +62,29 @@ def elaborate(self, platform): pix_out_size = self.pix_out_size pix_out_bpp = pix_out_size * 8 - cyc_ctr = Signal(range(t_reset+1)) - bit_ctr = Signal(range(pix_out_bpp+1)) - byt_ctr = Signal(range((pix_in_size)+1)) - pix_ctr = Signal(range(self.count+1)) + cyc_ctr = Signal(range(t_reset + 1)) + bit_ctr = Signal(range(pix_out_bpp + 1)) + byt_ctr = Signal(range((pix_in_size) + 1)) + pix_ctr = Signal(range(self.count + 1)) word_ctr = Signal(range(max(2, len(self.ports.out)))) - pix = Array([ Signal(8) for i in range((pix_in_size) - 1) ]) + pix = Array([Signal(8) for i in range((pix_in_size) - 1)]) word = Signal(pix_out_bpp * len(self.ports.out)) with m.FSM(): with m.State("LOAD"): m.d.comb += [ - self.out_fifo.r_en.eq(1), + self.i_stream.ready.eq(1), output.out.eq(0), ] - with m.If(self.out_fifo.r_rdy): + with m.If(self.i_stream.valid): with m.If(byt_ctr < ((pix_in_size) - 1)): m.d.sync += [ - pix[byt_ctr].eq(self.out_fifo.r_data), + pix[byt_ctr].eq(self.i_stream.payload), byt_ctr.eq(byt_ctr + 1), ] with m.Else(): - p = self.pix_format_func(*pix, self.out_fifo.r_data) + p = self.pix_format_func(*pix, self.i_stream.payload) m.d.sync += word.eq(Cat(word[pix_out_bpp:], p)) with m.If(word_ctr < (len(self.ports.out) - 1)): m.d.sync += [ @@ -91,8 +99,10 @@ def elaborate(self, platform): m.d.comb += output.out.eq((1 << len(self.ports.out)) - 1) m.d.sync += cyc_ctr.eq(cyc_ctr + 1) with m.Elif(cyc_ctr < t_one): - m.d.comb += (o.eq(word[(pix_out_bpp - 1) + (pix_out_bpp * i)]) - for i,o in enumerate(output.out)) + m.d.comb += ( + o.eq(word[(pix_out_bpp - 1) + (pix_out_bpp * i)]) + for i, o in enumerate(output.out) + ) m.d.sync += cyc_ctr.eq(cyc_ctr + 1) with m.Elif(cyc_ctr < t_period): m.d.comb += output.out.eq(0) @@ -135,7 +145,42 @@ def elaborate(self, platform): return m -class VideoWS2812OutputApplet(GlasgowApplet): +class VideoWS2812OutputInterface: + def __init__( + self, + logger: logging.Logger, + assembly: AbstractAssembly, + *, + out: tuple[GlasgowPin], + count: int, + pix_in_size: int, + pix_out_size: int, + pix_format_func, + buffer: int, + ): + self._logger = logger + self._frame_size = len(out) * pix_in_size * count + ports = assembly.add_port_group(out=out) + component = assembly.add_submodule( + VideoWS2812OutputComponent(ports, count, pix_in_size, pix_out_size, pix_format_func) + ) + self._pipe = assembly.add_out_pipe( + component.i_stream, buffer_size=len(out) * count * pix_in_size * buffer + ) + + async def write_frame(self, data): + """Send one or more frame's worth of pixel data to the LED string.""" + assert len(data) % self._frame_size == 0 + await self._pipe.send(data) + await self._pipe.flush(_wait=False) + + @property + def frame_size(self) -> int: + """Size of each frame in bytes.""" + return self._frame_size + + +class VideoWS2812OutputApplet(GlasgowAppletV2): logger = logging.getLogger(__name__) help = "display video via WS2812 LEDs" description = """ @@ -152,8 +197,7 @@ class VideoWS2812OutputApplet(GlasgowApplet): @classmethod def add_build_arguments(cls, parser, access): - super().add_build_arguments(parser, access) - + access.add_voltage_argument(parser) access.add_pins_argument(parser, "out", width=range(1, 17), required=True) parser.add_argument( "-c", "--count", metavar="N", type=int, required=True, @@ -161,44 +205,42 @@ def add_build_arguments(cls, parser, access): parser.add_argument( "-f", "--pix-fmt", metavar="F", choices=cls.pixel_formats.keys(), default="RGB-BRG", help="set the pixel format (one of: %(choices)s, default: %(default)s)") - - def build(self, target, args): - self.pix_in_size, pix_out_size, pix_format_func = self.pixel_formats[args.pix_fmt] - - self.mux_interface = iface = target.multiplexer.claim_interface(self, args) - subtarget = iface.add_subtarget(VideoWS2812OutputSubtarget( - ports=iface.get_port_group(out=args.out), - count=args.count, - pix_in_size=self.pix_in_size, - pix_out_size=pix_out_size, - pix_format_func=pix_format_func, - out_fifo=iface.get_out_fifo(), - )) - - return subtarget - - @classmethod - def add_run_arguments(cls, parser, access): - super().add_run_arguments(parser, access) - parser.add_argument( "-b", "--buffer", metavar="N", type=int, default=16, help="set the number of frames to buffer internally (buffered twice)") - async def run(self, device, args): - buffer_size = len(args.out) * args.count * self.pix_in_size * args.buffer - return await device.demultiplexer.claim_interface(self, self.mux_interface, args, - write_buffer_size=buffer_size) + def build(self, args): + self.pix_in_size, pix_out_size, pix_format_func = self.pixel_formats[args.pix_fmt] + + with self.assembly.add_applet(self): + self.assembly.use_voltage(args.voltage) + self.ws2812_iface = VideoWS2812OutputInterface( + self.logger, + self.assembly, + out=args.out, + count=args.count, + pix_in_size=self.pix_in_size, + pix_out_size=pix_out_size, + pix_format_func=pix_format_func, + buffer=args.buffer, + ) @classmethod - def add_interact_arguments(cls, parser): + def add_run_arguments(cls, parser): ServerEndpoint.add_argument(parser, "endpoint") - async def interact(self, device, args, leds): - frame_size = len(args.out) * args.count * self.pix_in_size + async def run(self, args): + # This buffer is for the socket only, and is independet from the one + # configured in VideoWS2812OutputInterface + frame_size = self.ws2812_iface.frame_size buffer_size = frame_size * args.buffer - endpoint = await ServerEndpoint("socket", self.logger, args.endpoint, - queue_size=buffer_size, deprecated_cancel_on_eof=True) + endpoint = await ServerEndpoint( + "socket", + self.logger, + args.endpoint, + queue_size=buffer_size, + deprecated_cancel_on_eof=True, + ) while True: try: data = await asyncio.shield(endpoint.recv(buffer_size)) @@ -206,12 +248,12 @@ async def interact(self, device, args, leds): while partial: data += await asyncio.shield(endpoint.recv(frame_size - partial)) partial = len(data) % frame_size - await leds.write(data) - await leds.flush(wait=False) + await self.ws2812_iface.write_frame(data) except asyncio.CancelledError: pass @classmethod def tests(cls): from . import test + return test.VideoWS2812OutputAppletTestCase diff --git a/software/glasgow/applet/video/ws2812_output/test.py b/software/glasgow/applet/video/ws2812_output/test.py index c80b79d93..f2b2d9c82 100644 --- a/software/glasgow/applet/video/ws2812_output/test.py +++ b/software/glasgow/applet/video/ws2812_output/test.py @@ -1,8 +1,8 @@ -from ... import * +from glasgow.applet import GlasgowAppletV2TestCase, synthesis_test from . import VideoWS2812OutputApplet -class VideoWS2812OutputAppletTestCase(GlasgowAppletTestCase, applet=VideoWS2812OutputApplet): +class VideoWS2812OutputAppletTestCase(GlasgowAppletV2TestCase, applet=VideoWS2812OutputApplet): @synthesis_test def test_build(self): self.assertBuilds(args=["--out", "A0:3", "-c", "1024"]) From 285c1d5ffcc8a5b91bfd6b513f13de77d21e70ea Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Fri, 28 Nov 2025 00:33:56 +0000 Subject: [PATCH 3/7] applet.video.ws2812_output: Remove deprecated_cancel_on_eof --- software/glasgow/applet/video/ws2812_output/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index 957d8542e..a068a108c 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -239,7 +239,6 @@ async def run(self, args): self.logger, args.endpoint, queue_size=buffer_size, - deprecated_cancel_on_eof=True, ) while True: try: @@ -249,7 +248,7 @@ async def run(self, args): data += await asyncio.shield(endpoint.recv(frame_size - partial)) partial = len(data) % frame_size await self.ws2812_iface.write_frame(data) - except asyncio.CancelledError: + except EOFError: pass @classmethod From 8bf14586b87cb7ccbdc93c3d969d103730baa739 Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Fri, 28 Nov 2025 22:58:33 +0000 Subject: [PATCH 4/7] applet.video.ws2812_output: Remove redundant socket buffering --- .../glasgow/applet/video/ws2812_output/__init__.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index a068a108c..09f79fa08 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -1,5 +1,4 @@ import logging -import asyncio from amaranth import * from amaranth.lib import io, wiring, stream @@ -232,8 +231,7 @@ def add_run_arguments(cls, parser): async def run(self, args): # This buffer is for the socket only, and is independet from the one # configured in VideoWS2812OutputInterface - frame_size = self.ws2812_iface.frame_size - buffer_size = frame_size * args.buffer + buffer_size = self.ws2812_iface.frame_size * args.buffer endpoint = await ServerEndpoint( "socket", self.logger, @@ -242,12 +240,9 @@ async def run(self, args): ) while True: try: - data = await asyncio.shield(endpoint.recv(buffer_size)) - partial = len(data) % frame_size - while partial: - data += await asyncio.shield(endpoint.recv(frame_size - partial)) - partial = len(data) % frame_size - await self.ws2812_iface.write_frame(data) + await self.ws2812_iface.write_frame( + await endpoint.recv(self.ws2812_iface.frame_size) + ) except EOFError: pass From 043d89957ce0696399413f80f7e940d564265e0e Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Wed, 3 Dec 2025 00:59:54 +0000 Subject: [PATCH 5/7] applet.video.ws2812_output: Add optional framerate limiter --- .../applet/video/ws2812_output/__init__.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index 09f79fa08..05abd8e34 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -4,7 +4,7 @@ from amaranth.lib import io, wiring, stream from amaranth.lib.wiring import In -from glasgow.abstract import AbstractAssembly, GlasgowPin, PortGroup +from glasgow.abstract import AbstractAssembly, ClockDivisor, GlasgowPin, PortGroup from glasgow.applet import GlasgowAppletV2 from glasgow.support.endpoint import * from glasgow.gateware.pll import * @@ -26,6 +26,7 @@ def elaborate(self, platform): class VideoWS2812OutputComponent(wiring.Component): i_stream: In(stream.Signature(8)) + framerate_divisor: In(24) def __init__( self, ports: PortGroup, count: int, pix_in_size: int, pix_out_size: int, pix_format_func @@ -66,10 +67,14 @@ def elaborate(self, platform): byt_ctr = Signal(range((pix_in_size) + 1)) pix_ctr = Signal(range(self.count + 1)) word_ctr = Signal(range(max(2, len(self.ports.out)))) + framerate_ctr = Signal(self.framerate_divisor.shape()) pix = Array([Signal(8) for i in range((pix_in_size) - 1)]) word = Signal(pix_out_bpp * len(self.ports.out)) + with m.If(framerate_ctr + 1 != 0): + m.d.sync += framerate_ctr.eq(framerate_ctr + 1) + with m.FSM(): with m.State("LOAD"): m.d.comb += [ @@ -130,14 +135,16 @@ def elaborate(self, platform): with m.State("RESET"): m.d.comb += output.out.eq(0) - m.d.sync += cyc_ctr.eq(cyc_ctr + 1) - with m.If(cyc_ctr == t_reset): + with m.If(cyc_ctr + 1 != 0): + m.d.sync += cyc_ctr.eq(cyc_ctr + 1) + with m.If((cyc_ctr >= t_reset) & (framerate_ctr >= self.framerate_divisor)): m.d.sync += [ cyc_ctr.eq(0), pix_ctr.eq(0), bit_ctr.eq(0), byt_ctr.eq(0), word_ctr.eq(0), + framerate_ctr.eq(0), ] m.next = "LOAD" @@ -166,6 +173,9 @@ def __init__( self._pipe = assembly.add_out_pipe( component.i_stream, buffer_size=len(out) * count * pix_in_size * buffer ) + self._framerate = assembly.add_clock_divisor( + component.framerate_divisor, ref_period=assembly.sys_clk_period, name="framerate" + ) async def write_frame(self, data): """Send one or more frame's worth of pixel data to the LED string.""" @@ -178,6 +188,11 @@ def frame_size(self) -> int: """Size of each frame in bytes.""" return self._frame_size + @property + def framerate_limiter(self) -> ClockDivisor: + """Framerate limiter.""" + return self._framerate + class VideoWS2812OutputApplet(GlasgowAppletV2): logger = logging.getLogger(__name__) @@ -224,6 +239,16 @@ def build(self, args): buffer=args.buffer, ) + @classmethod + def add_setup_arguments(cls, parser): + parser.add_argument( + "-r", "--framerate", type=float, + help="configure a framerate limiter in Hz") + + async def setup(self, args): + if args.framerate is not None: + await self.ws2812_iface.framerate_limiter.set_frequency(args.framerate) + @classmethod def add_run_arguments(cls, parser): ServerEndpoint.add_argument(parser, "endpoint") From 506082d65d8cf2b103925eb3b27da40e27e18dd7 Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Thu, 4 Dec 2025 00:35:16 +0000 Subject: [PATCH 6/7] applet.video.ws2812_output: Add docs --- docs/manual/src/applets/index.rst | 1 + docs/manual/src/applets/video/index.rst | 11 +++++++++++ .../manual/src/applets/video/ws2812_output.rst | 18 ++++++++++++++++++ .../applet/video/ws2812_output/__init__.py | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/manual/src/applets/video/index.rst create mode 100644 docs/manual/src/applets/video/ws2812_output.rst diff --git a/docs/manual/src/applets/index.rst b/docs/manual/src/applets/index.rst index d303b33f6..9b82361fa 100644 --- a/docs/manual/src/applets/index.rst +++ b/docs/manual/src/applets/index.rst @@ -16,4 +16,5 @@ Applet index sensor/index bridge/index audio/index + video/index internal/index diff --git a/docs/manual/src/applets/video/index.rst b/docs/manual/src/applets/video/index.rst new file mode 100644 index 000000000..57f4e0631 --- /dev/null +++ b/docs/manual/src/applets/video/index.rst @@ -0,0 +1,11 @@ +.. _applet.video: + +Video capture and output +======================== + +.. automodule:: glasgow.applet.video + +.. toctree:: + :maxdepth: 3 + + ws2812_output diff --git a/docs/manual/src/applets/video/ws2812_output.rst b/docs/manual/src/applets/video/ws2812_output.rst new file mode 100644 index 000000000..5e54b5e77 --- /dev/null +++ b/docs/manual/src/applets/video/ws2812_output.rst @@ -0,0 +1,18 @@ +``video-ws2812-output`` +======================= + +CLI reference +------------- + +.. _applet.video.ws2812_output: + +.. autoprogram:: glasgow.applet.video.ws2812_output:VideoWS2812OutputApplet._get_argparser_for_sphinx("video-ws2812-output") + :prog: glasgow run video-ws2812-output + + +API reference +------------- + +.. module:: glasgow.applet.video.ws2812_output + +.. autoclass:: VideoWS2812OutputInterface diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index 05abd8e34..afc354494 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -218,7 +218,7 @@ def add_build_arguments(cls, parser, access): help="set the number of LEDs per string") parser.add_argument( "-f", "--pix-fmt", metavar="F", choices=cls.pixel_formats.keys(), default="RGB-BRG", - help="set the pixel format (one of: %(choices)s, default: %(default)s)") + help="set the pixel format (default: %(default)s)") parser.add_argument( "-b", "--buffer", metavar="N", type=int, default=16, help="set the number of frames to buffer internally (buffered twice)") From 26eb7e78583159323f5759cb7e393e43a95b1661 Mon Sep 17 00:00:00 2001 From: Balint Kovacs Date: Sat, 6 Dec 2025 22:17:24 +0000 Subject: [PATCH 7/7] applet.video.ws2812_output: Improve interface class usability --- .../applet/video/ws2812_output/__init__.py | 77 +++++++++++-------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/software/glasgow/applet/video/ws2812_output/__init__.py b/software/glasgow/applet/video/ws2812_output/__init__.py index afc354494..25b6aff76 100644 --- a/software/glasgow/applet/video/ws2812_output/__init__.py +++ b/software/glasgow/applet/video/ws2812_output/__init__.py @@ -1,4 +1,6 @@ +from dataclasses import dataclass import logging +import typing as t from amaranth import * from amaranth.lib import io, wiring, stream @@ -10,6 +12,37 @@ from glasgow.gateware.pll import * +__all__ = [ + "VideoWS2812PixelFormat", + "VIDEO_WS2812_PIXEL_FORMATS", + "VideoWS2812OutputComponent", + "VideoWS2812OutputInterface", +] + + +@dataclass(frozen=True) +class VideoWS2812PixelFormat: + in_size: int + out_size: int + format_func: t.Callable + + +VIDEO_WS2812_PIXEL_FORMATS = { + "RGB-BRG": VideoWS2812PixelFormat( + in_size=3, out_size=3, format_func=lambda r, g, b: Cat(b, r, g) + ), + "RGB-BGR": VideoWS2812PixelFormat( + in_size=3, out_size=3, format_func=lambda r, g, b: Cat(b, g, r) + ), + "RGB-xBRG": VideoWS2812PixelFormat( + in_size=3, out_size=4, format_func=lambda r, g, b: Cat(Const(0, unsigned(8)), b, r, g) + ), + "RGBW-WBRG": VideoWS2812PixelFormat( + in_size=4, out_size=4, format_func=lambda r, g, b, w: Cat(w, b, r, g) + ), +} + + class VideoWS2812Output(Elaboratable): def __init__(self, ports: PortGroup): self.ports = ports @@ -28,14 +61,10 @@ class VideoWS2812OutputComponent(wiring.Component): i_stream: In(stream.Signature(8)) framerate_divisor: In(24) - def __init__( - self, ports: PortGroup, count: int, pix_in_size: int, pix_out_size: int, pix_format_func - ): + def __init__(self, ports: PortGroup, count: int, pixel_format: VideoWS2812PixelFormat): self.ports = ports self.count = count - self.pix_in_size = pix_in_size - self.pix_out_size = pix_out_size - self.pix_format_func = pix_format_func + self.pixel_format = pixel_format super().__init__() @@ -58,8 +87,8 @@ def elaborate(self, platform): m.submodules.output = output = VideoWS2812Output(self.ports) - pix_in_size = self.pix_in_size - pix_out_size = self.pix_out_size + pix_in_size = self.pixel_format.in_size + pix_out_size = self.pixel_format.out_size pix_out_bpp = pix_out_size * 8 cyc_ctr = Signal(range(t_reset + 1)) @@ -88,7 +117,7 @@ def elaborate(self, platform): byt_ctr.eq(byt_ctr + 1), ] with m.Else(): - p = self.pix_format_func(*pix, self.i_stream.payload) + p = self.pixel_format.format_func(*pix, self.i_stream.payload) m.d.sync += word.eq(Cat(word[pix_out_bpp:], p)) with m.If(word_ctr < (len(self.ports.out) - 1)): m.d.sync += [ @@ -159,19 +188,15 @@ def __init__( *, out: tuple[GlasgowPin], count: int, - pix_in_size: int, - pix_out_size: int, - pix_format_func, + pixel_format: VideoWS2812PixelFormat, buffer: int, ): self._logger = logger - self._frame_size = len(out) * pix_in_size * count + self._frame_size = len(out) * pixel_format.in_size * count ports = assembly.add_port_group(out=out) - component = assembly.add_submodule( - VideoWS2812OutputComponent(ports, count, pix_in_size, pix_out_size, pix_format_func) - ) + component = assembly.add_submodule(VideoWS2812OutputComponent(ports, count, pixel_format)) self._pipe = assembly.add_out_pipe( - component.i_stream, buffer_size=len(out) * count * pix_in_size * buffer + component.i_stream, buffer_size=self._frame_size * buffer ) self._framerate = assembly.add_clock_divisor( component.framerate_divisor, ref_period=assembly.sys_clk_period, name="framerate" @@ -201,14 +226,6 @@ class VideoWS2812OutputApplet(GlasgowAppletV2): Output RGB(W) frames from a socket to one or more WS2812(B) LED strings. """ - pixel_formats = { - # in-out in size out size format_func - "RGB-BRG": ( 3, 3, lambda r,g,b: Cat(b,r,g) ), - "RGB-BGR": ( 3, 3, lambda r,g,b: Cat(b,g,r) ), - "RGB-xBRG": ( 3, 4, lambda r,g,b: Cat(Const(0, unsigned(8)),b,r,g) ), - "RGBW-WBRG": ( 4, 4, lambda r,g,b,w: Cat(w,b,r,g) ), - } - @classmethod def add_build_arguments(cls, parser, access): access.add_voltage_argument(parser) @@ -217,15 +234,13 @@ def add_build_arguments(cls, parser, access): "-c", "--count", metavar="N", type=int, required=True, help="set the number of LEDs per string") parser.add_argument( - "-f", "--pix-fmt", metavar="F", choices=cls.pixel_formats.keys(), default="RGB-BRG", - help="set the pixel format (default: %(default)s)") + "-f", "--pix-fmt", metavar="F", choices=VIDEO_WS2812_PIXEL_FORMATS.keys(), + default="RGB-BRG", help="set the pixel format (default: %(default)s)") parser.add_argument( "-b", "--buffer", metavar="N", type=int, default=16, help="set the number of frames to buffer internally (buffered twice)") def build(self, args): - self.pix_in_size, pix_out_size, pix_format_func = self.pixel_formats[args.pix_fmt] - with self.assembly.add_applet(self): self.assembly.use_voltage(args.voltage) self.ws2812_iface = VideoWS2812OutputInterface( @@ -233,9 +248,7 @@ def build(self, args): self.assembly, out=args.out, count=args.count, - pix_in_size=self.pix_in_size, - pix_out_size=pix_out_size, - pix_format_func=pix_format_func, + pixel_format=VIDEO_WS2812_PIXEL_FORMATS[args.pix_fmt], buffer=args.buffer, )