From 0571939de0e4e36e487a4e504464e4900e1f24e1 Mon Sep 17 00:00:00 2001 From: Olivier Jacques Date: Sat, 25 Jul 2026 11:21:45 +0200 Subject: [PATCH 1/4] feat: add render_alert helper with word-wrap and blinking border New PluginHelpers.render_alert() method providing a reusable alert display capability for any plugin: - Blinking colored border (configurable color, width, blink rate) - Auto word-wrap: text is split at word boundaries to fit the available width inside the border - Centered text rendering inside the bordered area - Used by reolink-alert plugin (replaces inline rendering logic) Also: - Add _word_wrap() helper for pixel-width-aware text wrapping - 17 unit tests covering render_alert and word_wrap - CI: restrict image artifact upload to main/tags only (saves GH storage) --- .github/workflows/build-rpi-image.yml | 4 +- tests/test_alert_helper.py | 204 ++++++++++++++++++++++++ zeclock/plugins/helpers.py | 124 ++++++++++++++ zeclock/plugins/reolink_alert_plugin.py | 44 ++--- 4 files changed, 339 insertions(+), 37 deletions(-) create mode 100644 tests/test_alert_helper.py diff --git a/.github/workflows/build-rpi-image.yml b/.github/workflows/build-rpi-image.yml index e92fbf8..b3d5213 100644 --- a/.github/workflows/build-rpi-image.yml +++ b/.github/workflows/build-rpi-image.yml @@ -110,8 +110,8 @@ jobs: echo "IMAGE_FILE=${OUTPUT_IMG}.gz" >> $GITHUB_ENV echo "IMAGE_PATH=deploy/rpi/${OUTPUT_IMG}.gz" >> $GITHUB_ENV - - name: Upload artifact (PR and feature branch builds) - if: github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/main') + - name: Upload artifact (main and tag builds only) + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main') uses: actions/upload-artifact@v4 with: name: zeclock-rpi-image diff --git a/tests/test_alert_helper.py b/tests/test_alert_helper.py new file mode 100644 index 0000000..5c50c86 --- /dev/null +++ b/tests/test_alert_helper.py @@ -0,0 +1,204 @@ +"""Tests for the render_alert and _word_wrap methods in PluginHelpers.""" + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from PIL import Image + +from zeclock.plugins.helpers import PluginHelpers + + +@pytest.fixture +def helpers(tmp_path): + """Create a PluginHelpers instance with standard test dimensions.""" + fonts_dir = tmp_path / "Fonts" + fonts_dir.mkdir() + return PluginHelpers(128, 32, tmp_path) + + +@pytest.fixture +def helpers_hd(tmp_path): + """Create a PluginHelpers instance with HD dimensions.""" + fonts_dir = tmp_path / "Fonts" + fonts_dir.mkdir() + return PluginHelpers(256, 64, tmp_path) + + +class TestRenderAlert: + """Tests for PluginHelpers.render_alert().""" + + def test_returns_rgb_image_with_correct_dimensions(self, helpers): + """render_alert returns an RGB image with the display dimensions.""" + frame = helpers.render_alert("TEST ALERT", frame_index=0) + assert frame.mode == "RGB" + assert frame.size == (128, 32) + + def test_returns_correct_dimensions_hd(self, helpers_hd): + """render_alert works with HD dimensions.""" + frame = helpers_hd.render_alert("TEST ALERT", frame_index=0) + assert frame.mode == "RGB" + assert frame.size == (256, 64) + + def test_border_visible_on_blink_on_frame(self, helpers): + """Border pixels are colored when blink is on (even cycle).""" + frame = helpers.render_alert( + "X", + frame_index=0, + border_color=(255, 0, 0), + blink_interval_frames=5, + ) + pixels = frame.load() + # Top-left corner pixel should be the border color + assert pixels[0, 0] == (255, 0, 0) + # Middle of top border + assert pixels[64, 0] == (255, 0, 0) + + def test_border_hidden_on_blink_off_frame(self, helpers): + """Border pixels are black when blink is off (odd cycle).""" + # frame_index=5 with blink_interval_frames=5 means cycle 1 (odd = off) + frame = helpers.render_alert( + "X", + frame_index=5, + border_color=(255, 0, 0), + blink_interval_frames=5, + ) + pixels = frame.load() + # Top-left corner should be black (border off) + assert pixels[0, 0] == (0, 0, 0) + + def test_border_alternates(self, helpers): + """Border alternates between visible and hidden.""" + frame_on = helpers.render_alert("X", frame_index=0, blink_interval_frames=3) + frame_off = helpers.render_alert("X", frame_index=3, blink_interval_frames=3) + pixels_on = frame_on.load() + pixels_off = frame_off.load() + # On frame: border visible + assert pixels_on[0, 0] != (0, 0, 0) + # Off frame: border hidden + assert pixels_off[0, 0] == (0, 0, 0) + + def test_custom_border_color(self, helpers): + """Custom border color is used.""" + frame = helpers.render_alert( + "X", + frame_index=0, + border_color=(0, 255, 0), + blink_interval_frames=100, + ) + pixels = frame.load() + assert pixels[0, 0] == (0, 255, 0) + + def test_border_width_3_pixels(self, helpers): + """Default border width is 3 pixels deep.""" + frame = helpers.render_alert( + "X", + frame_index=0, + border_color=(255, 0, 0), + border_width=3, + blink_interval_frames=100, + ) + pixels = frame.load() + # Pixel at depth 0, 1, 2 should be border + assert pixels[0, 0] == (255, 0, 0) + assert pixels[1, 1] == (255, 0, 0) + assert pixels[2, 2] == (255, 0, 0) + # Pixel at depth 3 should NOT be border (it's the gap or text area) + # (it's either black background or text) + assert pixels[3, 3] != (255, 0, 0) + + def test_empty_text_renders_without_error(self, helpers): + """Empty text doesn't crash.""" + frame = helpers.render_alert("", frame_index=0) + assert frame.size == (128, 32) + + def test_very_long_text_does_not_crash(self, helpers): + """Very long text is wrapped without crashing.""" + long_text = "A " * 100 + frame = helpers.render_alert(long_text, frame_index=0) + assert frame.size == (128, 32) + + +class TestWordWrap: + """Tests for PluginHelpers._word_wrap().""" + + def _make_helpers_with_font(self, tmp_path, char_width=6): + """Create helpers with a mocked font for predictable width measurement.""" + fonts_dir = tmp_path / "Fonts" + fonts_dir.mkdir(exist_ok=True) + h = PluginHelpers(128, 32, tmp_path) + + # Mock a font with fixed character width + mock_font = MagicMock() + mock_font.char_height = 10 + mock_font.name = "MOCK" + mock_font.get_text_width = lambda text: len(text) * char_width + h._fonts["STANDARD"] = mock_font + return h + + def test_short_text_no_wrap(self, tmp_path): + """Text that fits in one line is not wrapped.""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + # 128px wide, 6px per char = 21 chars fit + result = h._word_wrap("HELLO WORLD", 128, "STANDARD") + assert result == "HELLO WORLD" + + def test_long_text_wraps_at_word_boundary(self, tmp_path): + """Text wider than max_width wraps at word boundaries.""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + # max_width=60px, 6px/char = 10 chars per line + result = h._word_wrap("HELLO WORLD FOO", 60, "STANDARD") + lines = result.split("\n") + assert len(lines) >= 2 + # Each line should fit within 60px (10 chars) + for line in lines: + assert len(line) * 6 <= 60 or len(line.split()) == 1 + + def test_single_long_word_stays_on_own_line(self, tmp_path): + """A word wider than max_width gets its own line (not split mid-word).""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + # max_width=30px (5 chars), word is 10 chars + result = h._word_wrap("ABCDEFGHIJ", 30, "STANDARD") + # Single word, can't break — stays as-is on one line + assert result == "ABCDEFGHIJ" + + def test_existing_newlines_preserved(self, tmp_path): + """Existing newlines in text are preserved.""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + result = h._word_wrap("LINE1\nLINE2", 128, "STANDARD") + assert "LINE1" in result + assert "LINE2" in result + lines = result.split("\n") + assert lines[0] == "LINE1" + assert lines[1] == "LINE2" + + def test_empty_text(self, tmp_path): + """Empty text returns empty string.""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + result = h._word_wrap("", 128, "STANDARD") + assert result == "" + + def test_multiple_spaces_treated_as_separators(self, tmp_path): + """Multiple spaces between words are collapsed by split().""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + result = h._word_wrap("A B C", 128, "STANDARD") + # split() collapses spaces + assert result == "A B C" + + def test_wraps_correctly_with_narrow_width(self, tmp_path): + """Each word ends up on its own line when width is very narrow.""" + h = self._make_helpers_with_font(tmp_path, char_width=6) + # max_width=36px (6 chars) — "PERSONNE" is 8 chars, won't fit with another word + result = h._word_wrap("PERSONNE DETECTEE", 36, "STANDARD") + lines = result.split("\n") + assert "PERSONNE" in lines + assert "DETECTEE" in lines + + def test_no_font_returns_original_text(self, tmp_path): + """If font is not found, returns original text unchanged.""" + fonts_dir = tmp_path / "Fonts" + fonts_dir.mkdir(exist_ok=True) + h = PluginHelpers(128, 32, tmp_path) + # No fonts loaded — _get_font returns None + result = h._word_wrap("HELLO WORLD", 60, "NONEXISTENT") + assert result == "HELLO WORLD" diff --git a/zeclock/plugins/helpers.py b/zeclock/plugins/helpers.py index 25308a6..6c2cf31 100644 --- a/zeclock/plugins/helpers.py +++ b/zeclock/plugins/helpers.py @@ -569,6 +569,130 @@ def draw_staleness_indicator( """ draw_staleness_indicator(frame, current_frame, frame_delay_ms) + def render_alert( + self, + text: str, + frame_index: int, + border_color: Tuple[int, int, int] = (255, 0, 0), + text_color: Tuple[int, int, int] = (255, 255, 255), + font_name: Optional[str] = None, + border_width: int = 3, + blink_interval_frames: int = 5, + ) -> Image.Image: + """Render an alert frame with blinking border and auto-wrapped text. + + Draws a blinking colored border around the display, with the alert + text centered inside. Text is automatically word-wrapped to fit + within the bordered area. + + Args: + text: Alert message to display (auto-wrapped to fit). + frame_index: Current frame counter (for blink timing). + border_color: RGB tuple for the border color (default: red). + text_color: RGB tuple for the text color (default: white). + font_name: Font to use (default: plugin helpers default font). + border_width: Border thickness in pixels (default: 3). + blink_interval_frames: Toggle border every N frames (default: 5). + + Returns: + PIL Image in RGB mode with the alert rendered. + """ + from PIL import ImageDraw + + frame = self.create_frame() + draw = ImageDraw.Draw(frame) + + # Blinking border + blink_on = (frame_index // blink_interval_frames) % 2 == 0 + draw_color = border_color if blink_on else (0, 0, 0) + + for i in range(border_width): + draw.rectangle( + [i, i, self.width - 1 - i, self.height - 1 - i], + outline=draw_color, + ) + + # Inner area for text (inside the border) + inner_x = border_width + 1 + inner_y = border_width + 1 + inner_w = self.width - 2 * (border_width + 1) + inner_h = self.height - 2 * (border_width + 1) + + if inner_w <= 0 or inner_h <= 0: + return frame + + # Word-wrap text to fit inner width + font_name = self._resolve_font_name(font_name) + wrapped_text = self._word_wrap(text, inner_w, font_name) + + # Render wrapped text centered in the inner area + text_frame = self.render_text( + wrapped_text, color=text_color, centered=True, font_name=font_name + ) + + # Crop the inner region from the text frame and paste into alert frame + text_crop = text_frame.crop( + (inner_x, inner_y, inner_x + inner_w, inner_y + inner_h) + ) + frame.paste(text_crop, (inner_x, inner_y)) + + return frame + + def _word_wrap(self, text: str, max_width: int, font_name: str) -> str: + """Wrap text to fit within a given pixel width. + + Splits on spaces and joins with newlines so each line fits + within max_width pixels using the specified font. Words that + are individually wider than max_width are kept on their own line. + + Args: + text: Input text (may already contain newlines). + max_width: Maximum line width in pixels. + font_name: Font name for width measurement. + + Returns: + Text with newlines inserted for wrapping. + """ + font = self._get_font(font_name) + if font is None: + return text + + # Transliterate for accurate width measurement + text = transliterate(text) + + # Process each existing line separately + result_lines = [] + for paragraph in text.split("\n"): + words = paragraph.split() + if not words: + result_lines.append("") + continue + + current_line = "" + for word in words: + if not current_line: + # First word on line + current_line = word + else: + # Try adding word to current line + test_line = current_line + " " + word + test_width = font.get_text_width(test_line) + # Account for font scale in HD mode + is_hd_font = font.name.endswith("_HD") if font.name else False + if self._font_scale > 1 and not is_hd_font: + test_width *= self._font_scale + + if test_width <= max_width: + current_line = test_line + else: + result_lines.append(current_line) + current_line = word + + if current_line: + result_lines.append(current_line) + + return "\n".join(result_lines) + def resolve_color( self, color_name: str, default: str = "orange" ) -> Tuple[int, int, int]: diff --git a/zeclock/plugins/reolink_alert_plugin.py b/zeclock/plugins/reolink_alert_plugin.py index 9620c36..ac60d71 100644 --- a/zeclock/plugins/reolink_alert_plugin.py +++ b/zeclock/plugins/reolink_alert_plugin.py @@ -24,7 +24,7 @@ from typing import Any, Dict, List, Optional, Tuple import aiohttp -from PIL import Image, ImageDraw +from PIL import Image from .base import ClockPlugin, ConfigField, PluginNotConfiguredError @@ -434,43 +434,17 @@ async def render_frame(self, width: int, height: int) -> Optional[Image.Image]: ) message = self._get_message(self._alert_type) - # Create frame - frame = Image.new("RGB", (width, height), (0, 0, 0)) - draw = ImageDraw.Draw(frame) - - # Blinking border - blink_on = (self._frame_count // BLINK_INTERVAL_FRAMES) % 2 == 0 - border_color = border_color_rgb if blink_on else (0, 0, 0) - - # Draw thick border (2px for 128x32, 4px for 256x64) - border_width = max(2, min(width, height) // 16) - for i in range(border_width): - draw.rectangle( - [i, i, width - 1 - i, height - 1 - i], - outline=border_color, - ) - - # Render text centered inside the border + # Use the render_alert helper for consistent alert rendering if self._helpers: - text_frame = self._helpers.render_text( - message, - color=(255, 255, 255), - centered=True, + return self._helpers.render_alert( + text=message, + frame_index=self._frame_count, + border_color=border_color_rgb, + text_color=(255, 255, 255), ) - # Composite text onto frame (inside the border) - inner_x = border_width + 1 - inner_y = border_width + 1 - inner_w = width - 2 * (border_width + 1) - inner_h = height - 2 * (border_width + 1) - - if inner_w > 0 and inner_h > 0: - # Crop center of text frame to fit inside border - text_crop = text_frame.crop( - (inner_x, inner_y, inner_x + inner_w, inner_y + inner_h) - ) - frame.paste(text_crop, (inner_x, inner_y)) - return frame + # Fallback if helpers not available + return Image.new("RGB", (width, height), (0, 0, 0)) def _render_status(self, width: int, height: int) -> Image.Image: """Render a status screen when no alert is active.""" From d24bf0a0269ad67ad5ef493a2065a4f4f3008ae4 Mon Sep 17 00:00:00 2001 From: Olivier Jacques Date: Sat, 25 Jul 2026 11:26:46 +0200 Subject: [PATCH 2/4] feat: render_alert helper + /api/alert endpoint + alert icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New reusable alert system for zeClock: 1. PluginHelpers.render_alert() method: - Blinking colored border (3px, configurable) - Auto word-wrap based on font pixel width - Optional emoji icon (left-aligned, vertically centered) - Text rendered in MENU font, forced uppercase - Centered text in the bordered area 2. /api/alert REST endpoint: POST {"text": "...", "duration": 15, "icon": "person", "color": [255,0,0]} - Triggers rich alert rendering in the clock main loop - Takes priority over text overlay and normal plugins - Auto-expires after duration, resumes normal display 3. Alert icons (from Noto Color Emoji via scripts/emoji_to_icon.py): beacon 🚨, person đŸšļ, vehicle 🚗, animal 🐾, motion 💨, warning âš ī¸, camera 📷, info â„šī¸ 4. Reolink plugin simplified: - Now calls /api/alert instead of force-plugin mechanism - Pure background listener, no render_frame complexity - cleanup() is a no-op (listener runs independently) 5. CI: restrict image artifact upload to main/tags only Tests: 23 tests (render_alert, word_wrap, icons) Tested on Pi: all 7 detection types validated visually --- tests/test_alert_helper.py | 55 ++++++++++ zeclock/clock.py | 33 +++++- zeclock/plugins/alert_icons.py | 105 +++++++++++++++++++ zeclock/plugins/helpers.py | 130 +++++++++++++++++++++--- zeclock/plugins/reolink_alert_plugin.py | 127 +++++++---------------- zeclock/remote/command_handler.py | 16 +++ zeclock/remote/rest_remote.py | 67 ++++++++++++ 7 files changed, 426 insertions(+), 107 deletions(-) create mode 100644 zeclock/plugins/alert_icons.py diff --git a/tests/test_alert_helper.py b/tests/test_alert_helper.py index 5c50c86..1e489e7 100644 --- a/tests/test_alert_helper.py +++ b/tests/test_alert_helper.py @@ -202,3 +202,58 @@ def test_no_font_returns_original_text(self, tmp_path): # No fonts loaded — _get_font returns None result = h._word_wrap("HELLO WORLD", 60, "NONEXISTENT") assert result == "HELLO WORLD" + + +class TestRenderAlertWithIcon: + """Tests for render_alert with icon parameter.""" + + def test_icon_parameter_accepted(self, helpers): + """render_alert accepts icon parameter without error.""" + frame = helpers.render_alert("ALERT", frame_index=0, icon="beacon") + assert frame.size == (128, 32) + + def test_unknown_icon_does_not_crash(self, helpers): + """Unknown icon name doesn't crash (gracefully ignored).""" + frame = helpers.render_alert("ALERT", frame_index=0, icon="nonexistent") + assert frame.size == (128, 32) + + def test_none_icon_same_as_no_icon(self, helpers): + """icon=None produces same result as no icon parameter.""" + frame_no_icon = helpers.render_alert("ALERT", frame_index=0) + frame_none = helpers.render_alert("ALERT", frame_index=0, icon=None) + assert frame_no_icon.tobytes() == frame_none.tobytes() + + def test_icon_with_real_alert_icons_module(self, helpers): + """Test with the actual alert_icons module (if available).""" + try: + from zeclock.plugins.alert_icons import get_alert_icon + + icon = get_alert_icon("beacon", hd=False) + assert icon is not None + assert icon.size == (16, 16) + assert icon.mode == "RGB" + except ImportError: + pytest.skip("alert_icons module not generated") + + def test_all_detection_icons_available(self): + """All detection type icons exist in the alert_icons module.""" + try: + from zeclock.plugins.alert_icons import get_alert_icon + + for name in ("beacon", "person", "vehicle", "animal", "motion", "warning"): + icon = get_alert_icon(name, hd=False) + assert icon is not None, f"Icon '{name}' not found" + assert icon.size == (16, 16) + except ImportError: + pytest.skip("alert_icons module not generated") + + def test_hd_icons_are_32x32(self): + """HD icons are 32x32.""" + try: + from zeclock.plugins.alert_icons import get_alert_icon + + icon = get_alert_icon("beacon", hd=True) + assert icon is not None + assert icon.size == (32, 32) + except ImportError: + pytest.skip("alert_icons module not generated") diff --git a/zeclock/clock.py b/zeclock/clock.py index 171aa43..e74c394 100644 --- a/zeclock/clock.py +++ b/zeclock/clock.py @@ -261,8 +261,13 @@ async def run(self) -> None: self.color = COLOR_LIST[int(now // 60) % len(COLOR_LIST)] self.last_color_change = now + # Remote control: check for alert overlay (rich: border + icon) + if self._command_handler and self._command_handler.has_alert: + frame = self._render_alert_overlay() + frame_time = 0.1 # 10 FPS for smooth blink + # Remote control: check for text overlay - if self._command_handler and self._command_handler.has_text_overlay: + elif self._command_handler and self._command_handler.has_text_overlay: frame = self._render_text_overlay() frame_time = 0.5 @@ -571,6 +576,32 @@ async def _init_remote_control(self) -> None: self._rest_remote = RestRemote(rest_cfg, self._command_handler) asyncio.create_task(self._rest_remote.run()) + def _render_alert_overlay(self) -> Image.Image: + """Render a rich alert overlay using the PluginHelpers.render_alert method. + + Displays a blinking border with optional icon and auto-wrapped text. + """ + if not self._command_handler: + return Image.new("RGB", (self.width, self.height), (0, 0, 0)) + + text = self._command_handler._alert_text or "" + icon = self._command_handler._alert_icon + color = self._command_handler._alert_color or (255, 0, 0) + self._command_handler._alert_frame_count += 1 + frame_index = self._command_handler._alert_frame_count + + if self._plugin_manager: + return self._plugin_manager._helpers.render_alert( + text=text, + frame_index=frame_index, + border_color=color, + text_color=(255, 255, 255), + icon=icon, + ) + + # Fallback: simple text + return Image.new("RGB", (self.width, self.height), (0, 0, 0)) + def _render_text_overlay(self) -> Image.Image: """Render a text overlay frame centered on screen. diff --git a/zeclock/plugins/alert_icons.py b/zeclock/plugins/alert_icons.py new file mode 100644 index 0000000..39fa332 --- /dev/null +++ b/zeclock/plugins/alert_icons.py @@ -0,0 +1,105 @@ +"""Alert icons rendered from Noto Color Emoji (SD 16x16 + HD 32x32). + +Generated with: scripts/emoji_to_icon.py + +Icons are pre-rendered from the Noto Color Emoji font (SIL OFL). +https://fonts.google.com/noto/specimen/Noto+Color+Emoji + +SD icons (16x16) are used for 128x32 displays. +HD icons (32x32) are used for 256x64 displays. + +Available icons: + beacon: 🚨 + person: đŸšļ + vehicle: 🚗 + animal: 🐾 + motion: 💨 + warning: âš ī¸ + camera: 📷 + info: â„šī¸ +""" + +from typing import Dict, Optional + +from PIL import Image + +# === SD Icons (16x16) === + +_BEACON_HEX = "000000000000010000000000631b16cd392ce83e33f34337f34336e83d32cd392e621b1600000001000000000000000000000003010100000039100dff4538fb4739f6473af54737f54738f64b3efb4437ff473939100d0000000301010000000000000401010000007e211bfe4d3cee493bf77c73f74d35f9654df7776fef4433fe4a397e211b000000040101000000000000030101000000ab332aff4d3df64725fc9c8bfd876afe9f86fb826bf54928ff594cab332a000000030101000000000000000000010101cc3c31fe9087f9927affa689ffd5c9ffd1c5ffa488faaa9bfe8379cc362b010101000000000000000000000000100504e64132fc553bfcb09dffdbd1ffd5caffd5caffded6fe977afd4c2ae64232100404000000000000000000000000270a08f64a3bfa6b50fead96ffdad0ffd2c7ffd4c8ffd5cafeb29ef97a6af54e422609070000000000000000000000003e0e0bff6457f58b84fda18effcebfffdad0ffd9cfffcfc3ff987af78272ff5d513e0f0c000000000000000000000000571813ff473af3452afd714bffcbbcfec3b3ffc4b3fec5b3fb7860f44626ff4b3a571813000000000000000000020202711f1aff4d3df44e37fd998ffc735bff9170fe9a88ff6840fb978ef14a3cff4b3a71201a020202000000000000000000842019ff493ce8463be95047ee412af06e5cec6f69ed422bed4d3ee8483dff493c842119000000000000000000414645c75a54d11b17c6201ec71e1dcf2e28cb2e2ccb3230c92623cb2623c51e1dd21b17c75a544146450000000000007d939ad7cfd4d28585d66664d6524fd44542d3413ed3403dd44543d5514ed76664d18585d7cfd47d939a000000000000688e9f82b3c68fc0d09fc5d2abc6d0b2c3ccb6c3cbb6c3cbb2c3ccabc6d09fc5d28fc1d082b3c6688e9f0000000000007197a68bbacd84b0c37fadc17dacc07cacc07cacc07cacc07cacc07dacc07fadc184b0c38bbacd7197a600000000000011171941565f61818e7398a87ba3b381abbd84aec084aec081abbd7ba3b37398a861818e41565f111719000000" + +_PERSON_HEX = "000000000000000000000000000000140e0d402a2c563a3319110e0000000101010000000000000000000000000000000000000000000000000000000000002e2117976f33624432452f28000000000000000000000000000000000000000000000000000000000000000000000000382c0ae9be2cab8131362225000000010100000000000000000000000000000000000000000000000000000000000000271e08e8bf26e1b22a120d0d000000010101000000000000000000000000000000000000000000000000000000000000000000462d0af98e1f321a0400000002020000000000000000000000000000000000000000000000000000000000000001010068370dff7927602c0d000000040201000000000000000000000000000000000000000000000000000000000000000000874910ffa31d6d4009000000040300000000000000000000000000000000000000000000000000000000000000000000974b0cffc21f7d5b0e000000040301000000000000000000000000000000000000000000000000000000000000000000a6580dffc2208b4a05000000040200000000000000000000000000000000000000000000000000000000000000000001a98323f5c2364a3a3900000003020100000000000000000000000000000000000000000000000000000000000008080e897f60988a6931363e0101010202020000000000000000000000000000000000000000000000000000000000001a1a1977797d6a6c724645430000000102020000000000000000000000000000000000000000000000000000000000001b1b1b747473333332696969232323000000000000000000000000000000000000000000000000000000000000000000131312706f6c0202022828285c5c5c141c2a0000000000000000000000000000000000000000000000000000000000001113155c616a0304050000002636552e4d880000000000000000000000000000000000000000000000000000000000003a58913f60a105070c010101182b51080f1c000000000000000000000000000000" + +_VEHICLE_HEX = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050100000000410500a9180cac2d25ac3632b63b35af332db3241a700c020000000400000200000000000300000100000000004f0600c3352e8a8a9b8fbbd295c7df6c93aa8dbbd385a9beb94845890d000000000000000000000000000200015f0a02be383082a0b4b2f4fcbaf2febbf2ff7ea5bab4e9ffbdf9fd86cae4bf4946941104100000020000660200d21f0df42f1c8e595fe4392f979daf8ab6c98db1c56d8a9d8bb1c593b6ca87bad0975d61ff260cba44386f5855ff8c7eff210bec2617dc1f1ad20e13d51e18d5211ad52019d9261fd6221bd5221ad3231bed190cec332ee7d9d6ccaba4f16967dc010ae10416e20114db0618dc0616dd0515dd0516dc0414dc0515df0415e80214e10316dc0014cd595fd7110fdb0009bb0e1d792b2d80282bc70a19d50517d20718d20717d20718d50617cb0918872227802528c80b19c8000c930410ec071a6b3e3ca2b3b08ca19c7f2c2df10317e6081ae80719e6081af10317862a2d8a9e9a93a9a47c3536ce04140a010245020751403bbdb9b8aba9a74a2a2965010963040c64030b68040c6a010a4d2e2bb3b1b0c1bebe533b3633010400000000000013100e463b37413632080a08000000000000000000000000000000090a09473d384c423e11100e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_ANIMAL_HEX = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002040001020a2d5409284a0414241667c107203b01040701060b0000000000010000000000000000000000000000000000000000001566bf1768c20d3e721c81ef0f45801150941253990001020000010000000000000000000000000000000d3b6d0b335f0f47841669c303101e0c3664061b32186ecc1462b40002030000000000010000000000000000000000001769c11b7de7061d350412220b34600d3b6d071f39061f3905162900000000010100000000000000000000000000000007223f104c8d0411201252971975d91871d21975d80c36640000000001020000000000010001030000000000000000000000000000000b35621a79de186dca186ecc1974d612539c0001020002030001010000000000000000000001020001020003050001030a2f57176cc80f4786082341061e3701070d00000000000000000001060c061e37000000000000000000000000000000000000020a12000000000000000000000000000102051a2f030e1a0a30591b7ce60723410c3e720d3c70000000000000000000000000000102000103000203000000010204176dca155fb20c3a6b1976db0b325d186cc91870d0000000000000000000000101000000000001000000030e1a061b330f4c8d1972d3030f1d05162900000109294d0a2f560000000000000000000000000000000002030000000f437c1a7be40a2e5404111f0a305911529712569e09294d000000000000000000000000000000000000000102000000061d361771d10b35620825451a78de186fce1972d21a7be30c39690000000000000000000000000000000000000000000000000209120103051359a41871d2186cc81460b11460b20a2c530000000000000000000000000000000000000000000001010000000000000d3d72166ac40a3059020910010408000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_MOTION_HEX = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012327280d1113446172a8cde1c1d9e6afc1c84750550000000202020000000000001016194a66730c1012111a1f98b7c5d7e9f0bed4ddc3e4f6ddf2fbd7ecf6e2f6febde4f91b2a3301010102030300000012191da7e7ff4b6a7b597486e7ffffd2e8f2c5e6f7d2e9f3d4e9f3d3eaf5d6e7efb3e4fe53758a0000000102020202021a242991c9e92a3c4666889adff9fecce3ef89c6e8b2daf0daedf6d3eaf5d8ecf5b6dbeec7e6f58e9ba107080804070939515f1b272d000000293d48bce3f6d6eaf397cdebbbdef1d9edf6d4ebf5d5ebf5d8edf5d5eaf3e8ffff6570752f3e473c4f5a405561526d7d4e6e7f86bddb9cd1efb1d9eed7ecf5d5ebf5d5ebf5d5ebf5d4ebf5d2e8f2d9f0faadbec75597b977d4ff74ccfb70cafa90d6fb9ccfed9bcfecd8ecf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d2e9f3d9eef9bfd5dd124c6949b4ea45acdf47ace14eaee268bce8a5d4eed8ecf5d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d8eaf3dff1fa85b7ce62a8cd77cefc75ccf977ccf972c9f775c1ea8fcaeac1e2f2dbeef6d5ebf5d5ebf5d5ebf5d6ebf5c2e2f094e0ff2f5d721e242737434b32393f3e4e574e64729dcde7b6ddf29acfecb9def1d5ebf5d4ebf5d9ecf5bfe1f064c4ee357692030405000406336c832a596d000000091117a8d6f0dcedf4d1e9f4cce7f4d3eaf5d4eaf5d4eaf3d1eaf5c3e2f11c1c1b00000000000013252c72deff33637804060796c5dcdef4fed2e6efd4e8f2dbebf2daebf3d3e9f3d5e9f3ddf0fa1f23240000000102020000003d79923f7d96000000344b58bfe2f3e2f8ffe0f7ffa4e0fab5e3f7d5ecf6e2feff96a8b00102020202020000000001010000000001010101010000001d293064747c6a73773364784ea3c771b8d95c889a0b1113000000010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_WARNING_HEX = "000000000000000000000000000000020100000000a67100b87d000604000201000100000000000000000000000000000000000000000000000000000303010000004a380affd02bffbc0d593c00000000030200000000000000000000000000000000000000000000000000020201010000caa934fedc43fdc626ce8d010604000101000100000000000000000000000000000000000000000303010000004b3c0ffff86afcd23dfccb31ffc0105a3c00000000040200000000000000000000000000000000000000020201010000cab242fbee61726134655d3ff5c428d18e000504000101000100000000000000000000000000000303010000004b3d10fffa6befc73d433f3935373de2b835ffc210593b00000000040200000000000000000000000000020201010000c8b245feef5ff7c32b504b39423f38efc134fecc28cd8b000604000201000100000000000000000403010000004c3f13fff96dfccf3affcc30544d36434036f9c731fccd34ffbd105a3c00000000030300000000000000020201010100c9aa36fee652fac72dffd2335b5133494333ffcd31fdcc34fec928ce8c010604000101000100000403010000004d3c0cffe952fcce37fdc930ffd632655831514930ffd232fecb31fccb34ffbe105b3d00000000040300020200010100caa126fed536fccb32fcca32ffd93366582e50472effd632fdca31fdcb34fec928cf8d000604000201000000004b3c0fffd835fcc930ffcc32fcca32ffd7339d822f8d752effd633fcca32ffcb31fccc34ffbf10593b00000000010100c59e26fed535fcca31ffcc32fdcb32ffd332b4932fa98a2effd233fecb32ffcc31fdcb34feca28cb8a00060400514110ffd334fcca30feca32feca32fac932fed53362542a4d4428fed233fcc932feca32feca31fccc34feba10614100d9ad2affd635ffce33ffd033ffd033ffcf33ffd133ffcc33fecb33ffd133ffcf33ffd033ffcf32ffd136ffcb28da9600a18220e6bf2ee5b72ce7b82ce7b82ce7b82ce7b82ce9bb2ce9bb2de7b82ce7b82ce7b82ce7b82ce6b930e6b21e9b6900" + +_CAMERA_HEX = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014191b82a1ac81929b1b1e2051646e262e330000000000000000000000000000000000000000000000000000000000008da1a6afc6cfabb9c199a5acadbdc5a0adb58b969b88939788929588929588939787909680878b7e85898793977e9095bbdae0aebec4aebac1b3c1c8aebbc2b3c0c7b1c1cad0d5d1fffef0fff4d7d9d9cd9eaab13d3b392a282790a3aab0cfd5bedde4bbcdd5bac8cfb8c7cebac9d0b9c8d0b5c3cbafbbbeb7bebeb6bdbab2bec0a3afb65960644a54599aafb8b0cfd6748387768084767e82767e83778084727a7e848c91929799767576757475909396868f93747e837b848977818669767a3937371c1b1b1615141817171312112a2a29716f6f2f2f2f1c2f331b30342e2f2f716e6d3231300f0e0d2321213433336868682f2f2f212121242424202021777777292d2e3b6e7a81c5d6378497165361252b2c7b7a7a1f20203838386161616060602c2c2c1f1f1f1d1d1d38393966616012343c79c1d2346e7b245d6b3887990f3842605c5a3d3e3e2e2e2e5959596161612c2c2c1f1f1f1b1b1b4142425c575516444e337e8f1d515d215460317a8b174b5754504f4546462d2d2d5a5a5a6161612c2c2c1f1f1f1f1f1f2e2f2f716e6d0d23293282943075852e72823586990a262d6e6a683233333131315a5a5a6262622c2c2c1e1d1d22222219191962636353515009282f2162712263730a2d354847476b6b6b1616163636355a5a5a5c5c5c2c2c2c2021212424242222232323236b6c6c635e5d3838383737375d59577071712727281e1e1e34353554555596abaf96a3a895a1a795a1a796a1a794a0a5929ea49ea7ab93989b93989a9da6ab939fa5939fa5949fa59aa8ae869a9d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_INFO_HEX = "5f768082a3b27f9fac82a3b182a2b07f9eac809fae7f9eac809fae7f9eac7f9eac7f9eac7f9dac809fae7196a62b4c5789abbb98becda2cbd9a2cad998becd8eb1c28aafc08fb2c28aafc08eb2c28fb2c28eb2c28eb2c28fb3c38bb1c147778684a4b59dc4d39cc3d18db1c08badbd86aabab0c8d2eaf0f3c7d8df88abbc8baebe8badbd8badbd8caebe86abbb45728286a7b79fc6d48fb3c38aadbd8eb0c086abbceaf1f3ffffffffffff97b6c589adbd8dafbf8cafbf8db0c088adbd44738386a8b796bbcb8cafbf8cafbf8db0c087acbcb4cbd5ebf1f4ccdce38aaebe8cafbf8cafbf8cafbf8db0c088adbd44738386a7b78cafc08cafbf8cafbf8cafbf8baebe8fb1c1a4c0cc96b6c58aadbe8cafbf8cafbf8cafbf8db0c088adbd44738386a7b78db0c18cafbf8cafbf8eb0c084a9bac3d5ddffffffe0eaee88acbd8db0bf8cafbf8cafbf8db0c088adbd44738386a7b78db0c18cafbf8cafbf8eb0c084a9bac3d5ddffffffe0eaee88acbd8db0bf8cafbf8cafbf8db0c088adbd44738386a7b78db0c18cafbf8cafbf8eb0c084a9bac3d5ddffffffe0eaee88acbd8db0bf8cafbf8cafbf8db0c088adbd44738386a7b78db0c18cafbf8cafbf8eb0c084a9bac3d5ddffffffe0eaee88acbd8db0bf8cafbf8cafbf8db0c088adbd44738386a7b78db0c18cafbf8cafbf8eb0c084a9bac3d5ddffffffe0eaee88acbd8db0bf8cafbf8cafbf8db0c088adbd44738386a7b78db0c18cafbf8cafbf8eb0c084a9bac3d5ddffffffe0eaee88acbd8db0bf8cafbf8cafbf8db0c088adbd44738385a7b78db0c18cafbf8cafbf8eb0c084a9bac4d6ddffffffe1ebef88acbd8db0bf8cafbf8cafbf8db0c088adbd44738387a7b88eb1c28db0c08db0c08fb1c185aabbbfd3dbfafbfcdbe6eb89adbe8eb1c08db0c08db0c08eb1c18aafbf457282799faf8bafbf88acbc89adbd89adbd88acbc8cafbe92b3c28eb0c088acbc89adbd89adbd88acbc8baebe7ca4b4407181335b674e81924b7c8c4b7d8e4b7d8e4b7d8e4a7d8e497b8d4a7c8d4b7d8e4b7d8e4b7d8e4b7d8e4c7d8d467b8d305762" + + +# === HD Icons (32x32) === + +_BEACON_HD_HEX = "0000000000000000000000000000000000000200000000000e0403601a15a22c23c6362bd93b30f14336f44336f44336f44336f14336d93b30c6362ba22c23601a150e040300000002000000000000000000000000000000000000000000000000000000000000000000000000000002010100000041120fd73b30ff4639ff4638fc4638f94538f44336f44336f44437f44336f44336f94538fc4538ff4638ff4639d63a303f110e0000000201000000000000000000000000000000000000000000000000000000000000000200000100001d0806ec4134ff4739ef4234f14236f13f33f24134f44437f44336f64c3bf44336f44336f23e31f14336f14135ef4234ff4739ec41341d080601000002000000000000000000000000000000000000000000000000000000000004010100000074201aff483aed4034f44437f44033f55547f54d3ff44034f54637f9583ef44537f44236f76659f44437f44336f44236ed4134ff483a74201a000000040101000000000000000000000000000000000000000000000000000000010101000000b03027fe4739f24637f44438f33a2df76b5efa9286f3372bf64b3afd6542f54030f66458fb9d91f33b2df44337f64b3af24637fe4638b030270000000101010000000000000000000000000000000000000000000000000100000000000a0302da3c30fa4437f44f3bf9563ef44134f44a3efec8bcf65c51f64632ff7144f5412efcafa4fa8d80f33429f8543dfa5b3ff24437fa4638da3b2f0a03020000000100000000000000000000000000000000000000000000000201000000002a0c0af54235f34336f44639fc6141fc6341f44030fdb8aefcb3a9f84f34ff6d3bfa745cffded5f76c60f74b33ff6e46f8523df33e33f33c2ff543362a0c0a0000000201000000000000000000000000000000000000000000000301010000004f1511ff4c3ef04033f23429f64734ff7144fd6138fc9d8cffe4dcfd8c6fff6231ffc0aefed8d0fa6b52ff6c3bfc5c38f3362af54b3df36a5dff493c4f1511000000030101000000000000000000000000000000000000000000040101000000791e18ff584af6a094fa8b7df5554afa5a3cff632dff906effded7ffcbbcffb29cffdad0ffcbbfff7043ff6f41f9705dfa9c90fec5b9f3786bff453779221b0000000401010000000000000000000000000000000000000000000301010000009b2c23ff4335f15a4cfec5b9ffdfd4fdb8abff9f84ffac94ffd8ceffd6cbffdbd2ffd5caffd4c8ffb7a2ffc3b4ffe0d7fed4cbf6655aee3329ff463a9b2a21000000030101000000000000000000000000000000000000000000000000000000be3429ff473aef392ef43f34fba398ffe3daffdcd3ffdad1ffd5caffd5c9ffd4c8ffd5caffd5cbffdad0ffdcd4ffc5b6fa654bf84a31f5553dff533dbf392c000000010000000000000000000000000000000000000000010000000000090202d84231fe573ef95f40fc613efb5a37fea087ffd7cdffd3c7ffd5caffd5caffd5caffd5caffd4c9ffd6cbffcabbff6f43ff6b38ff7346f96140fc513cd63b2f0902020000000100000000000000000000000000000000000100000000001a0706ec4134f74a3af7533afc5d38ff6332ff8863ffd7cdffd4c9ffd5caffd5caffd5caffd5caffd5c9ffd7ccffcdc0ff916ffb6243f53e2df1362bf53e32ec40331a0706000000010000000000000000000000000000000000020101000000360f0cf84235f1382bf34136f7695efda998ffd5c9ffd6cbffd5caffd5caffd5caffd5caffd5caffd5caffd4c9ffdad0ffe1dafecec6fcaa9ff7796cf45346f74437330e0c0000000201010000000000000000000000000000000301010000004f1410ff5142f5897bfdbeb2ffd7cbffd6cdfecfc6ffd5cbffd5caffd5caffd5caffd5caffd5caffd4c9ffd7ccffc6b7ffb6a3fdafa1fbaba1fcaca1f69387ff55474f130f000000030101000000000000000000000000000000030101000000681b15ff5042f26459f66154f55347f65648fd6d49ff997affd9d0ffd3c7ffd5caffd5caffd5caffd4c8ffdbd2ff9679ff6532fe653af6422df2362aef3b2eff4739671c170000000401010000000000000000000000000000000401010000008b271fff4638ee392cf33a2ef54433fd663dff6834ffa389ffdbd3ffd9cfffd6cbffd5caffd5caffd4c9ffddd4fec0b2fb5f3ffe6c41ff7044f9583eef4335ff473a8b261f000000040101000000000000000000000000000000020101000000992a21ff483aef4135f64c3afd6a43ff693dfc6f50ffd7cbffdcd2ff9678ffa389ffdfd8ffc3b1ff7c52ffb7a3ffe9e0f98379f43a2cf8553dfb5d3ff4523aff493a9b2a21000000020101000000000000000000000000000000010000000000be342afe4638f34838fa5c3ffa5b3ff53d2cfba399ffddd2f87366fd5a2cff8964ffe3dafca394fe5e31fe6c3ffca091ffd8cdf55145f33b30f44437f24638ff493abe342a000000010000000000000000000000000000010000000000010000cd392dfd4538f24738f5483af2382df6685dfeccbff55d52f5422fff6f3ffc6f4fffdad1f9867cf74b31ff7444f6452ef88a80fba598f33e31f44336f14036fd4538cd392d010000000000010000000000000000000000010000010101110606dc3c30f94437f24236f64436f64133fd8c7ef95c4ef5382cfa593fff6c41f84b39fec7bcf97668f53a2cff6943fc5e3ff5382cfa6f60f85143f64133f24437f94437dc3c30110606010101010000000000000000000000030201000000150000f34337f74437f04235ec3f33e63d33e2372fdd322bd8342fee533ae94e39d22724f4a79ddf5650d32724e04134f0573cde3931e1312ae63b31ec3f34f04235f74437f343371500000000000302010000000000000000000000000509096a4341ea3d34d5342fce2e2bca2b29c92a28c82928c82a29ca2b29e04938ce312dc52121e0716bd03d3ac72524c92928d93f35ce312dc72929c92a29ca2b29ce2e2bd5342fea3d346a4441050909000000000000000000000000131111b4bab9ebc1c1c51c1ac7201fca2826cb2c2acc2e2ccc2e2ccb2d2bd0312dd23530ca2c2acc2c2acc302ecc2d2bcc2d2acb2c2acd2f2dce302dcc2e2ccc2e2ccb2c2aca2826c7201fc51b19ebc1c1b4bab91311110000000000000000002a3235dae2e5e4e1dfdda6a4d25c5acb3534c82423c81f1dc91f1dc9201eca2221ca2321ca2523ca2523ca2422ca2523ca2523ca2422c92220c9201ec91f1dc81f1dc82423cb3534d25c5adda6a4e4e1dfdae2e52a32350000000000000000003042498ab5c6adc4cdcde0e4dee5e5e2d2d1e0b3b1dc9492d97b78d36766d15957cf4d4cce4846cd4746ce4645cd4746ce4846cf4d4cd15957d36766d97b78dc9492e0b2b1e2d2d1dee5e6cee0e4adc3cd8ab5c6304249000000000000000000465d6588b7cb7aa8bc7faabd8db2c2a0c2ceb1d0d9c1dbe1cbe0e5d4e1e3d9e0e2dbdedfdbdadbdedbdbdfdcdcdfdbdbdcdadbdbdedfd9e0e2d4e1e3cbe0e5c1dbe1b1d0d9a0c2ce8db3c280abbd7aa8bc88b7cb465d6600000000000000000057748088b6c982adbe82aec07facbf7caabd7ba8bc7ca8bb7faabd83adbe87b0c18cb3c38db5c591b7c693b8c692b7c68eb5c58bb3c387b0c183adbe7faabd7ca8bb7ba8bc7caabd7facbf82aec082adbe88b6c95674800000000000000000006e94a484b2c57faabb81acbe83aec083afc084afc183afc082aec082aec081adbf80acbf7facbf7facbe7eacbe7eacbe7facbf80acbf81adbf82aec082aec083afc084afc183afc083aec081acbe7faabb84b2c56e95a40000000000000000004f6a758cbbcf89b8cb84b1c381acbe80acbe80acbe81adbe80acbe81acbe82aec083aec083aec083aec083aec083aec083aec083aec082aec081acbe80acbe81adbe80acbe80acbe81acbe84b1c389b8cb8cbbcf4f6a750000000000000000000000002c3a4060808e78a2b385b2c489b7ca89b7ca87b5c886b4c685b2c584b1c382aec082aec082aec082aec082aec082aec082aec084b1c385b2c586b4c687b5c889b7ca89b7ca85b2c478a2b360808e2c3a400000000000000000000000000000000000000000000d11132430353d525a4e6973628290698c9c6f94a4769eaf82adbf82aec082aec082aec082aec082aec082adbf769eaf6f94a4698c9c6282904e69733d525a2430350d1113000000000000000000000000000000" + +_PERSON_HD_HEX = "000000000000000000000000000000000000000000000000000000000000100b0a251a162e201b583d335b3f35271b17000000010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000261a166c4a3e5c3f35583c335b3e345f4237251a160000000101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004a332c5a3d374f352e593e3054392f573b3263443a120d0b0000000101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e3923cea030dbb529a27b2d3f273154382e5c3e341a120f000000010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000242002bc9332d6aa31c4982a926d2f5c40305a3d35110c0a00000001010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000068520ed5b22ed7ad2cffd626c1952e4f34334730270503030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005d4910f3c72efac926fecd27c3972d6646362b1e19000000010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100000000b48c23ffd82bfccb28ffe12a705b13000001040301000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100010100100d013c330add9a1cfd9e219758100101000202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000301000000002a1604ec721deb6521f97920311b050000000201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000302000101009a5812ff7d23e56821f97622502909000000030200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100000000bb6b16fd7d22e66522fc7225612f0b000000040200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c67017fc8020e7701eff871d783b0c0000000402000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101000000000b0601df7c18fa861af6ad11ffc40f7f4307000000040200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100000000140b02e88017f88014f6ac19ffd2238a5009000000030300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020100000000231404ef8117f57c11f6b01fffd92b99590b0000000302000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000302000000003d2106fd8313f28210fccb29fdbb22b04c010000000201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000302000000003f2206ff7e10f59c18fcd12aff8b03b24b000000000201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020000002b2729e6a431fbce24f0bb2bca79367458410000000201000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020000003f444be5b430fbc907dcb4387481904e5154000000010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101000000585a61b19b5fdeac22a18b576a6b714c4947000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060606727271797c82777a826c6e746362624d4e4e0001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001313137c7c7c7d7c7b807f7e6969686363635454540000000101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001f1f1f8282827e7e7e6e6e6d5c5c5c6060606262621918180000000101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001e1e1e7f7f7f8585853f3f3f3535356b6b6b6060605858580b0b0b0000000101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001414147979798686863939390505055353536666666464644d4d4d0303030101020101020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0c0c7575758484842a2a2a0000000707074545456666666767683f3f3f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009090974747583848420202000000000000000000035353566656464666827395e09101e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010205050573716d807e7c131313000000020202010101000000252422405784345caa2e4e8b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000203046168776d75820708080000000101010101020101010811233158a12f52930e182b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002233553c5b974366ac476bb4151f34000000010102010102000000243c6a385ea90e182b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000486cb5486cb44465a94a6fb91c2b46000000010203010102000000152440162645000000010203000000000000000000000000000000000000000000000000000000000000" + +_VEHICLE_HD_HEX = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000003b0a05911305c71906d51b08d91d09e51e0aed1e09ed1d09e71f0ade1d09d31a078e13062406040000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000001a0503ad1809ff250fe73427bc3e39a445469b494c8f4b519150559d4f5298474aa34545b83f3bed3223f5220c6b1109000000020000010000000000000000000000000000000000000000000000000000000000000000030000000000220604e21f0bfb2f1d7f51584e71855c8ea66ba4bc74acc57fb8cf6193aa4d799172a8bf6a9fb85d8fa64e6d80ac4441ff260e8a160b000000030100010000000000000000000000000000000000000000000000010000050100000000150504de1e0ae534245b5d6b6598b0a4d4eab5e6fdb8eaffb7e8ffbff5ff85a9be678195bef1ffb6e8ffb7e8fe99c8df4d7b919e4544ff270f7f140a000000020100030000000000000000000000000000020000030100000000000000180503d21d0ae6332452647676abc4b8e7fdb2e7ffaee2faade1f9acdff7b4ebfd7fa5bb638195b2e7fdade0f8ade1f9b6eaffa4d1e84e7b92aa3f3bff240c7d140a000000000000020000000000010000020000000000000000090101540e07df200df42e1b5964767899afc0daebade6feaee0f9afe3fbafe2faaee0f8b6ecfe80a6bc638195b3e8feade1f9afe2faacdff8b5e9ff9ecde456758acb352cff260fbd1d0e33090500000000000000000000000028070486150ad7210fff2813ff26107959625e7285f24031f02e1db0c7dbb2ecffb2e4fbb2e5fdb1e3fbbaf0ff80a5ba658195b8edffb2e6feb3e7ffb3e7ffb2e5fdb9e8fe5f95ae9a4648fc220bff2b17f8200b4e08010201010500008a1004f5210cff2914ff2813fa230edc31244c6f86cc3930ff2205e1140f755d715c8ea261849b60869c60859c62889d55788e52748a678fa5658da4668da4668da4668ba257839a6d606ef02917fa2713f31904ff5444ab887f000000931c11ff402bf83d2bf3220ff72612fa2712fa2912e43021d41d1ccd1a1cba1320d62c21d43425d33224d33224d43224d33224d63528d73629d33123d33124d33123d33123d33123d23427ef2d1aff2811f9200af74b3dfef3f2c7c7c02b2623edc6befbfbfbf79289fb1902f82916f32113f01d13f41910df1516cb0f1bde1517f41911f5170ff51710f51710f51710f51710f4170ff4160ff51810f51810f51810f71a10f71a10f7190ff11a12ec1d16e40d0fdb2631feefefcbc7c16d6c66fbfefefce1def73221e81310db0b18d60617d30518d20519d70517db0616d80517d30418d30519d30518d30518d30518d30518d30518d30518d30518d30518d30518d30518d30519d20619d30518d40417d5081bd20004ea5f67c8cdc6a88b83ff9084ee271edb0410d40519d50517dd0315e10215e00315da0516d50718d70617d80617d70617d70617d70617d70617d70617d70617d70617d70617d70617d80617d60717d90617de0315de0315da0516d60717d50618dd0314b55356c92613f50e05d30113d50619d90616dd0415bb1421a31e26a91c26c90e1cde0315d60617d70617d70617d70617d70617d70617d70617d70617d70617d70617d70617d60717dd0315d40919b91622b71722cd0c1bdf0315d50717dd0618b4000dbe150dde0a1bd30718d80616d70617802e30433f3743433b403f394e3e38a31e26de0315d50718d70617d70617d70617d70617d70617d70617d70617d70617d50717dd0315b715215c3935423c35423c354f3c36992329d90415dc0718b4051482040ee50618ce0716dd021493262b3b443c7a706cb1acaca39c9b58534d47413bbd121fda0415d30717d50617d50617d50617d50617d50617d50617d40617d80416c70e1c503d3853534d9a92919f9998635c573e433baa1b24e10315b105131d0103c60616eb0619e10719613533595650c9c7c7cdceced5d6d7a59d9c3e413999252be70215da0719dd0618dd0618dd0618dd0618db0617db0617d90718e502159e22293f433baaa2a1d5d6d6d2d3d4bdb8b84b4c45762f30f404189f061100000020020482040ea706145834315e5a54cccbccc9c9c8d1d2d2ada8a740413a872529c70211be0615c00514c00514c00514c00514c40615c50614c30715ce02138a252945453fb8b3b5cdcecdc7c7c7cac8c856554e652e2da50210270204010000000000000000000000322a2651453e85807dc0bfbfb9b7b86b635f4c413a18100f0700000801020700010700010700010700010d01010e00020f01020e00011f13134d413a77706dc2c2c2c8c8c8918c8a52453f2923200000000000000000000200000401010100000807064238324e423b574c475348424f433c2e2724000000000000000000000000000000000000000000000000000000000000000000000000322a274f433c5b504b60575150443e4237330606050200010200000000000000000000000100000000000605042924223b312d38302b1e1a1800000001000001000001000001000001000001000001000001000001000001000001000001010001000027211e3b322d3c322d312b26080707000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_ANIMAL_HD_HEX = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003101d030d18000000000101030e1911549d145ead061b330000000002030000000000000001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001020000000825441669c31568bf09284b0000000f49881974d61870cf135eaf01060b000000030f1c030d180000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001030002040001021258a41871d1186ecb1669c3030e1a135aa7186dcb1768c2186fce041221092b4f186ecc176bc6082443000000000102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010306145faf176cc81768c01972d2082340114e901972d31768c3176cc7061d351464b9176dcb1870d01357a10001020000010000000000000000000000000000000000000000000000000000000000000000000000000102040825450825440000000f45801975d81566bb1975d807213c061d361871d11977dc0f427d05172a186ecc1668bf186cc9145ba90002030000000000010000000000000000000000000000000000000000000000000000000000000000000e41791974d71974d70e498704111e1462b61976da1561b4020c1500000003101e071f3a0001030209111666bd1871d21a77dc0e427a000000000102000000000000000000000000000000000000000000000000000000000000000000000000135cac186cc91568c01976da082748020911092b4f02080f000000041526051527020b1500000100000007203b1359a50f458102080e000101000001000000000000000000000000000000000000000000000000000000000000000000000000104b8a1972d31566bb1971d10f447f000000000000061c331256a0176cc81870cf176dc9145aa70a2f570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000411201460b41974d61976da0a2e550000000a2d531871d01870cf176bc41769c3176bc6186fcd1974d8104c8b010508000203000101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030b150b345f09294b000001061b321870cf176bc71769c3176bc6176bc6176bc61769c31667c11975da0b325d00000000020300000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100000000000000000000010111559b1870cf1769c3176bc6176bc6176ac31768c31768c31666bf1870d1104b8b00000000010200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100030500010303101e176bc4176bc4176bc41769c3176bc61871d11973d41973d41971d21973d5092c51000000000202000000000000000000000000000000000101000203000103000000000000000000000000000000000000000000000000000102000000030f1c176dca176cc6176cc81972d3176cc71049880d3c6f0d3d710b335f05192e0000000000010000000000000000000000000000000000000000000000000000000000000000000001010002030001030000000000000000000000000001020000000e3e741870d01664b80f447f041527000000000000000000000000000000000000000000000000000102000203000203000102000000030f1d1047860b335e00010200010100000000000000000000000000000000000000000000000000000000000004122102091200000000000000010100020300020300020300010200000000000000000000000000000000000000000000020312549c1976da1976db0b3461000000030f1c0d447d0b315b00020300000000000000000000000000000000000000000000000000010200010200000000000000000000000000000000000000010100000005172a0e447e0b3662010407030e19176cc71668c0186ecc135ba701030612559f1a77db1974d70c396800000000000000000000000000000000000000010200010100000000000000000000000000000000000000000000000100000101060b1561b21975d81976da114d90020a131567be176ac5186cca135ba804101e1769c21668c2186cc9135aa9000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000204000103030f1e1769c31668c21667c11974d7071e380d3d701a78de1a78de0e407501060a1769c3176bc6186fcb1358a5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000145cac1871d11566bc1972d30b325e0103060d3b6d0d3c6e02070e0001010c3a6b1973d5186dca09294c000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000092a4d104a8907203c05182d1669c31974d6186ecc061a30000000000000000000000000000000000000061c350516280000000000000000000000000000000000000000000000000000000000000000000000000000000001020000000414251871d01972d31974d70a2e56020b150b315b05182d00000005172b0d3c6e104b8a0f49870b3a6a05172b000000000000000102000000000000000000000000000000000000000000000000000000000000000000000000000102000000061a30186fce1567be176cc7176ac5020a130000000000000e4179186fcd1973d41871d11871d21972d4186fcd0e468201060b000101000000000000000000000000000000000000000000000000000000000000000000000000000001000001010509145baa1872d31667c11871d0061d370000000e437d1976d91769c41768c2176ac3176ac31768c31768c31a77db0f4582000000000000000000000000000000000000000000000000000000000000000000000000000000000000000102000000051527135eae1870d0135eaf02070d05182d1871d11769c2176bc4176bc6176bc6176bc4176ac41769c31768c3176bc7041425000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101000000010406061a30020a120000000d3d731972d31768c3176bc6176bc5176ac3176cc8186ecc186dcb1870d01769c2030f1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001020000000000000000000000010f48851870d01769c21769c3176cc81972d31667bf135daf145daf1460b40a2b510000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001020002040000000b35621a76da176bc51870cf1769c50c3665030c1701030501030500010200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000101060b0f47831561b610519405192e000000000000000000000000000000000102000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_MOTION_HD_HEX = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010000000000000000000000000203030000000000000000000000000000000000002c3e486691a98cb8d1a1c6daa7c0cc919fa45d676b11131400000002020200000000000000000000000000000000000000000000000001010100000000000001020200000000000000000000000007090a2d3234353a3c1e21230001014e6f829cd6f7c3e8fbd6eef9daf0f9dcf2fcdef5ffe1f7ffc2d6dc3a4348000000020202000000000000000000000000000000000000000000000000070a0b06090a000000000101010101000000436070acccdcdef3fddef6ffd4e7f09ebac899d2f2c7e5f3d8ebf2d3e9f3d4eaf4d2e8f2d2e8f2d2e7f0d7f1fc9dd3f11d29300000000102030000000000000000000000000202030000002f414b88beda86bcd9394f5b0000000000004d7084baecffdcf1fad2e8f3d2e8f2d7ecf7d8f2fecee7f2d8ecf5d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d6ecf5cbe2ee9edafa648ba10101010202030000000000000000000000000203040000004f6e7fa2e1ff95cff092caea0a0e10161f2399d2f2cce5f1d5e9f2d4ebf5d6ebf5d7ebf4d7eaf3d8ecf5d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d6ecf5cde6f298d0ef86bbd80305070000000203030000000000000000000101010101010d12157fb1ce92cdec94ceef11171a2f4450abe0fed4e7f0d4ebf5d6ebf5d3eaf5afd9efa4d4eec6e4f3d8ecf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d9edf6bfe0f190caeb90c8e7323b400000000000000102020000000000000101010303030000006993aaa0deff82b4d0030304364e5ba9dffcd4e7f1d4ebf5d9edf6c0e1f28bc8ea8cc8ea9bd0edd5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d9edf6bcdff18ac7eaa0d1ecd4ebf49dadb41e2123000000010202000000000000000000384e5a9fddff91cbea2b3b450000002e414b9cd7f8cce4f0d6ecf5d7ecf5c9e6f393cceb92cbeb94ccebcee8f4d7ecf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5c5e3f3d0e9f4d4eaf3e4fcffc2d6dd161819010101010101161e2361869a8ec5e5648ba11a23290000000102020d121487bedfb7e0f5daecf4d3eaf5d8edf5a7d5ee8dc8ea9acfecd3eaf5d6ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d8edf5d6ebf5d5ebf5cee4ede6feff8c9aa10000000102021e29303347500d1214000000000000000000000000000000435d6c9cd8fabedeeed9edf6daedf6a8d6ee88c6eab7ddf1d9edf6d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d1e5f0212426000000000000000000000000040507070b0c0d12150f161a11181c141c20709eb896d2f4a3d1eba4d4ee8cc8eaacd8efd7ecf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d2e7f1e0f6ff60696e28363e719ab17aa6bf7aa7c080adc884b5d18dc2e091c5e491c5e48ec5e58dc3e393ccec8fc9ea8cc8eab3dbf0d9edf6d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d2e8f2dff6ff87969c6fa3c094dcff8bcdf28ccff48bcef28accf188caed87c9ed88c9ed93cded93cded93cbeb90caeba0d2edd8edf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d2e8f2ddf4feaab9c00d3f5724a7e825a4e326a5e326a6e426a5e428a5e524a4e533abe68dc9ea91caeb92cbeb8dc9eab1daf0daedf6d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d2e8f2ddf3fe99b4c20000005b7889a1dbfc95cae797cdeb97cdeb97cdeb96cceb99cdeb9cceeb9bceeb98cdeb8cc9eab3dbf0daedf6d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d4e9f2d5f1fe5b97b2001d2e0f8ac626a9eb22a3e423a6e623a6e623a6e623a6e622a5e623a6e61fa2e65ebae996cceba5d4eed9edf6d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d3eaf5d5ebf5deecf298dbf934677e4792b953bff649aee14ab1e44ab1e44bb1e44bb1e44bb2e44bb2e54cb3e54db3e879c3ea97cceb94ccebcde7f4d8ecf5d4eaf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d8ecf5ddeef6d4eaf595d2ec60c1eb1b2b32648394ade3ffa7dcfba7dcfba7dcfba6dbfaa5daf8a5daf8a4d7f5a3d7f59ed0ec97cceb8ec9ea8ec9eaa4d3eed4ebf5d9edf6d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5daedf6cae7f49ad5ef6cbee362ccfb437e97010000070b0d425f6f587e93597e935d839960879d6690a76891aa75a4bf77a6c28dc6e793ccecaad7ee96cdec8ac7ea9dd0edc4e3f3d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4eaf5daedf6b5dff165c1e957bae465cdfa53a1c4070e10010101000000000000000000000000000000000000000000000000000000171f248cc6e8a7d5eeddeff6c8e5f3a5d4ee94cceb92cbebabd7efd3eaf5d5ebf5d5ebf5d5ebf5d5ebf5d3eaf5ddeef69bd4ee53b8e465c2eb488da9091216000000010102010101122229468ba8386c831c36420000000000000405060001012e3f4996d5f8b6daedd9edf6d7ecf5d8ecf5d0e9f4cee8f4d0e9f4d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d6ebf5d3eaf4b6dff1bce0f21c1c1c0000000203030000000101020305072f5c716ad1fe67caf64588a5060c0f0203030000002a3a4295d3f6bcddeed9edf6d3ebf5d4ebf5d6ecf5d7ecf5d6ecf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d6ebf5d8eaf2e4f9fe41484c0101010303030000000000000001010000002c586b65c5f069cffb4284a000000000000010161987c1e1b8def1d8ecf4d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d5ebf5d1e7f1dff7fe41484c00000003030300000000000001010101020204080a5ab0d65fbde664c6f0182e37000000000000628ba4afe1fdd6e8f0d4ebf5d5ebf5d5ebf5d5ebf5d5ebf5d7ecf5d9edf5d4ebf5d4eaf5d5ebf5d5ebf5d5ebf5d5eaf4d4eaf5d4e9f41f21230000000202020000000000000000000103030000003c77926ad0fc6ad0fd2a52630000000101011b262c92cdeec8e8f8d7e8f0d1e8f2d3e9f3d3e9f3d4e9f2cbe5f0c3e4f3d9edf5dbedf6d4ebf5d3eaf5d3ebf5d0e5efe1f8ffa8b9c10202020101010000000000000000000000000101020001010a13174c95b654a6ca1020270000000203040000002e424d95cae7d4f5ffe1f6ffdbf1fbd9f0fbe2f7ff89d7f95cbee880c9e7bbdfefd7eaf3dbebf3daeaf1def4fdd7f1f9323739000000020202000000000000000000000000000000000000000000000000010202000000000000000000010202000000131b2155768892b0c2bcd2dec2d7e0afbfc56697ae5abbe562c9f562c5f07ccef290d6f597dcfb7bbbd72c404a0000000102020000000000000000000000000000000000000000000101010000000000000101010000000000000000000202030000000000000000000a0c0e0d0e0f000001000000172d37396e854c93b44998bb4890b12d5d72020d12000000020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_WARNING_HD_HEX = "0000000000000000000000000000000000000000000000000000000000000000000000000201000000001f1601a57201bb8000362500000000020100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020100020100be8100ffb200ffae00e09900130d00010100010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040300000000564009ffcd24f5be24eda90affb1007c5600000000030200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000010101040301d3a829fed536fdcc36fbc52cf5a802e39b00140e0001010001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003030100000059460fffd936fcc92fffcb30ffcf35f2b214ffb0007c5501000000030200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100040300d3a829fef269fbd849ffc92effcd34fbc52bf5a802e39a00140e0001010002010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004030100000058440cffea52fcf37afddd51fec92effcc32fdcd35f2b214ffaf007c5601000000030200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010100040300d3ab2dfef66ffbec6dffd638ffcf2fffce31ffd433fbc52bf5a801e39b00140e0001010002010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004030100000058440cffec55faf178ffe553cca42f675d3d5d553db79634ffd435f1b013ffaf007c5601000000030200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000020101040200d3ac2efef670fdee6df7cb38534c3b3034413739403c3c3ee2b835feca2bf4a702e39b00140e0001010001010000000000000000000000000000000000000000000000000000000000000000000000000000000000000003030100000058440cffed57faf178ffe252edbc2e4a473d3d3e3e42413e3a3b3ed0a934ffd635f0b014ffb0007c5501000000030300000000000000000000000000000000000000000000000000000000000000000000000000000000010000010101040300d3ac2ffef871fbec6bffd036f6c531524c3a37383b3d3c3b3c3c3bdeb433ffd234f9c32bf5a802e39a00140e0001010001010000000000000000000000000000000000000000000000000000000000000000000000000004030100000058440cffef5afcf278ffdd4fffca2efdcb3358503835363a3b3a3a413f3ae9bc33ffd031ffce35f2b114ffaf007c5501000000030200000000000000000000000000000000000000000000000000000000000000000000000000010101040200d3ad30fef873fdeb6affce36fecb31ffd032625738313338373738494537f2c232ffce32fecd34fbc52bf5a802e39b00140e0001010001010000000000000000000000000000000000000000000000000000000000000003030100000059440cffef5bfcf278ffdc4dffc92efdcb33ffd2326f60352e3138343538504a37f9c732ffcd32fecb31ffcf35f2b114ffaf007c5501000000030200000000000000000000000000000000000000000000000000000000000100010101050300d3ae34fef973fbeb69ffce34ffcb31fccb32ffd4327867332a2d35303135574e34fecd32fecc32ffcc32ffcd34fbc52bf5a902e39a00140e0001010001010000000000000000000000000000000000000000000000000003030100000059440dffec55fcf47bffda4affca2effcd33fcc932ffd632857133282b342e2f34615634ffd032fecb32ffcc32ffcb31ffcf35f2b114ffaf007c5601000000030200000000000000000000000000000000000000000000010000010100050401d3a726fed73cfdd646ffcc32ffcc32ffcc32fcca32ffd732947c33272a332b2e336e6033ffd232fdcb32ffcc32ffcc32ffcd34fcc52bf5a802e39b00140e0001010001010000000000000000000000000000000000000004030100000059450effe549fcea6bffd13bffca2fffcc33ffcc32fcca32ffd732a185312528302628307b6831ffd532fcca32ffcc32ffcc32ffcb31ffcf35f2b113ffb0007c5601000000030200000000000000000000000000000000010000010100040401d3a726fedd44fdeb69ffd43fffcb2fffcc33ffcc32fcca32ffd632ae8f3125282f23262f887230ffd832fcc932ffcc32ffcc32ffcc32ffcd33fbc52bf5a801e39c00140e00010100010100000000000000000000000000030301000000594711ffd935fcc82effcb30ffcb31ffcc32ffcc32ffcc32fcca32ffd632bd9a3125272d21242d967c31ffd732fcc932ffcc32ffcc32ffcc32ffcb31ffcf35f2b214ffaf007c5501000000030300000000000000000000010000010100040401d3a829fed334fbc932ffcc32ffcc32ffcc32ffcc32ffcc32fdca32ffd432c9a3312a2b2c22252ca48830ffd732fcca32ffcc32ffcc32ffcc32ffcc32ffcd34fbc52bf5a801e39b00140e00010100020100000000000000040301000000594712ffd935fcc930ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32fdca32ffd332d4ab3121232a13182ab19130ffd732fcca32ffcc32ffcc32ffcc32ffcc32ffcb31ffce35f2b214ffb0007c5501000000030200000000000000020100040301d3a829fed334fbc932ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32fecc32fecd32f6c832705f2c574c2ce4b932ffd032fecb32ffcc32ffcc32ffcc32ffcc32ffcc32ffcd34fbc52bf5a802e39b00140e000101000101000403010000005a4811ffd935fcc930ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32fecb32ffd332f8c733f1c132ffd533ffcb32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcb31ffcf35f2b114ffaf007d5601000000030200020200040301d4aa29fed334fbc932ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32fdcb32ffd232d2ab30383429292927b49430ffd433fdca32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcd34fbc52bf5a902e49b00140e00010100000000574611ffda35fcc930ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32fcc932ffd9338c742d0e142314182361522affd633fcca32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcb31ffcf35f2b114ffb0007a5301000000040301d0a529fed434fbc931ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32fdcb32ffd232d4ad30383427282826b7962fffd433fdca32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcd34fbc52bf6a901de9800150e00624e13ffd534fdc931ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcb32ffd232f0c132eabc31ffd232ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcc32ffcb31ffce35f4b314fead00855c01cda428fcd233fbc830fdc932fdc932fdc932fdc932fdc932fdc932fdc932fdc932fdc932fdc932fdca32fbc832fecc32fecd32fbc832fdca32fdc932fdc932fdc932fdc932fdc932fdc932fdc932fdc932fdc931fdcb34f8c127f5a800d492019d7c1dffde37ffd133ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd234ffd234ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd334ffd333ffd637ffc320ffb2009d6c01100d039b7d1ecaa629c9a027caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327caa327c89b21c78a05936400100b01" + +_CAMERA_HD_HEX = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000203031c1f2172878f718289717f88738289202425000000253238475d681c262a000000040405030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030304020203000000343b3fa3dfef93b6c191a3ada0b7c22b3235070808687e88748f9b596b75010202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000484e5289979ea8c5cfa2b9c1a2b2b9a8b8c07e898e8b989eaebfc7a7b4bcadbec686919870797e717a7f71797f717b8071798071798071798071797f717b80727b7f70797d727b80747e82727d82727d82727d82747e8270797d7782874c5254b8cdd5c1e8eeb1bdc3b2bfc5b2c0c7b1bfc6b9c8cfb7c5cdafbdc3b2bfc6afbdc4b8c7cebccbd2bdccd3b7c7cfafbec3b1bfc3b1bfc3b1bfc4b1c0c5afbec3b4c4cbbfced6adbac0a3abafa8b0b4a7afb3a7afb2a3adb2b6c6ceccf4fb95a6adb1c7ceb6dde3b0bfc7afbec5afbec5b0bec5aebcc3aebcc2b0bec5afbec5b0bec5aebcc2acbac2afbdc59faeb4e0dbcdf2eee1f0e8dbf0e3ccf2e1c5e9dcc1a3b0b4afbfc8878b8d4d4a4a3939383c3b3b3c3a3866737a9fb2bbbee5eb899ba1b2c8cfb8e0e6b0bfc6b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b2c0c8a4b2b8f5eddefffff8fffcedfff5dafff1cefeecccaab6b9b2c1ca8f93953d3c3d1718191d1e1e1d1c1c687983a5b7bfc1e8ee8b9ea2b2c8cfb8e0e6b0bfc6b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b0bec5b2bfc7a5b4bbbbc0bac6cac3c6c9c2c6c9c0c7c7bebfc1b9a4b2b8b3c2ca8c90913e3c3b1c1c1c22212121201f667780a4b6bec1e8ee8b9ea2b1c8cfb8e0e6afbec5afbdc4afbdc4afbdc4afbdc4afbdc4afbdc4afbdc4afbdc4afbdc4b0bec5b0bec5b5c4cbafc0caa7b6bfa0afb79faeb7a5b5bfaebfc9b4c3cbb3c2c990999e717f866f80886f7f876e7f877a8f9aa4b4bcc0e9ef8b9ea2b6cbd2bbdfe6b4c3cab4c2cab4c2cab4c2cab4c2cab4c2cab4c2cab4c2cab4c2c9b5c3cab3c1c8b2c0c796a0a5757a7b70717177777778787871727274777790989cb0bec4b1c1c9b4c4cdb5c5cdb5c5cdb5c5cdb3c1c9b3c0c7c2e7ed8fa0a53c3f413c3f413e41423e41433e41433e41433e41433e41433e41433e41433f42433c3f4046494b5e60617575759898989b9d9d8e8f8f8c8d8d999b9b9b9c9c7b7b7a6163644b4e503b3e3f3f42433e41423e41423f42433e3f413e41432f32332322222323221b1b1a1a1a191b1a1a1a1a1a1a1a1a1a1a1a1a1a191b1b1a1818172423234e4d4d9797979395964140400b0705000000000000060100333131878889a1a1a25858582929291817171b1a1a1a1a191a19191d1d1c2525241b1a1a606161636363313132202020232324222323222323222323232323222222262727525252a6a7a76c6d6e0000000000000c2b321b49531d4b5615363e000507000000535454aeaeae5f5f5f2c2c2d2020202424241f1f1f3f3f3f6969694949496262626464643232321e1e1e2222222121212121212121212222221e1e1e3c3c3c9494947677770000000c21264182917bc0d187cadb3f8b9d307e9131788918363d000000565757a2a3a34647472020202222221d1d1d4141416969694c4c4c6161616363633232321e1e1e222222212121212121212121202020252525626363a4a5a508070708171b438b9c97d5e59dd8e681c0cf3b82932c7586307a8b338194162f350000009394957676762b2b2b2020201d1d1d4040406969694b4b4b6161616363633232321e1e1e2222222121212121212222221e1e1e2f2f2f8e8e8e686869000000215a677bbfd094d0df438697215f6e24616f2c70802e7788307b8c2f717f0002024746469d9e9e3838381f1f1f1e1e1e4040406969694b4b4b6161616363633232321e1e1e2222222121212121212222221e1e1e3a393a9ea0a03b393901050731798982c1d14c90a01a53612459662359662359662d72822f7889327e9005191d1c1918a4a6a64949492020201d1d1d4040406969694b4b4b6161616363633232321e1e1e2222222121212121212222221f1f1f404040a3a5a5292524021318327c8e367f902a6d7d235966235b68235b692258652969782f7889338092102d340f0a09a0a1a14f4f4f2020201d1d1d4040406969694b4b4b6161616363633232321e1e1e2222222121212121212222221f1f1f3c3c3ca1a2a234313100090b317a8b2e77892d73842359662359662359662258642b6f7f2f7889337f91092126171211a3a4a44c4c4c2020201d1d1d4040406969694b4b4b6161616363633232321e1e1e2222222121212121212222221e1e1e3232329495955959590000002c6b79317c8d2f78892b6d7c245d6b245c6a2968772f78892f7989307787010607393837a1a2a23d3d3e2020201e1e1e4040406969694b4b4b6161616363633232321e1e1e2222222121212121212222221f1f1f2828286f6f6f969798000000152e343382952e77882f78892e76872e76862f78892e76863484961d434c0000008081828484842d2e2d1f1f1f1e1e1e4040406969694b4b4b6161616363633232321e1e1e222222212121212121212121222222202020444444a1a2a25253540000001d424b327e90327f91317d8e317d8e327e9033819323515c000000333434aaabab5050502223232222221d1d1d4040406969694b4b4b6161616363633232321e1e1e2222222121212121212121212222221f1f1f2a2b2b5f5f5fadadae414242000000081b1f22535e2966742a67752458640d252b000000282929aaacac6e6e6e3131311e1e1e2323231d1d1d4040406969694b4b4b6565656767673434341f1f1f2324242222232222232222232222222323241f2020343535686868acadae7172731511110000000000000000000000000c08075d5e5eacadae7576763b3c3c2020202323232323241e1e1e4343436d6d6d4d4d4d4f4d4d52504f2625251817171b1a1a1a19191a19191a19191a19191a19191b1a1a171716292929504f4f8b8b8ba0a1a18587877070716f6f6f7f81819d9f9f9393925a59582f2f2e1818171a19191a19191b1a1a1616153332325855553c3a3a8ca1a689a0a5848d92859095858f94859095859095859095859095859095858f95869095828c918d979c8187896d6e6f7777778181818282827a7a7a6d6e6f7b80838e989c828c92869095869095858f95859095858f95848e9390abaf6e7a7ea3bdc4c0dee4b6c4cbb9c8cfb9c8cfb9c8cfb9c8cfb9c8cfb9c8cfb9c8cfb9c8cfb9c7cfbac8d0b7c6ceb7c6ceb0bcc39da8ad919ba08e9aa09aa5abacb9c0b7c6cdb7c6cdbac8d0b9c8cfb9c8cfb9c8cfb9c8cfb9c8cfb7c5ccc5e7ee768489000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +_INFO_HD_HEX = "1216185f7680728e9b6f8b966e8b976d8a966d8a966e8a966d8a966e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8b976e8996486d7a264752060a0b67808c97bdcf90b4c490b4c591b5c695b8c895b9ca94b9ca94b9c992b6c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c691b5c690b5c594b7c885acbc4a82942c505b80a0ae8db0c08aadbc95bac79dc3d1a0c5d39fc6d3a0c7d5a2c9d695bac889acbc8baebe8baebd8caebe8caebe8baebe8dafbe8baebe8baebd8baebe8baebe8baebe8baebe8baebe8baebe8baebe8baebe8baebe8cadbd89adbd4c80913664717e9cab8db0c191b5c4a2cbd8a1c9d7a0c8d6a1cad89ec6d495bbca8db1c18cafbf8cafbf8db0c087acbc86abbc89adbd85aabb8baebf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819234606e7f9ead8db0c197bccba1cad8a0c8d6a0c8d696bbca8db0c08aadbd8caebf8cafbf8db0c087abbca6c1cedae6ebe5edf1c7d8e08db0c08baebe8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9ead8eb1c198bfcda1cad7a0c7d592b5c48aadbd8cafbf8cafbf8cafbf8db0bf88acbd9ebcc9fbfdfdffffffffffffffffffd5e2e887acbc8dafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9ead8eb1c199bfcea2cbd995bbca8aacbd8cafbf8cafbf8cafbf8cafbf8eb0c084a9bacbdbe2fffffffbfdfdfefefefdfefefdfdfe9ab9c789acbd8db0bf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9ead8eb1c199c0cea0c8d78eb1c18cafbf8cafbf8cafbf8cafbf8cafbf8eb0c084a9baccdce3fffffffcfdfdfefffffdfefefefefe9cbac888acbd8db0bf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9ead8db1c19ac1d09ec4d28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8db0c087acbca4c0ccffffffffffffffffffffffffdfe9ed89adbd8dafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9ead8db1c190b4c491b5c58baebe8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8db0c087acbcafc7d3e6eef1eff4f6d5e2e895b5c489adbd8dafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28aacbc8baebe8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8db0c081a7b983a8ba88acbd7fa6b887acbc8db0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8baebea7c2ceb1c9d3adc6d2b1c9d498b7c68aadbe8dafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe91b3c2f6f9faffffffffffffffffffc6d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fefefefcfcfdfefefec7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f5f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe93b4c3f4f8f9fffffffdfdfeffffffc7d9e084a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8eb2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8aaebe94b4c3f7fafafffffffefeffffffffc9dae184a9bb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7f9eab8db2c28badbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8baebe8fb2c1eaf1f3fafbfcf8f9fbfafbfcbed2db84aabb8eb0c08cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8dafbf89acbc4d819235606e7d9caa90b4c48aadbd8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8baebe92b3c296b6c496b6c496b6c48eb0c08bafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8cafbf8eb1c189acbc4a7f9035616f57808f8aaebe8dafbf8db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08cafbf8baebe8baebf8baebe8db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08db0c08dafc06590a0417787396473315c6a4d82944e7f904e7f904e80904e80904e80904e80904e80904e80904e80904e80904e80904f80914f80914f80914f80914f80904e7f904e80904e80904e80904e80904e80904e80904e80904e7f904f80914a7c8d3e7183467d90325d6a111e223865744075853f73833f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f74853f73854074844479893865730d181b" + + +def _hex_to_image(hex_data: str, size: int = 16) -> Image.Image: + """Convert hex-encoded RGB data to a PIL Image.""" + return Image.frombytes("RGB", (size, size), bytes.fromhex(hex_data)) + + +_ICON_IMAGES_SD: Dict[str, Image.Image] = { + "beacon": _hex_to_image(_BEACON_HEX, 16), + "person": _hex_to_image(_PERSON_HEX, 16), + "vehicle": _hex_to_image(_VEHICLE_HEX, 16), + "animal": _hex_to_image(_ANIMAL_HEX, 16), + "motion": _hex_to_image(_MOTION_HEX, 16), + "warning": _hex_to_image(_WARNING_HEX, 16), + "camera": _hex_to_image(_CAMERA_HEX, 16), + "info": _hex_to_image(_INFO_HEX, 16), +} + +_ICON_IMAGES_HD: Dict[str, Image.Image] = { + "beacon": _hex_to_image(_BEACON_HD_HEX, 32), + "person": _hex_to_image(_PERSON_HD_HEX, 32), + "vehicle": _hex_to_image(_VEHICLE_HD_HEX, 32), + "animal": _hex_to_image(_ANIMAL_HD_HEX, 32), + "motion": _hex_to_image(_MOTION_HD_HEX, 32), + "warning": _hex_to_image(_WARNING_HD_HEX, 32), + "camera": _hex_to_image(_CAMERA_HD_HEX, 32), + "info": _hex_to_image(_INFO_HD_HEX, 32), +} + + +def get_alert_icon(name: str, hd: bool = False) -> Optional[Image.Image]: + """Get an alert icon by name. + + Args: + name: Icon name ("beacon", "person", "vehicle", "animal", + "motion", "warning", "camera", "info"). + hd: If True, return 32x32 HD icon. Otherwise 16x16 SD. + + Returns: + PIL Image (16x16 or 32x32 RGB), or None if not found. + """ + icons = _ICON_IMAGES_HD if hd else _ICON_IMAGES_SD + img = icons.get(name) + return img.copy() if img else None diff --git a/zeclock/plugins/helpers.py b/zeclock/plugins/helpers.py index 6c2cf31..7b1be11 100644 --- a/zeclock/plugins/helpers.py +++ b/zeclock/plugins/helpers.py @@ -578,12 +578,14 @@ def render_alert( font_name: Optional[str] = None, border_width: int = 3, blink_interval_frames: int = 5, + icon: Optional[str] = None, ) -> Image.Image: """Render an alert frame with blinking border and auto-wrapped text. Draws a blinking colored border around the display, with the alert text centered inside. Text is automatically word-wrapped to fit - within the bordered area. + within the bordered area. An optional icon is displayed to the left + of the text. Args: text: Alert message to display (auto-wrapped to fit). @@ -593,6 +595,8 @@ def render_alert( font_name: Font to use (default: plugin helpers default font). border_width: Border thickness in pixels (default: 3). blink_interval_frames: Toggle border every N frames (default: 5). + icon: Optional alert icon name (e.g. "beacon", "person", "vehicle", + "animal", "motion", "warning", "camera"). Displayed left of text. Returns: PIL Image in RGB mode with the alert rendered. @@ -621,23 +625,123 @@ def render_alert( if inner_w <= 0 or inner_h <= 0: return frame - # Word-wrap text to fit inner width - font_name = self._resolve_font_name(font_name) - wrapped_text = self._word_wrap(text, inner_w, font_name) + # Try to load icon if requested + icon_img = None + icon_size = 0 + if icon: + icon_img = self._get_alert_icon(icon) + if icon_img: + icon_size = icon_img.width + + # Calculate text area (reduced by icon width + gap) + icon_gap = 2 if icon_img else 0 + text_area_x = inner_x + icon_size + icon_gap + text_area_w = inner_w - icon_size - icon_gap + + if text_area_w <= 0: + text_area_w = inner_w + text_area_x = inner_x + icon_img = None # No room for icon + + # Word-wrap text to fit text area width (uppercase for DMD fonts) + # Use MENU font for alerts (smaller, fits better with border+icon) + alert_font = "MENU" + wrapped_text = self._word_wrap(text.upper(), text_area_w, alert_font) + + # Render wrapped text centered in the text area + # Create a sub-frame for the text area + text_frame = Image.new("RGB", (self.width, self.height), (0, 0, 0)) + font = self._get_font(alert_font) + if font and wrapped_text: + lines = wrapped_text.split("\n") + char_h = font.char_height + num_lines = min(len(lines), max(1, inner_h // char_h)) + lines = lines[:num_lines] + + # Vertical centering within inner area + total_text_h = char_h * num_lines + line_gap = 0 + if num_lines > 1: + remaining = inner_h - total_text_h + line_gap = min(remaining // (num_lines + 1), char_h // 2) + total_h = total_text_h + line_gap * (num_lines - 1) + y_start = inner_y + max(0, (inner_h - total_h) // 2) + + rgb_data = bytearray(self.width * self.height * 3) + for line_idx, line in enumerate(lines): + if not line: + continue + # Render text left-aligned in a buffer sized to the text width + line_width = font.get_text_width(line) + buf_width = max(line_width, 1) + grayscale = font.render_text(line, buf_width, char_h) + gray_data = grayscale.tobytes() + line_y = y_start + line_idx * (char_h + line_gap) - # Render wrapped text centered in the inner area - text_frame = self.render_text( - wrapped_text, color=text_color, centered=True, font_name=font_name - ) + # Horizontal centering within text area + line_x = text_area_x + max(0, (text_area_w - line_width) // 2) + + for row in range(grayscale.height): + fy = line_y + row + if fy >= self.height or fy < 0: + continue + for col in range(min(grayscale.width, self.width)): + pixel = gray_data[row * grayscale.width + col] + if pixel > 0: + px = line_x + col + if 0 <= px < self.width: + offset = (fy * self.width + px) * 3 + rgb_data[offset] = (text_color[0] * pixel) // 255 + rgb_data[offset + 1] = (text_color[1] * pixel) // 255 + rgb_data[offset + 2] = (text_color[2] * pixel) // 255 + + text_frame = Image.frombytes( + "RGB", (self.width, self.height), bytes(rgb_data) + ) - # Crop the inner region from the text frame and paste into alert frame - text_crop = text_frame.crop( - (inner_x, inner_y, inner_x + inner_w, inner_y + inner_h) - ) - frame.paste(text_crop, (inner_x, inner_y)) + # Composite text onto the bordered frame + # Use text pixels as mask (non-black = text) + from PIL import ImageChops + + r, g, b = text_frame.split() + mask = ImageChops.lighter(ImageChops.lighter(r, g), b) + frame = Image.composite(text_frame, frame, mask) + + # Paste icon vertically centered in the inner area, left side + if icon_img: + icon_y = inner_y + max(0, (inner_h - icon_img.height) // 2) + icon_x = inner_x + # Paste icon onto frame using non-black as mask + from PIL import ImageChops as _IC + + r_ch, g_ch, b_ch = icon_img.split() + icon_mask = _IC.lighter(_IC.lighter(r_ch, g_ch), b_ch) + # Create a temporary frame-sized image with the icon positioned + icon_canvas = Image.new("RGB", (self.width, self.height), (0, 0, 0)) + icon_canvas.paste(icon_img, (icon_x, icon_y)) + mask_canvas = Image.new("L", (self.width, self.height), 0) + mask_canvas.paste(icon_mask, (icon_x, icon_y)) + frame = Image.composite(icon_canvas, frame, mask_canvas) return frame + def _get_alert_icon(self, name: str) -> Optional[Image.Image]: + """Get an alert icon image by name. + + Args: + name: Icon name (e.g. "beacon", "person", "vehicle"). + + Returns: + PIL Image or None if the icon module is not available. + """ + try: + from .alert_icons import get_alert_icon + + hd = self._font_scale > 1 + return get_alert_icon(name, hd=hd) + except ImportError: + return None + def _word_wrap(self, text: str, max_width: int, font_name: str) -> str: """Wrap text to fit within a given pixel width. diff --git a/zeclock/plugins/reolink_alert_plugin.py b/zeclock/plugins/reolink_alert_plugin.py index ac60d71..99dce66 100644 --- a/zeclock/plugins/reolink_alert_plugin.py +++ b/zeclock/plugins/reolink_alert_plugin.py @@ -81,12 +81,12 @@ class ReolinkAlertPlugin(ClockPlugin): """Reolink camera intrusion alert plugin. Connects to a Reolink camera using the Baichuan protocol for - real-time push event detection. Displays blinking alert messages - on the DMD when person, vehicle, animal, or motion is detected. + real-time push event detection. When a person, vehicle, animal, + or motion is detected, triggers a rich alert via /api/alert + (blinking border + icon + wrapped text). - This plugin is non-rotatable — it only activates when an alert - is triggered and deactivates after the alert duration expires. - The alert is displayed immediately via the REST API text overlay. + This plugin is non-rotatable — it runs as a background listener + and never participates in display rotation. """ def __init__(self) -> None: @@ -94,11 +94,6 @@ def __init__(self) -> None: self._helpers: Any = None self._host: Any = None # reolink_aio Host instance self._listener_task: Optional[asyncio.Task] = None - self._alert_active: bool = False - self._alert_type: str = "motion" - self._alert_start_time: float = 0.0 - self._alert_duration: int = DEFAULT_ALERT_DURATION - self._frame_count: int = 0 self._camera_host: str = "" self._camera_user: str = "" self._camera_password: str = "" @@ -106,6 +101,7 @@ def __init__(self) -> None: self._connected: bool = False self._rest_port: int = 8080 self._language: str = "en" + self._alert_duration: int = DEFAULT_ALERT_DURATION # Keep track of last alert to avoid re-triggering too fast self._last_alert_time: float = 0.0 self._cooldown_seconds: int = 10 @@ -241,17 +237,9 @@ async def initialize(self, config: dict) -> None: except (ValueError, TypeError): self._rest_port = 8080 - # Cancel any existing listener task before starting a new one - if self._listener_task and not self._listener_task.done(): - self._connected = False - self._listener_task.cancel() - try: - await self._listener_task - except asyncio.CancelledError: - pass - - # Start background connection and event listener - self._listener_task = asyncio.create_task(self._run_listener()) + # Start background connection and event listener (only if not already running) + if self._listener_task is None or self._listener_task.done(): + self._listener_task = asyncio.create_task(self._run_listener()) async def _run_listener(self) -> None: """Background task: connect to camera and listen for events.""" @@ -371,11 +359,7 @@ def _on_event(self) -> None: return logger.info("Reolink alert triggered: %s detected", detected_type) - self._alert_active = True - self._alert_type = detected_type - self._alert_start_time = now self._last_alert_time = now - self._frame_count = 0 # Schedule async alert trigger on the running event loop try: @@ -385,69 +369,47 @@ def _on_event(self) -> None: logger.debug("No running event loop for alert trigger") async def _trigger_alert_display(self, detected_type: str) -> None: - """Trigger the text overlay display via the local REST API. + """Trigger a rich alert display via the /api/alert endpoint. - This sends an HTTP request to the zeClock REST API to display - the alert text immediately, regardless of which plugin is active. + Sends an HTTP request to display the alert with blinking border, + detection-specific icon, and localized text. """ text = self._get_message(detected_type).replace("\n", " ") + icon_map = { + "person": "person", + "vehicle": "vehicle", + "dog_cat": "animal", + "motion": "motion", + } + icon = icon_map.get(detected_type, "beacon") + color = list(_BORDER_COLORS.get(detected_type, (255, 0, 0))) try: - url = f"http://127.0.0.1:{self._rest_port}/api/text" + url = f"http://127.0.0.1:{self._rest_port}/api/alert" async with aiohttp.ClientSession() as session: async with session.post( url, - json={"text": text, "duration": self._alert_duration}, + json={ + "text": text, + "duration": self._alert_duration, + "icon": icon, + "color": color, + }, timeout=aiohttp.ClientTimeout(total=5), ) as resp: if resp.status == 200: - logger.debug("Alert text overlay triggered: %s", text) + logger.debug("Alert triggered: %s (%s)", text, icon) else: - logger.warning( - "Failed to trigger alert overlay: HTTP %d", resp.status - ) + logger.warning("Failed to trigger alert: HTTP %d", resp.status) except Exception as e: logger.warning("Could not trigger alert via REST API: %s", e) async def render_frame(self, width: int, height: int) -> Optional[Image.Image]: - """Render the alert frame with blinking border. + """Render a status screen (plugin is background-only). - This is called when the plugin is force-activated via the web UI. - Normally the alert is displayed via the text overlay mechanism for - immediate response, but this provides a richer visual with a - blinking colored border if the plugin is force-activated. + This is only called if the plugin is force-activated via the Web UI. + Since the plugin is a background listener, just show connection status. """ - if not self._alert_active: - # No active alert — render a status screen - return self._render_status(width, height) - - # Check if alert has expired - elapsed = time.time() - self._alert_start_time - if elapsed >= self._alert_duration: - self._alert_active = False - return None # Signal completion, resume normal rotation - - self._frame_count += 1 - - border_color_rgb = _BORDER_COLORS.get( - self._alert_type, _BORDER_COLORS["motion"] - ) - message = self._get_message(self._alert_type) - - # Use the render_alert helper for consistent alert rendering - if self._helpers: - return self._helpers.render_alert( - text=message, - frame_index=self._frame_count, - border_color=border_color_rgb, - text_color=(255, 255, 255), - ) - - # Fallback if helpers not available - return Image.new("RGB", (width, height), (0, 0, 0)) - - def _render_status(self, width: int, height: int) -> Image.Image: - """Render a status screen when no alert is active.""" if self._helpers: status = "CONNECTED" if self._connected else "CONNECTING..." return self._helpers.render_text( @@ -458,26 +420,5 @@ def _render_status(self, width: int, height: int) -> Image.Image: return Image.new("RGB", (width, height), (0, 0, 0)) async def cleanup(self) -> None: - """Disconnect from camera and cancel listener task.""" - self._connected = False - - if self._listener_task and not self._listener_task.done(): - self._listener_task.cancel() - try: - await self._listener_task - except asyncio.CancelledError: - pass - - if self._host: - try: - await self._host.baichuan.unsubscribe_events() - except Exception as e: - logger.debug("Error unsubscribing from events: %s", e) - try: - await self._host.logout() - except Exception as e: - logger.debug("Error logging out from camera: %s", e) - self._host = None - - self._alert_active = False - logger.info("Reolink alert plugin cleaned up") + """No-op: the background listener keeps running independently.""" + pass diff --git a/zeclock/remote/command_handler.py b/zeclock/remote/command_handler.py index 6bc1853..cdb13ae 100644 --- a/zeclock/remote/command_handler.py +++ b/zeclock/remote/command_handler.py @@ -58,6 +58,12 @@ def __init__(self, clock: "ZeClock") -> None: # Text overlay state self._text_overlay: Optional[str] = None self._text_overlay_expires: float = 0.0 + # Alert overlay state (rich alert with blinking border + icon) + self._alert_text: Optional[str] = None + self._alert_expires: float = 0.0 + self._alert_icon: Optional[str] = None + self._alert_color: Optional[tuple] = None + self._alert_frame_count: int = 0 # Forced plugin name (None = normal rotation) self._forced_plugin: Optional[str] = None # Brightness override (None = use scheduler, 0-15 = manual HW brightness) @@ -78,6 +84,16 @@ def has_text_overlay(self) -> bool: return False return True + @property + def has_alert(self) -> bool: + """Whether a rich alert overlay is currently active.""" + if self._alert_text is None: + return False + if time.time() >= self._alert_expires: + self._alert_text = None + return False + return True + @property def text_overlay(self) -> Optional[str]: """Current text overlay content, or None if expired/inactive.""" diff --git a/zeclock/remote/rest_remote.py b/zeclock/remote/rest_remote.py index 9b820e2..450c6fd 100644 --- a/zeclock/remote/rest_remote.py +++ b/zeclock/remote/rest_remote.py @@ -98,6 +98,7 @@ def _setup_routes(self) -> None: self._app.router.add_get("/api/plugins", self._handle_list_plugins) self._app.router.add_get("/api/text", self._handle_display_text) self._app.router.add_post("/api/text", self._handle_display_text) + self._app.router.add_post("/api/alert", self._handle_alert) # Brightness API routes self._app.router.add_get("/api/brightness", self._handle_get_brightness) @@ -324,6 +325,72 @@ async def _handle_display_text(self, request: web.Request) -> web.Response: result = await self._handler.execute(cmd) return self._json_response(result) + async def _handle_alert(self, request: web.Request) -> web.Response: + """POST /api/alert — Display a rich alert with blinking border and icon. + + POST: {"text": "Person detected!", "duration": 15, "icon": "person", "color": [255,0,0]} + + Parameters: + text (str): Alert message (auto-wrapped to fit display). + duration (int): Display duration in seconds (1-300, default 15). + icon (str): Optional icon name ("beacon", "person", "vehicle", + "animal", "motion", "warning", "camera"). + color (list): Optional border color as [R, G, B] (default: [255, 0, 0] red). + """ + try: + body = await request.json() + except (json.JSONDecodeError, Exception): + return web.json_response( + {"success": False, "message": "Invalid JSON body"}, + status=400, + ) + + text = body.get("text") + if not isinstance(text, str) or not text.strip(): + return web.json_response( + {"success": False, "message": "Missing 'text' field"}, + status=400, + ) + + duration = body.get("duration", 15) + try: + duration = max(1, min(300, int(duration))) + except (ValueError, TypeError): + duration = 15 + + icon = body.get("icon") + if icon is not None and not isinstance(icon, str): + icon = None + + color = body.get("color") + if isinstance(color, (list, tuple)) and len(color) == 3: + try: + color = tuple(int(c) for c in color) + except (ValueError, TypeError): + color = (255, 0, 0) + else: + color = (255, 0, 0) + + # Set alert state on the command handler + import time as _time + + self._handler._alert_text = text.strip() + self._handler._alert_expires = _time.time() + duration + self._handler._alert_icon = icon + self._handler._alert_color = color + self._handler._alert_frame_count = 0 + + logger.info( + "Alert displayed: '%s' for %ds (icon=%s)", text.strip(), duration, icon + ) + return web.json_response( + { + "success": True, + "message": f"Alert displayed for {duration}s", + "data": {"text": text.strip(), "duration": duration, "icon": icon}, + } + ) + # --- Brightness handlers --- async def _handle_get_brightness(self, request: web.Request) -> web.Response: From cc6e237956a9ee1d3d806fbbae2f4ca1cb5be0ed Mon Sep 17 00:00:00 2001 From: Olivier Jacques Date: Sat, 25 Jul 2026 12:06:46 +0200 Subject: [PATCH 3/4] docs: add info icon + document /api/alert in README --- README.md | 1 + docs/plugin_authoring.md | 35 +++++++++++++++++++++++++++++++++++ docs/tech.md | 1 + 3 files changed, 37 insertions(+) diff --git a/README.md b/README.md index 883913a..92923d0 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ Enable in config, then control via HTTP: | `POST` | `/api/plugin/force` | Force plugin `{"plugin": "name"}` | | `POST` | `/api/plugin/resume` | Resume normal rotation | | `POST` | `/api/text` | Display text `{"text": "...", "duration": 10}` | +| `POST` | `/api/alert` | Rich alert with blinking border `{"text": "...", "duration": 15, "icon": "person", "color": [255,0,0]}` | | `POST` | `/api/speaker-timer/set` | Set timer `{"seconds": 300}` | | `POST` | `/api/speaker-timer/start` | Start/resume timer | | `POST` | `/api/speaker-timer/pause` | Pause timer | diff --git a/docs/plugin_authoring.md b/docs/plugin_authoring.md index 8fff34d..874b473 100644 --- a/docs/plugin_authoring.md +++ b/docs/plugin_authoring.md @@ -311,6 +311,41 @@ if self._is_cache_stale(): self._helpers.draw_staleness_indicator(frame, total_frames, self._frame_delay_ms) ``` +#### `render_alert(text, frame_index, border_color=(255, 0, 0), text_color=(255, 255, 255), font_name=None, border_width=3, blink_interval_frames=5) -> Image.Image` + +Renders a complete alert frame with a blinking colored border and auto-wrapped centered text. Handles word-wrapping automatically so the message fits inside the bordered area. Useful for notification or alarm plugins that need to draw attention. + +```python +# Render an alert with a red blinking border +frame = self._helpers.render_alert( + "PERSON DETECTED", + frame_index=self._frame_count, + border_color=(255, 0, 0), + text_color=(255, 255, 255), + font_name="MENU", +) + +# Render an alert with an icon +frame = self._helpers.render_alert( + "PERSON DETECTED", + frame_index=self._frame_count, + border_color=(255, 0, 0), + icon="person", +) +``` + +**Parameters:** +- `text`: Alert message to display (automatically word-wrapped to fit) +- `frame_index`: Current frame counter (used for blink timing) +- `border_color`: RGB tuple for the border color (default: red) +- `text_color`: RGB tuple for the text color (default: white) +- `font_name`: Font to use (default: plugin helpers default font) +- `border_width`: Border thickness in pixels (default: 3) +- `blink_interval_frames`: Toggle border visibility every N frames (default: 5) +- `icon`: Optional alert icon name to display left of the text. Available icons: `"beacon"`, `"person"`, `"vehicle"`, `"animal"`, `"motion"`, `"warning"`, `"camera"`, `"info"`. Icons are rendered at 16x16 (SD) or 32x32 (HD) automatically. + +The border blinks by alternating between the specified color and black. Text is automatically uppercased (for DMD font compatibility) and centered both horizontally and vertically within the inner area. When an icon is specified, it is displayed vertically centered on the left side and the text area is reduced accordingly. + --- ## Using ConfettiAnimation diff --git a/docs/tech.md b/docs/tech.md index 7326e1b..ba543ad 100644 --- a/docs/tech.md +++ b/docs/tech.md @@ -58,6 +58,7 @@ This document details the technologies, libraries, protocols, and tools that mak - **Plugin list** (`GET /api/plugins`): Returns all registered plugins with state, frequency, source, active/forced status, and optional `web_controls` metadata exposed by plugins via `get_web_controls()`. - **Brightness** (`/api/brightness`): `GET` returns current brightness state (override, SW dimming, time-only flag, auto/manual mode). `POST {"brightness": 0-15}` sets manual brightness override. `POST /api/brightness/auto` clears the override and resumes automatic scheduling. - **Speaker Timer sub-API** (`/api/speaker-timer/`): Dedicated REST endpoints for the conference timer plugin — `GET status`, `POST start` (also forces the speaker-timer plugin active), `POST pause`, `POST reset` (also resumes normal plugin rotation), `POST set` (accepts `{"seconds": N}` or `{"minutes": N}`). + - **Alert** (`POST /api/alert`): Displays a rich alert with blinking border, auto-wrapped text, and optional icon. Body: `{"text": "...", "duration": 15, "icon": "person", "color": [255,0,0]}`. Duration clamped to 1–300s. Available icons: `beacon`, `person`, `vehicle`, `animal`, `motion`, `warning`, `camera`, `info`. - **TCP Sockets (standard `socket` module)**: Alternative communication via `DMDServerBackend` for development. - **DMDStream network header** (big-endian): From 46d70cde182b916cbe84ae18d3f1023205e58dc9 Mon Sep 17 00:00:00 2001 From: Olivier Jacques Date: Sat, 25 Jul 2026 12:09:21 +0200 Subject: [PATCH 4/4] ci: skip image compression on PRs/branches (only main/tags) --- .github/workflows/build-rpi-image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-rpi-image.yml b/.github/workflows/build-rpi-image.yml index b3d5213..87de24c 100644 --- a/.github/workflows/build-rpi-image.yml +++ b/.github/workflows/build-rpi-image.yml @@ -101,6 +101,7 @@ jobs: build rpi-zeclock.pkr.hcl - name: Compress image + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main') run: | cd deploy/rpi ls -lh "${OUTPUT_IMG}"