Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/renjfk/OpenNeato/discussions
about: Ask questions, share ideas, or get help with setup before opening an issue.
blank_issues_enabled: true
contact_links:
- name: Discussions
url: https://github.com/Leicas/OpenNeato/discussions
about: Ask questions, share ideas, or get help with setup before opening an issue.
31 changes: 21 additions & 10 deletions custom_components/openneato/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,19 +231,30 @@ async def get_history_session(self, filename: str) -> str:
async with timeout(TIMEOUT):
async with self._session.get(url) as response:
response.raise_for_status()
raw = await response.content.read(MAX_HISTORY_RESPONSE_BYTES + 1)
if len(raw) > MAX_HISTORY_RESPONSE_BYTES:
raise OpenNeatoApiError(
f"Session {filename} exceeds size cap "
f"({MAX_HISTORY_RESPONSE_BYTES} bytes)"
)
# The firmware serves this endpoint as an HTTP chunked
# transfer with no Content-Length (beginChunkedResponse
# in web_server.cpp). On a chunked aiohttp response,
# content.read(n) returns as soon as ANY buffered data is
# available — it does NOT block until n bytes or EOF — so
# a single bounded read silently truncates the JSONL to
# the first chunk, producing a PARTIAL map. Drain the full
# stream to EOF (matching the frontend's res.text()),
# enforcing the size cap incrementally as we accumulate.
buf = bytearray()
async for chunk in response.content.iter_chunked(65536):
buf.extend(chunk)
if len(buf) > MAX_HISTORY_RESPONSE_BYTES:
raise OpenNeatoApiError(
f"Session {filename} exceeds size cap "
f"({MAX_HISTORY_RESPONSE_BYTES} bytes)"
)
# Firmware emits UTF-8 JSONL; hardcode rather than
# call response.get_encoding(), which raises in
# modern aiohttp when content was streamed via
# response.content.read() (the streaming path
# doesn't populate the response's _body buffer
# that get_encoding's chardet fallback needs).
return raw.decode("utf-8", errors="replace")
# response.content (the streaming path doesn't
# populate the response's _body buffer that
# get_encoding's chardet fallback needs).
return bytes(buf).decode("utf-8", errors="replace")
except aiohttp.ClientConnectionError as err:
raise OpenNeatoConnectionError(
f"Unable to connect to OpenNeato at {self._host}: {err}"
Expand Down
15 changes: 14 additions & 1 deletion custom_components/openneato/history_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io
import json
import logging
import math
import re
from typing import Any
Expand All @@ -43,6 +44,8 @@
MOTION_TOTAL_MS,
)

_LOGGER = logging.getLogger(__name__)


# ── JSONL parsing (ported from history-data.ts) ─────────────────────

Expand Down Expand Up @@ -90,13 +93,23 @@ def parse_session_jsonl(raw: str) -> dict[str, Any]:
poses: list[dict[str, float]] = []
recharges: list[tuple[float, float]] = []

for line in lines:
for idx, line in enumerate(lines):
try:
obj = json.loads(line)
except json.JSONDecodeError:
repaired = _try_repair_pose(line)
if repaired:
poses.append(repaired)
elif idx == len(lines) - 1:
# A final line that parses as neither JSON nor a repairable
# pose is the classic signature of a truncated download (the
# stream was cut mid-line). Surface it so a silently-partial
# map is observable rather than rendered without warning.
_LOGGER.warning(
"Final history line failed to parse (%d chars); "
"session data may be truncated, map will be partial",
len(line),
)
continue

obj_type = obj.get("type")
Expand Down
5 changes: 4 additions & 1 deletion custom_components/openneato/lidar_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ def render_lidar_scan(

# ── Helpers ──────────────────────────────────────────────────────
def to_canvas(angle_deg: float, dist: float) -> tuple[float, float]:
rad = math.radians(90 - angle_deg)
# Match the frontend reference (lidar-map.tsx): rad = (90 + angle).
# Using (90 - angle) here flips the cos sign and mirrors the scan
# horizontally relative to the web dashboard.
rad = math.radians(90 + angle_deg)
return (cx + dist * scale * math.cos(rad), cy - dist * scale * math.sin(rad))

def opacity(age: int) -> float:
Expand Down
Loading