feat(api-input): make SegmentPayload start/length optional

start defaults to 0, length defaults to led_count - start (the rest of
the strip from start). A single segment with only mode + color now
fills the entire strip — no more length: 9999 magic value clients have
to pass.

Buffer auto-grow only fires for segments with an explicit length past
the current end; implicit "to the end" segments adapt to the current
strip size.
This commit is contained in:
2026-04-26 23:34:42 +03:00
parent a56569b02f
commit 1c9acc5afb
2 changed files with 46 additions and 16 deletions
@@ -655,10 +655,22 @@ class ColorStripSourceListResponse(BaseModel):
class SegmentPayload(BaseModel): class SegmentPayload(BaseModel):
"""A single segment for segment-based LED color updates.""" """A single segment for segment-based LED color updates.
start: int = Field(ge=0, description="Starting LED index") ``start`` and ``length`` are optional: when omitted, the segment defaults
length: int = Field(ge=1, description="Number of LEDs in segment") to ``start=0`` and ``length=led_count - start`` (i.e. the rest of the
strip from ``start``). Sending a single segment with only ``mode`` and
``color`` therefore fills the entire strip.
"""
start: Optional[int] = Field(
None, ge=0, description="Starting LED index (default 0 = beginning of strip)"
)
length: Optional[int] = Field(
None,
ge=1,
description="Number of LEDs in segment (default = led_count - start)",
)
mode: Literal["solid", "per_pixel", "gradient"] = Field(description="Fill mode") mode: Literal["solid", "per_pixel", "gradient"] = Field(description="Fill mode")
color: Optional[List[int]] = Field(None, description="RGB for solid mode [R,G,B]") color: Optional[List[int]] = Field(None, description="RGB for solid mode [R,G,B]")
colors: Optional[List[List[int]]] = Field( colors: Optional[List[List[int]]] = Field(
@@ -148,23 +148,37 @@ class ApiInputColorStripStream(ColorStripStream):
"""Apply segment-based color updates to the buffer. """Apply segment-based color updates to the buffer.
Each segment defines a range and fill mode. Segments are applied in Each segment defines a range and fill mode. Segments are applied in
order (last wins on overlap). The buffer is auto-grown if needed. order (last wins on overlap).
``start`` and ``length`` are optional: ``start`` defaults to 0,
``length`` defaults to ``led_count - start`` (i.e. the remainder of
the strip). The buffer is only auto-grown for segments that supply
an explicit ``length`` extending past the current end — implicit
"to the end" segments adapt to whatever the current strip size is.
Args: Args:
segments: list of dicts with keys: segments: list of dicts with keys:
start (int) starting LED index start (int, optional) starting LED index (default 0)
length (int) number of LEDs in segment length (int, optional) number of LEDs in segment
mode (str) "solid" | "per_pixel" | "gradient" (default = led_count - start)
color (list) [R,G,B] for solid mode mode (str) "solid" | "per_pixel" | "gradient"
colors (list) [[R,G,B], ...] for per_pixel/gradient color (list) [R,G,B] for solid mode
colors (list) [[R,G,B], ...] for per_pixel/gradient
""" """
# Compute required buffer size from all segments # Compute required buffer size from segments that supply an explicit
max_index = max(seg["start"] + seg["length"] for seg in segments) # length. Segments without a length take the strip as-is and so do
# not trigger growth.
explicit_max = 0
for seg in segments:
seg_start = seg.get("start") or 0
seg_len = seg.get("length")
if seg_len is not None:
explicit_max = max(explicit_max, seg_start + seg_len)
with self._lock: with self._lock:
# Auto-grow buffer if needed # Auto-grow buffer if any explicit segment extends past current end
if max_index > self._led_count: if explicit_max > self._led_count:
self._ensure_capacity(max_index) self._ensure_capacity(explicit_max)
# Start from current buffer (or fallback if timed out) # Start from current buffer (or fallback if timed out)
if self._timed_out: if self._timed_out:
@@ -173,8 +187,12 @@ class ApiInputColorStripStream(ColorStripStream):
buf = self._colors.copy() buf = self._colors.copy()
for seg in segments: for seg in segments:
start = seg["start"] seg_start = seg.get("start")
length = seg["length"] start = 0 if seg_start is None else seg_start
seg_len = seg.get("length")
length = max(0, self._led_count - start) if seg_len is None else seg_len
if length <= 0:
continue
mode = seg["mode"] mode = seg["mode"]
end = start + length end = start + length