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):
"""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")
length: int = Field(ge=1, description="Number of LEDs in segment")
``start`` and ``length`` are optional: when omitted, the segment defaults
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")
color: Optional[List[int]] = Field(None, description="RGB for solid mode [R,G,B]")
colors: Optional[List[List[int]]] = Field(
@@ -148,23 +148,37 @@ class ApiInputColorStripStream(ColorStripStream):
"""Apply segment-based color updates to the buffer.
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:
segments: list of dicts with keys:
start (int) starting LED index
length (int) number of LEDs in segment
start (int, optional) starting LED index (default 0)
length (int, optional) number of LEDs in segment
(default = led_count - start)
mode (str) "solid" | "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
max_index = max(seg["start"] + seg["length"] for seg in segments)
# Compute required buffer size from segments that supply an explicit
# 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:
# Auto-grow buffer if needed
if max_index > self._led_count:
self._ensure_capacity(max_index)
# Auto-grow buffer if any explicit segment extends past current end
if explicit_max > self._led_count:
self._ensure_capacity(explicit_max)
# Start from current buffer (or fallback if timed out)
if self._timed_out:
@@ -173,8 +187,12 @@ class ApiInputColorStripStream(ColorStripStream):
buf = self._colors.copy()
for seg in segments:
start = seg["start"]
length = seg["length"]
seg_start = seg.get("start")
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"]
end = start + length