diff --git a/.gitignore b/.gitignore
index b6a5691e..362b6a5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
emulator/
output/
+tests/config.local.yaml
poetry.lock
.python-version
diff --git a/docs/ttl_control.md b/docs/ttl_control.md
new file mode 100644
index 00000000..a4c0255b
--- /dev/null
+++ b/docs/ttl_control.md
@@ -0,0 +1,119 @@
+# TTL Control in ionpulse_sdk_core
+
+## Overview
+
+Each of the 32 (on the RFSoC it think it's 16?) TTL output channels can be in one of three states:
+
+| State | Name | Behavior |
+|---|---|---|
+| 0 | OFF | Ionizer forces the line **LOW** (static override) |
+| 1 | ON | Ionizer forces the line **HIGH** (static override) |
+| 2 | CONTROL | sdk_core pulse sequence controls the line dynamically |
+
+State 2 is what Ionizer shows in **blue** — it means the FPGA pulse sequence owns that channel. States 0 and 1 are static overrides that the pulse sequence cannot override.
+
+---
+
+## Hardware Mechanism: The Two-Mask System
+
+The FPGA exposes two 32-bit registers via RPC:
+
+```
+setTTLMasks(uint32 high_mask, uint32 low_mask)
+ttlMasks() → (uint32 high_mask, uint32 low_mask)
+```
+
+For each bit position `n` (TTL channel n):
+
+| `high_mask[n]` | `low_mask[n]` | Result |
+|---|---|---|
+| 0 | 1 | Line forced **LOW** (state 0) |
+| 1 | 0 | Line forced **HIGH** (state 1) |
+| 0 | 0 | Line under **pulse sequence control** (state 2) |
+| 1 | 1 | Undefined — do not use |
+
+Setting a channel to state 2 means clearing **both** its bits in both masks. The FPGA pulse sequence then drives that line as programmed in the waveform.
+
+### Per-channel helper logic
+
+To set channel `n` to a given state, read the current masks, modify the two bits, then write back:
+
+```python
+def set_ttl_state(n, state, high_mask, low_mask):
+ bit = 1 << n
+ if state == 0: # forced LOW
+ high_mask &= ~bit
+ low_mask |= bit
+ elif state == 1: # forced HIGH
+ high_mask |= bit
+ low_mask &= ~bit
+ elif state == 2: # pulse-sequence control
+ high_mask &= ~bit
+ low_mask &= ~bit
+ return high_mask, low_mask
+```
+
+Reading the current state back:
+
+```python
+def get_ttl_state(n, high_mask, low_mask):
+ bit = 1 << n
+ if low_mask & bit: return 0 # forced LOW
+ if high_mask & bit: return 1 # forced HIGH
+ return 2 # pulse-sequence control
+```
+
+---
+
+## RPC API Reference
+
+These are msgpack-RPC calls on the sdk_core server:
+
+```
+# Read current override masks
+(high_mask, low_mask) = rpc.call("ttlMasks")
+
+# Write override masks (affects all 32 channels at once)
+rpc.call("setTTLMasks", high_mask, low_mask)
+```
+
+Both masks must be sent together. Always do a read-modify-write when changing individual channels to avoid clobbering others.
+
+---
+
+## Pulse-Level Control (sdk_core side)
+
+When a TTL channel is in state 2 (CONTROL), the pulse sequence drives it. Each pulse step in the waveform carries two TTL fields:
+
+- **`ttl_pattern`** (uint32): The desired output state — which channels should be HIGH during this step.
+- **`ttl_line_mask`** (uint32): Which channels are actually updated by this step. Channels not in the mask keep their previous value.
+
+Only channels in state 2 (both override bits clear) respond to these pulse-sequence commands. Channels held in state 0 or 1 are statically overridden at the hardware level and will not follow the pulse sequence regardless of `ttl_pattern`.
+
+---
+
+## What to Implement in Your Server
+
+1. **On startup / connect**: call `ttlMasks()` to read the current hardware state.
+2. **Per-channel state control**: implement read-modify-write using `setTTLMasks` to set individual channels to state 0, 1, or 2.
+3. **Expose three operations per channel**:
+ - Force LOW (state 0)
+ - Force HIGH (state 1)
+ - Release to pulse sequence (state 2 — the "blue" / CONTROL mode)
+4. **Pulse sequence**: when building waveforms, pass `ttl_pattern` and `ttl_line_mask` per step. Only channels in state 2 will respond.
+5. **Persistence**: save the mask state so it can be restored on reconnect (the hardware forgets on power cycle).
+
+---
+
+## Ionizer UI Mapping (for reference)
+
+| Ionizer element | Meaning |
+|---|---|
+| StateButton **red** | Channel in state 0 (forced LOW) |
+| StateButton **green** | Channel in state 1 (forced HIGH) |
+| ControlButton **blue** (PI / "397 0th") | Channel in state 2 (pulse-sequence control) |
+| ControlButton **black** | Ionizer is in manual override mode (state 0 or 1) |
+
+The ControlButton is what switches between static override and pulse-sequence control. The StateButton only has effect when the ControlButton is not active.
+
+Key source files: `FPGAConnection.cpp:96–120` (mask logic), `ExperimentsSheet.cpp:747–786` (UI handlers), `api.h:141–147` (RPC method names), `bp_dds.h:112–165` (pulse-level TTL fields).
diff --git a/docs/ttl_server_implementation.md b/docs/ttl_server_implementation.md
new file mode 100644
index 00000000..930ac6af
--- /dev/null
+++ b/docs/ttl_server_implementation.md
@@ -0,0 +1,128 @@
+# TTL Channel Control — Server Implementation
+
+## Background
+
+Each Zedboard FPGA TTL output channel can be in one of three states:
+
+| State | Name | Behavior |
+|-------|------|----------|
+| 0 | OFF | Channel forced LOW (static override) |
+| 1 | ON | Channel forced HIGH (static override) |
+| 2 | CONTROL | Pulse sequence owns the channel |
+
+The hardware exposes two RPC calls: `ttlMasks()` (read) and `setTTLMasks(high_mask, low_mask)` (write). Ionizer's C++ client implemented per-channel control via `FPGAConnection::setTTLlogicState()` / `getTTLlogicState()`. This document describes how ICON implements equivalent server-side control.
+
+---
+
+## Why not use `tiqi_zedboard.TTLs`?
+
+The `TTLs` class (v1.3.0, available via the `zedboard` optional extra) exposes only binary ON/OFF control. Its `_set_channel_zedboard()` always writes a 1 into either `high_mask` or `low_mask`, making it impossible to express state 2 (CONTROL — both bits clear). The underlying RPC supports all three states, so `TTLController` works directly with the raw masks via `HardwareController.get_ttl_masks()` / `set_ttl_masks()`. No changes to tiqi-zedboard are required.
+
+---
+
+## Architecture
+
+```
+APIService
+└── ttl: TTLController (pydase.DataService)
+ ├── HardwareController → Zedboard RPC (ttlMasks / setTTLMasks)
+ └── TTLRepository → SQLite ttl_mask_states table (single row)
+```
+
+`TTLController` is added to `APIService` alongside the existing `StatusController`, `DevicesController`, etc. It owns a dedicated `HardwareController` connection (same pattern as `StatusController`, i.e. `connect=False` initially) and persists masks through `TTLRepository` backed by a single-row SQLite table.
+
+---
+
+## Mask Encoding
+
+For channel `n`:
+
+| `high_mask[n]` | `low_mask[n]` | State |
+|---|---|---|
+| 0 | 1 | 0 — forced LOW |
+| 1 | 0 | 1 — forced HIGH |
+| 0 | 0 | 2 — CONTROL (pulse sequence) |
+
+Helper functions `_decode_state()` and `_encode_state()` in `ttl_controller.py` implement this logic.
+
+---
+
+## Files Changed
+
+| File | Change |
+|------|--------|
+| `src/icon/config/v1.py` | Added `n_ttl_channels: int = 32` to `HardwareConfig` |
+| `src/icon/server/hardware_processing/hardware_controller.py` | Added `get_ttl_masks()` and `set_ttl_masks()` |
+| `src/icon/server/data_access/models/sqlite/ttl_mask_state.py` | New — SQLAlchemy model |
+| `src/icon/server/data_access/models/sqlite/__init__.py` | Added `TTLMaskState` to `__all__` |
+| `src/icon/server/data_access/repositories/ttl_repository.py` | New — upsert/read masks |
+| `src/icon/server/api/ttl_controller.py` | New — pydase DataService |
+| `src/icon/server/api/api_service.py` | Registered `self.ttl = TTLController()` |
+| `src/icon/server/data_access/db_context/sqlite/alembic/versions/a1b2c3d4e5f6_add_ttl_mask_state_table.py` | New — migration |
+| `tests/server/__init__.py` | New — missing package marker |
+| `tests/server/api/test_ttl_controller.py` | New — unit tests |
+
+---
+
+## API
+
+### `ttl.get_states() -> list[int]`
+Returns a list of 32 integers (0/1/2), one per channel, read live from hardware.
+Falls back to the last persisted masks if the hardware is unreachable.
+
+### `ttl.set_state(channel: int, state: int) -> None`
+Sets one channel to state 0, 1, or 2. Writes to hardware, then persists the
+resulting masks to SQLite. Emits a `ttl.update` Socket.IO event.
+
+### `ttl.get_masks() -> dict[str, int]`
+Returns `{"high_mask": ..., "low_mask": ...}` from hardware (or DB fallback).
+
+### `ttl.restore_masks() -> None`
+Re-applies the last persisted masks to the hardware — useful after a power cycle.
+
+---
+
+## Persistence
+
+The `ttl_mask_states` table holds at most one row (`id=1`):
+
+```sql
+CREATE TABLE ttl_mask_states (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ high_mask INTEGER NOT NULL,
+ low_mask INTEGER NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL
+);
+```
+
+Migration: `a1b2c3d4e5f6_add_ttl_mask_state_table.py` (down_revision: `fc9af856df20`).
+
+---
+
+## Configuration
+
+`hardware.n_ttl_channels` in the YAML config (default 32). Set to 16 for RFSoC hardware.
+
+```yaml
+hardware:
+ host: zedboard.lab
+ port: 6007
+ n_ttl_channels: 32
+```
+
+---
+
+## Manual Verification (with hardware)
+
+```python
+# Start ICON server, then from a pydase client:
+client.proxy.ttl.get_states() # read all 32 channel states
+client.proxy.ttl.set_state(0, 1) # force channel 0 HIGH
+client.proxy.ttl.set_state(0, 2) # release channel 0 to pulse sequence
+client.proxy.ttl.get_masks() # inspect raw masks
+client.proxy.ttl.restore_masks() # re-apply persisted masks after power cycle
+```
+
+After `set_state()`, confirm the hardware line changes state. After restarting the
+server, the masks should be readable from the DB via `get_masks()` (hardware path)
+or restored via `restore_masks()`.
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index eb3645af..bd811740 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import DashboardIcon from "@mui/icons-material/Dashboard";
import ScienceIcon from "@mui/icons-material/Science";
import TimelineIcon from "@mui/icons-material/Timeline";
+import ToggleOnIcon from "@mui/icons-material/ToggleOn";
import { Outlet } from "react-router";
import { ReactRouterAppProvider } from "@toolpad/core/react-router";
import type { Navigation } from "@toolpad/core/AppProvider";
@@ -57,6 +58,11 @@ const NAVIGATION: Navigation = [
),
},
+ {
+ segment: "ttl",
+ title: "TTL",
+ icon: ,
+ },
{
kind: "divider",
},
diff --git a/frontend/src/components/ttl/TTLChannel.tsx b/frontend/src/components/ttl/TTLChannel.tsx
new file mode 100644
index 00000000..c02cb859
--- /dev/null
+++ b/frontend/src/components/ttl/TTLChannel.tsx
@@ -0,0 +1,148 @@
+import { useState, KeyboardEvent } from "react";
+import Box from "@mui/material/Box";
+import IconButton from "@mui/material/IconButton";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import Tooltip from "@mui/material/Tooltip";
+import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord";
+import MemoryIcon from "@mui/icons-material/Memory";
+
+interface Props {
+ channel: number;
+ /** Hardware state: 0=OFF, 1=ON, 2=CONTROL */
+ state: number;
+ /** Remembered manual ON(1)/OFF(0), persisted independently of CONTROL mode */
+ localManualState: 0 | 1;
+ label: string;
+ onSetState: (state: number) => void;
+ onSetLocalManual: (manual: 0 | 1) => void;
+ onSetLabel: (label: string) => void;
+}
+
+export function TTLChannel({
+ channel,
+ state,
+ localManualState,
+ label,
+ onSetState,
+ onSetLocalManual,
+ onSetLabel,
+}: Props) {
+ const [editingLabel, setEditingLabel] = useState(false);
+ const [labelDraft, setLabelDraft] = useState(label);
+
+ const inControl = state === 2;
+
+ function handleControlClick() {
+ if (inControl) {
+ // CONTROL → manual: apply the remembered manual state
+ onSetState(localManualState);
+ } else {
+ // manual → CONTROL
+ onSetState(2);
+ }
+ }
+
+ function handleStateClick() {
+ const newManual: 0 | 1 = localManualState === 1 ? 0 : 1;
+ onSetLocalManual(newManual);
+ if (!inControl) {
+ onSetState(newManual);
+ }
+ }
+
+ function handleLabelCommit() {
+ setEditingLabel(false);
+ const trimmed = labelDraft.trim() || label;
+ setLabelDraft(trimmed);
+ if (trimmed !== label) {
+ onSetLabel(trimmed);
+ }
+ }
+
+ function handleLabelKeyDown(e: KeyboardEvent) {
+ if (e.key === "Enter") handleLabelCommit();
+ if (e.key === "Escape") {
+ setLabelDraft(label);
+ setEditingLabel(false);
+ }
+ }
+
+ return (
+
+ {/* ControlButton: blue = CONTROL (FPGA owns channel), grey = manual */}
+
+
+
+
+
+
+ {/* StateButton: green = ON, red = OFF; faded when in CONTROL */}
+
+
+
+
+
+
+ {/* Editable label */}
+ {editingLabel ? (
+ setLabelDraft(e.target.value)}
+ onBlur={handleLabelCommit}
+ onKeyDown={handleLabelKeyDown}
+ autoFocus
+ slotProps={{ input: { style: { fontSize: 12, padding: "2px 6px" } } }}
+ sx={{ width: 90 }}
+ />
+ ) : (
+
+ {
+ setLabelDraft(label);
+ setEditingLabel(true);
+ }}
+ sx={{
+ cursor: "text",
+ userSelect: "none",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ minWidth: 0,
+ }}
+ >
+ {label}
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/ttl/TTLControlPanel.tsx b/frontend/src/components/ttl/TTLControlPanel.tsx
new file mode 100644
index 00000000..7ca68587
--- /dev/null
+++ b/frontend/src/components/ttl/TTLControlPanel.tsx
@@ -0,0 +1,53 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import { TTLChannel } from "./TTLChannel";
+import { TTLState } from "../../hooks/useTTLState";
+
+interface Props {
+ ttl: TTLState;
+}
+
+export function TTLControlPanel({ ttl }: Props) {
+ const { states, labels, localManualState, setState, setLocalManual, setLabel } = ttl;
+
+ return (
+
+
+ TTL Channels
+
+
+ ■
+ {" "}FPGA control
+ ●
+ {" "}ON
+ ●
+ {" "}OFF — double-click a label to rename
+
+
+ {states.map((state, channel) => (
+ setState(channel, s)}
+ onSetLocalManual={(m) => setLocalManual(channel, m)}
+ onSetLabel={(l) => setLabel(channel, l)}
+ />
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/hooks/useTTLState.tsx b/frontend/src/hooks/useTTLState.tsx
new file mode 100644
index 00000000..edaab972
--- /dev/null
+++ b/frontend/src/hooks/useTTLState.tsx
@@ -0,0 +1,104 @@
+import { useCallback, useEffect, useState } from "react";
+import { runMethod, socket } from "../socket";
+import { deserialize } from "../utils/deserializer";
+import { SerializedObject } from "../types/SerializedObject";
+
+const N_CHANNELS = 32;
+
+function defaultLabels(): string[] {
+ return Array.from({ length: N_CHANNELS }, (_, i) => `TTL ${i.toString().padStart(2, "0")}`);
+}
+
+interface TTLUpdateEvent {
+ channel: number;
+ state: number;
+}
+
+interface TTLLabelUpdateEvent {
+ channel: number;
+ label: string;
+}
+
+export interface TTLState {
+ /** Hardware state per channel: 0=OFF, 1=ON, 2=CONTROL. */
+ states: number[];
+ /** Per-channel labels (editable). */
+ labels: string[];
+ /**
+ * Remembered manual ON/OFF value per channel, independent of whether the
+ * channel is currently in CONTROL mode. Matches ionizer's StateButton state.
+ */
+ localManualState: number[];
+ setState: (channel: number, state: number) => void;
+ setLocalManual: (channel: number, manual: 0 | 1) => void;
+ setLabel: (channel: number, label: string) => void;
+}
+
+export function useTTLState(): TTLState {
+ const [states, setStates] = useState(Array(N_CHANNELS).fill(2));
+ const [labels, setLabels] = useState(defaultLabels());
+ const [localManualState, setLocalManualState] = useState(
+ Array(N_CHANNELS).fill(0),
+ );
+
+ useEffect(() => {
+ runMethod("ttl.get_states", [], {}, (ack) => {
+ const fetched = deserialize(ack as SerializedObject) as number[];
+ setStates(fetched);
+ setLocalManualState(fetched.map((s) => (s === 2 ? 0 : s)));
+ });
+
+ runMethod("ttl.get_labels", [], {}, (ack) => {
+ setLabels(deserialize(ack as SerializedObject) as string[]);
+ });
+
+ function onTTLUpdate(data: TTLUpdateEvent) {
+ setStates((prev) => {
+ const next = [...prev];
+ next[data.channel] = data.state;
+ return next;
+ });
+ if (data.state !== 2) {
+ setLocalManualState((prev) => {
+ const next = [...prev];
+ next[data.channel] = data.state;
+ return next;
+ });
+ }
+ }
+
+ function onLabelUpdate(data: TTLLabelUpdateEvent) {
+ setLabels((prev) => {
+ const next = [...prev];
+ next[data.channel] = data.label;
+ return next;
+ });
+ }
+
+ socket.on("ttl.update", onTTLUpdate);
+ socket.on("ttl.label_update", onLabelUpdate);
+
+ return () => {
+ socket.off("ttl.update", onTTLUpdate);
+ socket.off("ttl.label_update", onLabelUpdate);
+ };
+ }, []);
+
+ const setState = useCallback((channel: number, state: number) => {
+ runMethod("ttl.set_state", [channel, state]);
+ }, []);
+
+ const setLocalManual = useCallback((channel: number, manual: 0 | 1) => {
+ setLocalManualState((prev) => {
+ const next = [...prev];
+ next[channel] = manual;
+ return next;
+ });
+ }, []);
+
+ const setLabel = useCallback((channel: number, label: string) => {
+ runMethod("ttl.set_label", [channel, label]);
+ }, []);
+
+ return { states, labels, localManualState, setState, setLocalManual, setLabel };
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 4462a68e..d98f5af0 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -9,6 +9,7 @@ import ParameterPage from "./pages/parameters";
import { DataPage } from "./pages/data";
import DevicesPage from "./pages/devices";
import { SettingsPage } from "./pages/settings";
+import TTLPage from "./pages/ttl";
import JobViewerLayout from "./layouts/job-viewer";
import { JobViewerPage } from "./pages/job-viewer";
@@ -40,6 +41,10 @@ const router = createBrowserRouter([
path: "devices",
Component: DevicesPage,
},
+ {
+ path: "ttl",
+ Component: TTLPage,
+ },
{
path: "settings",
Component: SettingsPage,
diff --git a/frontend/src/pages/ttl.tsx b/frontend/src/pages/ttl.tsx
new file mode 100644
index 00000000..dba92865
--- /dev/null
+++ b/frontend/src/pages/ttl.tsx
@@ -0,0 +1,7 @@
+import { TTLControlPanel } from "../components/ttl/TTLControlPanel";
+import { useTTLState } from "../hooks/useTTLState";
+
+export default function TTLPage() {
+ const ttl = useTTLState();
+ return ;
+}
diff --git a/src/icon/config/v1.py b/src/icon/config/v1.py
index a29d6f04..603e7061 100644
--- a/src/icon/config/v1.py
+++ b/src/icon/config/v1.py
@@ -58,6 +58,7 @@ class ServerConfig(BaseModel):
class HardwareConfig(BaseModel):
host: str = "localhost"
port: int = 6007
+ n_ttl_channels: int = 32
class ServiceConfigV1(BaseConfig): # type: ignore[misc]
diff --git a/src/icon/server/api/api_service.py b/src/icon/server/api/api_service.py
index a455a7e9..752ef164 100644
--- a/src/icon/server/api/api_service.py
+++ b/src/icon/server/api/api_service.py
@@ -18,6 +18,7 @@
from icon.server.api.scans_controller import ScansController
from icon.server.api.scheduler_controller import SchedulerController
from icon.server.api.status_controller import StatusController
+from icon.server.api.ttl_controller import TTLController
from icon.server.data_access.repositories.parameters_repository import (
ParametersRepository,
)
@@ -83,6 +84,8 @@ def __init__(
processes."""
self.status = StatusController()
"""Controller for system status monitoring."""
+ self.ttl = TTLController()
+ """Controller for hardware TTL channel state."""
self._experiment_library_client = experiment_library_client
@task(autostart=True)
diff --git a/src/icon/server/api/ttl_controller.py b/src/icon/server/api/ttl_controller.py
new file mode 100644
index 00000000..d1f88e90
--- /dev/null
+++ b/src/icon/server/api/ttl_controller.py
@@ -0,0 +1,172 @@
+import asyncio
+import logging
+
+import pydase
+
+from icon.config.config import get_config
+from icon.server.data_access.repositories.ttl_repository import TTLRepository
+from icon.server.hardware_processing.hardware_controller import HardwareController
+from icon.server.web_server.socketio_emit_queue import emit_queue
+
+logger = logging.getLogger(__name__)
+
+_OFF = 0
+_ON = 1
+_CONTROL = 2
+
+
+def _decode_state(channel: int, high_mask: int, low_mask: int) -> int:
+ """Return the state (0=OFF, 1=ON, 2=CONTROL) of a single TTL channel."""
+ bit = 1 << channel
+ if low_mask & bit:
+ return _OFF
+ if high_mask & bit:
+ return _ON
+ return _CONTROL
+
+
+def _encode_state(
+ channel: int, state: int, high_mask: int, low_mask: int
+) -> tuple[int, int]:
+ """Return updated (high_mask, low_mask) after setting channel to state."""
+ bit = 1 << channel
+ if state == _OFF:
+ high_mask &= ~bit
+ low_mask |= bit
+ elif state == _ON:
+ high_mask |= bit
+ low_mask &= ~bit
+ elif state == _CONTROL:
+ high_mask &= ~bit
+ low_mask &= ~bit
+ else:
+ raise ValueError(f"Invalid TTL state {state!r}; must be 0 (OFF), 1 (ON), or 2 (CONTROL)")
+ return high_mask, low_mask
+
+
+class TTLController(pydase.DataService):
+ """Per-channel TTL state control for the Zedboard FPGA.
+
+ Each of the n_ttl_channels output channels can be set to one of three states:
+
+ - 0 (OFF): channel forced LOW
+ - 1 (ON): channel forced HIGH
+ - 2 (CONTROL): channel driven by the pulse sequence
+
+ Mask state is persisted to SQLite so it survives hardware power cycles.
+ """
+
+ def __init__(self) -> None:
+ super().__init__()
+ self._n_channels: int = get_config().hardware.n_ttl_channels
+ self._hw = HardwareController(connect=False)
+ self._repo = TTLRepository()
+
+ def get_states(self) -> list[int]:
+ """Return the current state (0/1/2) for all channels.
+
+ Reads live from the hardware when connected; falls back to the last
+ persisted masks if the hardware is unreachable.
+ """
+ try:
+ high_mask, low_mask = self._hw.get_ttl_masks()
+ except RuntimeError:
+ logger.warning(
+ "Hardware unreachable; returning persisted TTL mask state"
+ )
+ high_mask, low_mask = self._repo.get_masks()
+ return [
+ _decode_state(ch, high_mask, low_mask)
+ for ch in range(self._n_channels)
+ ]
+
+ def set_state(self, channel: int, state: int) -> None:
+ """Set a single TTL channel to state 0 (OFF), 1 (ON), or 2 (CONTROL).
+
+ Persists the resulting masks to SQLite and emits a ``ttl.update`` event.
+
+ Args:
+ channel: Channel index (0 to n_ttl_channels - 1).
+ state: 0 = forced LOW, 1 = forced HIGH, 2 = pulse-sequence control.
+
+ Raises:
+ ValueError: If channel or state is out of range.
+ RuntimeError: If the hardware is unreachable.
+ """
+ if not (0 <= channel < self._n_channels):
+ raise ValueError(
+ f"Channel {channel} out of range [0, {self._n_channels - 1}]"
+ )
+ high_mask, low_mask = self._hw.get_ttl_masks()
+ high_mask, low_mask = _encode_state(channel, state, high_mask, low_mask)
+ self._hw.set_ttl_masks(high_mask, low_mask)
+ self._repo.save_masks(high_mask, low_mask)
+ emit_queue.put(
+ {
+ "event": "ttl.update",
+ "data": {
+ "channel": channel,
+ "state": state,
+ "high_mask": high_mask,
+ "low_mask": low_mask,
+ },
+ }
+ )
+ logger.info("TTL channel %d set to state %d", channel, state)
+
+ def get_masks(self) -> dict[str, int]:
+ """Return the raw ``{'high_mask': ..., 'low_mask': ...}`` from hardware.
+
+ Falls back to persisted masks if the hardware is unreachable.
+ """
+ try:
+ high_mask, low_mask = self._hw.get_ttl_masks()
+ except RuntimeError:
+ logger.warning(
+ "Hardware unreachable; returning persisted TTL masks"
+ )
+ high_mask, low_mask = self._repo.get_masks()
+ return {"high_mask": high_mask, "low_mask": low_mask}
+
+ def get_labels(self) -> list[str]:
+ """Return per-channel labels; missing entries default to 'TTL XX'."""
+ return self._repo.get_labels(self._n_channels)
+
+ def set_label(self, channel: int, label: str) -> None:
+ """Persist a custom label for one channel and emit a ``ttl.label_update`` event.
+
+ Args:
+ channel: Channel index (0 to n_ttl_channels - 1).
+ label: New label string.
+
+ Raises:
+ ValueError: If channel is out of range.
+ """
+ if not (0 <= channel < self._n_channels):
+ raise ValueError(
+ f"Channel {channel} out of range [0, {self._n_channels - 1}]"
+ )
+ labels = self._repo.get_labels(self._n_channels)
+ labels[channel] = label
+ self._repo.save_labels(labels)
+ emit_queue.put(
+ {
+ "event": "ttl.label_update",
+ "data": {"channel": channel, "label": label},
+ }
+ )
+ logger.info("TTL channel %d label set to %r", channel, label)
+
+ async def restore_masks(self) -> None:
+ """Write the last persisted masks back to the hardware.
+
+ Useful after a hardware power cycle. Runs the RPC call in a thread to
+ avoid blocking the event loop.
+ """
+ high_mask, low_mask = self._repo.get_masks()
+ await asyncio.to_thread(self._hw.set_ttl_masks, high_mask, low_mask)
+ logger.info(
+ "Restored TTL masks to hardware: high=0x%08x low=0x%08x",
+ high_mask,
+ low_mask,
+ )
diff --git a/src/icon/server/data_access/db_context/sqlite/alembic/versions/a1b2c3d4e5f6_add_ttl_mask_state_table.py b/src/icon/server/data_access/db_context/sqlite/alembic/versions/a1b2c3d4e5f6_add_ttl_mask_state_table.py
new file mode 100644
index 00000000..bc6ab3e5
--- /dev/null
+++ b/src/icon/server/data_access/db_context/sqlite/alembic/versions/a1b2c3d4e5f6_add_ttl_mask_state_table.py
@@ -0,0 +1,33 @@
+"""Add ttl_mask_states table.
+
+Revision ID: a1b2c3d4e5f6
+Revises: fc9af856df20
+Create Date: 2026-05-18 00:00:00.000000
+
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "a1b2c3d4e5f6"
+down_revision: str | None = "f60d837b7263"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ op.create_table(
+ "ttl_mask_states",
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column("high_mask", sa.Integer(), nullable=False),
+ sa.Column("low_mask", sa.Integer(), nullable=False),
+ sa.Column("updated_at", sa.TIMESTAMP(timezone=True), nullable=False),
+ sa.PrimaryKeyConstraint("id"),
+ )
+
+
+def downgrade() -> None:
+ op.drop_table("ttl_mask_states")
diff --git a/src/icon/server/data_access/db_context/sqlite/alembic/versions/b2c3d4e5f6a1_add_labels_to_ttl_mask_states.py b/src/icon/server/data_access/db_context/sqlite/alembic/versions/b2c3d4e5f6a1_add_labels_to_ttl_mask_states.py
new file mode 100644
index 00000000..0d113658
--- /dev/null
+++ b/src/icon/server/data_access/db_context/sqlite/alembic/versions/b2c3d4e5f6a1_add_labels_to_ttl_mask_states.py
@@ -0,0 +1,28 @@
+"""Add labels column to ttl_mask_states table.
+
+Revision ID: b2c3d4e5f6a1
+Revises: a1b2c3d4e5f6
+Create Date: 2026-05-18 00:01:00.000000
+
+"""
+
+from collections.abc import Sequence
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "b2c3d4e5f6a1"
+down_revision: str | None = "a1b2c3d4e5f6"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
+
+
+def upgrade() -> None:
+ with op.batch_alter_table("ttl_mask_states", schema=None) as batch_op:
+ batch_op.add_column(sa.Column("labels", sa.Text(), nullable=True))
+
+
+def downgrade() -> None:
+ with op.batch_alter_table("ttl_mask_states", schema=None) as batch_op:
+ batch_op.drop_column("labels")
diff --git a/src/icon/server/data_access/models/sqlite/__init__.py b/src/icon/server/data_access/models/sqlite/__init__.py
index 34f2b976..0b54d1a5 100644
--- a/src/icon/server/data_access/models/sqlite/__init__.py
+++ b/src/icon/server/data_access/models/sqlite/__init__.py
@@ -12,6 +12,7 @@
from icon.server.data_access.models.sqlite.job import Job
from icon.server.data_access.models.sqlite.job_run import JobRun
from icon.server.data_access.models.sqlite.scan_parameter import ScanParameter
+from icon.server.data_access.models.sqlite.ttl_mask_state import TTLMaskState
__all__ = [
"Base",
@@ -20,4 +21,5 @@
"Job",
"JobRun",
"ScanParameter",
+ "TTLMaskState",
]
diff --git a/src/icon/server/data_access/models/sqlite/ttl_mask_state.py b/src/icon/server/data_access/models/sqlite/ttl_mask_state.py
new file mode 100644
index 00000000..c23cd59e
--- /dev/null
+++ b/src/icon/server/data_access/models/sqlite/ttl_mask_state.py
@@ -0,0 +1,46 @@
+import datetime
+
+import pytz
+import sqlalchemy
+import sqlalchemy.orm
+
+from icon.config.config import get_config
+from icon.server.data_access.models.sqlite.base import Base
+
+timezone = pytz.timezone(get_config().date.timezone)
+
+
+class TTLMaskState(Base):
+ """SQLAlchemy model for persisting TTL override masks.
+
+ A single row (id=1) stores the current high_mask and low_mask written to the
+ Zedboard FPGA. This allows the server to restore channel states after a hardware
+ power cycle.
+ """
+
+ __tablename__ = "ttl_mask_states"
+
+ id: sqlalchemy.orm.Mapped[int] = sqlalchemy.orm.mapped_column(
+ primary_key=True, autoincrement=True
+ )
+ high_mask: sqlalchemy.orm.Mapped[int] = sqlalchemy.orm.mapped_column(
+ nullable=False, default=0
+ )
+ low_mask: sqlalchemy.orm.Mapped[int] = sqlalchemy.orm.mapped_column(
+ nullable=False, default=0
+ )
+ updated_at: sqlalchemy.orm.Mapped[datetime.datetime] = sqlalchemy.orm.mapped_column(
+ nullable=False,
+ default=lambda: datetime.datetime.now(timezone),
+ onupdate=lambda: datetime.datetime.now(timezone),
+ )
+ labels: sqlalchemy.orm.Mapped[str | None] = sqlalchemy.orm.mapped_column(
+ nullable=True, default=None
+ )
+ """JSON-encoded list of per-channel label strings, e.g. '["TTL 00", "397 0th", ...]'."""
+
+ def __repr__(self) -> str:
+ return (
+ f""
+ )
diff --git a/src/icon/server/data_access/repositories/ttl_repository.py b/src/icon/server/data_access/repositories/ttl_repository.py
new file mode 100644
index 00000000..c839fd30
--- /dev/null
+++ b/src/icon/server/data_access/repositories/ttl_repository.py
@@ -0,0 +1,74 @@
+import json
+import logging
+
+import sqlalchemy.orm
+
+from icon.server.data_access.db_context.sqlite import engine
+from icon.server.data_access.models.sqlite.ttl_mask_state import TTLMaskState
+
+logger = logging.getLogger(__name__)
+
+_SINGLETON_ID = 1
+
+
+def _default_labels(n: int) -> list[str]:
+ return [f"TTL {i:02d}" for i in range(n)]
+
+
+class TTLRepository:
+ """Data access object for the TTL mask state table.
+
+ The table always contains at most one row (id=1), representing the last
+ mask pair and channel labels written to the Zedboard FPGA.
+ """
+
+ def get_masks(self) -> tuple[int, int]:
+ """Return (high_mask, low_mask); returns (0, 0) if no row exists yet."""
+ with sqlalchemy.orm.Session(engine) as session:
+ row = session.get(TTLMaskState, _SINGLETON_ID)
+ if row is None:
+ return 0, 0
+ return row.high_mask, row.low_mask
+
+ def save_masks(self, high_mask: int, low_mask: int) -> None:
+ """Upsert the single TTL state row with new mask values."""
+ with sqlalchemy.orm.Session(engine) as session:
+ row = session.get(TTLMaskState, _SINGLETON_ID)
+ if row is None:
+ row = TTLMaskState(id=_SINGLETON_ID, high_mask=high_mask, low_mask=low_mask)
+ session.add(row)
+ else:
+ row.high_mask = high_mask
+ row.low_mask = low_mask
+ session.commit()
+ logger.debug(
+ "Saved TTL masks: high=0x%08x low=0x%08x", high_mask, low_mask
+ )
+
+ def get_labels(self, n_channels: int) -> list[str]:
+ """Return stored per-channel labels; missing entries filled with 'TTL XX' defaults."""
+ with sqlalchemy.orm.Session(engine) as session:
+ row = session.get(TTLMaskState, _SINGLETON_ID)
+ if row is None or row.labels is None:
+ return _default_labels(n_channels)
+ stored: list[str] = json.loads(row.labels)
+ defaults = _default_labels(n_channels)
+ # Merge: use stored value where present, default otherwise
+ result = defaults[:]
+ for i, label in enumerate(stored):
+ if i < n_channels:
+ result[i] = label
+ return result
+
+ def save_labels(self, labels: list[str]) -> None:
+ """Persist the full labels list as a JSON string."""
+ with sqlalchemy.orm.Session(engine) as session:
+ row = session.get(TTLMaskState, _SINGLETON_ID)
+ if row is None:
+ row = TTLMaskState(id=_SINGLETON_ID, high_mask=0, low_mask=0,
+ labels=json.dumps(labels))
+ session.add(row)
+ else:
+ row.labels = json.dumps(labels)
+ session.commit()
+ logger.debug("Saved TTL labels")
diff --git a/src/icon/server/frontend/assets/index-BPLIYou4.js b/src/icon/server/frontend/assets/index-BPLIYou4.js
new file mode 100644
index 00000000..3ce4e641
--- /dev/null
+++ b/src/icon/server/frontend/assets/index-BPLIYou4.js
@@ -0,0 +1,45 @@
+import{r as hb,a as mb,b as y,c as Je,j as d,u as Or,S as pb,d as vb,B as yb,A as Vf,e as gb,f as Rt,I as mt,g as bb,h as ef,T as jv,i as xb,C as Tv,k as Dv,l as fl,m as Sb,n as e0,o as St,s as kf,p as pe,q as Gt,t as Rv,v as Nt,P as Eb,D as Nr,w as Sf,x as Cb,L as _b,y as wb,z as _r,G as jb,E as qf,F as Tb,H as wr,J as jr,K as Tr,M as tf,N as Eo,O as Yf,Q as hl,R as en,U as Co,V as lt,W as Ba,X as Ha,Y as aa,Z as Db,_ as Av,$ as Rb,a0 as ta,a1 as na,a2 as Yt,a3 as Va,a4 as Ze,a5 as Ab,a6 as Mb,a7 as _o,a8 as wo,a9 as jo,aa as Ob,ab as Mv,ac as Nb,ad as zb,ae as Lb,af as Ub,ag as Bb,ah as Ma}from"./mui-bAivgoRt.js";import{u as Hb,i as Vb,a as kb,b as qb,c as Yb,d as Gb,e as Xb,f as Pb,g as Qb,h as Kb,j as Zb,k as Fb,l as $b}from"./echarts-D1BCANXe.js";import"./zrender-Co-hdh87.js";(function(){const l=document.createElement("link").relList;if(l&&l.supports&&l.supports("modulepreload"))return;for(const u of document.querySelectorAll('link[rel="modulepreload"]'))s(u);new MutationObserver(u=>{for(const f of u)if(f.type==="childList")for(const h of f.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&s(h)}).observe(document,{childList:!0,subtree:!0});function r(u){const f={};return u.integrity&&(f.integrity=u.integrity),u.referrerPolicy&&(f.referrerPolicy=u.referrerPolicy),u.crossOrigin==="use-credentials"?f.credentials="include":u.crossOrigin==="anonymous"?f.credentials="omit":f.credentials="same-origin",f}function s(u){if(u.ep)return;u.ep=!0;const f=r(u);fetch(u.href,f)}})();var nf={exports:{}},hr={},af={exports:{}},lf={};/**
+ * @license React
+ * scheduler.production.js
+ *
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var t0;function Jb(){return t0||(t0=1,(function(a){function l(D,O){var X=D.length;D.push(O);e:for(;0>>1,te=D[ae];if(0>>1;aeu(ie,X))Ueu(yt,ie)?(D[ae]=yt,D[Ue]=X,ae=Ue):(D[ae]=ie,D[ve]=X,ae=ve);else if(Ueu(yt,X))D[ae]=yt,D[Ue]=X,ae=Ue;else break e}}return O}function u(D,O){var X=D.sortIndex-O.sortIndex;return X!==0?X:D.id-O.id}if(a.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var f=performance;a.unstable_now=function(){return f.now()}}else{var h=Date,m=h.now();a.unstable_now=function(){return h.now()-m}}var p=[],g=[],b=1,S=null,E=3,_=!1,j=!1,A=!1,C=!1,z=typeof setTimeout=="function"?setTimeout:null,V=typeof clearTimeout=="function"?clearTimeout:null,Y=typeof setImmediate<"u"?setImmediate:null;function P(D){for(var O=r(g);O!==null;){if(O.callback===null)s(g);else if(O.startTime<=D)s(g),O.sortIndex=O.expirationTime,l(p,O);else break;O=r(g)}}function $(D){if(A=!1,P(D),!j)if(r(p)!==null)j=!0,R||(R=!0,re());else{var O=r(g);O!==null&&se($,O.startTime-D)}}var R=!1,K=-1,J=5,ee=-1;function ne(){return C?!0:!(a.unstable_now()-eeD&&ne());){var ae=S.callback;if(typeof ae=="function"){S.callback=null,E=S.priorityLevel;var te=ae(S.expirationTime<=D);if(D=a.unstable_now(),typeof te=="function"){S.callback=te,P(D),O=!0;break t}S===r(p)&&s(p),P(D)}else s(p);S=r(p)}if(S!==null)O=!0;else{var be=r(g);be!==null&&se($,be.startTime-D),O=!1}}break e}finally{S=null,E=X,_=!1}O=void 0}}finally{O?re():R=!1}}}var re;if(typeof Y=="function")re=function(){Y(ue)};else if(typeof MessageChannel<"u"){var le=new MessageChannel,I=le.port2;le.port1.onmessage=ue,re=function(){I.postMessage(null)}}else re=function(){z(ue,0)};function se(D,O){K=z(function(){D(a.unstable_now())},O)}a.unstable_IdlePriority=5,a.unstable_ImmediatePriority=1,a.unstable_LowPriority=4,a.unstable_NormalPriority=3,a.unstable_Profiling=null,a.unstable_UserBlockingPriority=2,a.unstable_cancelCallback=function(D){D.callback=null},a.unstable_forceFrameRate=function(D){0>D||125ae?(D.sortIndex=X,l(g,D),r(p)===null&&D===r(g)&&(A?(V(K),K=-1):A=!0,se($,X-ae))):(D.sortIndex=te,l(p,D),j||_||(j=!0,R||(R=!0,re()))),D},a.unstable_shouldYield=ne,a.unstable_wrapCallback=function(D){var O=E;return function(){var X=E;E=O;try{return D.apply(this,arguments)}finally{E=X}}}})(lf)),lf}var n0;function Ib(){return n0||(n0=1,af.exports=Jb()),af.exports}/**
+ * @license React
+ * react-dom-client.production.js
+ *
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */var a0;function Wb(){if(a0)return hr;a0=1;var a=Ib(),l=hb(),r=mb();function s(e){var t="https://react.dev/errors/"+e;if(1te||(e.current=ae[te],ae[te]=null,te--)}function ie(e,t){te++,ae[te]=e.current,e.current=t}var Ue=be(null),yt=be(null),zt=be(null),yl=be(null);function gl(e,t){switch(ie(zt,t),ie(yt,e),ie(Ue,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Dp(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Dp(t),e=Rp(t,e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}ve(Ue),ie(Ue,e)}function ia(){ve(Ue),ve(yt),ve(zt)}function ut(e){e.memoizedState!==null&&ie(yl,e);var t=Ue.current,n=Rp(t,e.type);t!==n&&(ie(yt,e),ie(Ue,n))}function pn(e){yt.current===e&&(ve(Ue),ve(yt)),yl.current===e&&(ve(yl),or._currentValue=X)}var bl=Object.prototype.hasOwnProperty,yi=a.unstable_scheduleCallback,vn=a.unstable_cancelCallback,Ko=a.unstable_shouldYield,Zo=a.unstable_requestPaint,Xt=a.unstable_now,Fo=a.unstable_getCurrentPriorityLevel,Yr=a.unstable_ImmediatePriority,Gr=a.unstable_UserBlockingPriority,xl=a.unstable_NormalPriority,Ln=a.unstable_LowPriority,ra=a.unstable_IdlePriority,Xr=a.log,gi=a.unstable_setDisableYieldValue,Lt=null,Ie=null;function yn(e){if(typeof Xr=="function"&&gi(e),Ie&&typeof Ie.setStrictMode=="function")try{Ie.setStrictMode(Lt,e)}catch{}}var Ct=Math.clz32?Math.clz32:Pr,$o=Math.log,Cn=Math.LN2;function Pr(e){return e>>>=0,e===0?32:31-($o(e)/Cn|0)|0}var Ga=256,Xa=4194304;function Un(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194048;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Pa(e,t,n){var i=e.pendingLanes;if(i===0)return 0;var o=0,c=e.suspendedLanes,v=e.pingedLanes;e=e.warmLanes;var x=i&134217727;return x!==0?(i=x&~c,i!==0?o=Un(i):(v&=x,v!==0?o=Un(v):n||(n=x&~e,n!==0&&(o=Un(n))))):(x=i&~c,x!==0?o=Un(x):v!==0?o=Un(v):n||(n=i&~e,n!==0&&(o=Un(n)))),o===0?0:t!==0&&t!==o&&(t&c)===0&&(c=o&-o,n=t&-t,c>=n||c===32&&(n&4194048)!==0)?t:o}function _n(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Qr(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Sl(){var e=Ga;return Ga<<=1,(Ga&4194048)===0&&(Ga=256),e}function Kr(){var e=Xa;return Xa<<=1,(Xa&62914560)===0&&(Xa=4194304),e}function El(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Qa(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Zr(e,t,n,i,o,c){var v=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var x=e.entanglements,w=e.expirationTimes,B=e.hiddenUpdates;for(n=v&~n;0)":-1o||w[i]!==B[o]){var G=`
+`+w[i].replace(" at new "," at ");return e.displayName&&G.includes("")&&(G=G.replace("",e.displayName)),G}while(1<=i&&0<=o);break}}}finally{We=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?Ut(n):""}function Fr(e){switch(e.tag){case 26:case 27:case 5:return Ut(e.type);case 16:return Ut("Lazy");case 13:return Ut("Suspense");case 19:return Ut("SuspenseList");case 0:case 15:return oa(e.type,!1);case 11:return oa(e.type.render,!1);case 1:return oa(e.type,!0);case 31:return Ut("Activity");default:return""}}function $r(e){try{var t="";do t+=Fr(e),e=e.return;while(e);return t}catch(n){return`
+Error generating stack: `+n.message+`
+`+n.stack}}function nn(e){switch(typeof e){case"bigint":case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Cd(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function og(e){var t=Cd(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),i=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var o=n.get,c=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(v){i=""+v,c.call(this,v)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return i},setValue:function(v){i=""+v},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Jr(e){e._valueTracker||(e._valueTracker=og(e))}function _d(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),i="";return e&&(i=Cd(e)?e.checked?"true":"false":e.value),e=i,e!==n?(t.setValue(e),!0):!1}function Ir(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}var ug=/[\n"\\]/g;function an(e){return e.replace(ug,function(t){return"\\"+t.charCodeAt(0).toString(16)+" "})}function Jo(e,t,n,i,o,c,v,x){e.name="",v!=null&&typeof v!="function"&&typeof v!="symbol"&&typeof v!="boolean"?e.type=v:e.removeAttribute("type"),t!=null?v==="number"?(t===0&&e.value===""||e.value!=t)&&(e.value=""+nn(t)):e.value!==""+nn(t)&&(e.value=""+nn(t)):v!=="submit"&&v!=="reset"||e.removeAttribute("value"),t!=null?Io(e,v,nn(t)):n!=null?Io(e,v,nn(n)):i!=null&&e.removeAttribute("value"),o==null&&c!=null&&(e.defaultChecked=!!c),o!=null&&(e.checked=o&&typeof o!="function"&&typeof o!="symbol"),x!=null&&typeof x!="function"&&typeof x!="symbol"&&typeof x!="boolean"?e.name=""+nn(x):e.removeAttribute("name")}function wd(e,t,n,i,o,c,v,x){if(c!=null&&typeof c!="function"&&typeof c!="symbol"&&typeof c!="boolean"&&(e.type=c),t!=null||n!=null){if(!(c!=="submit"&&c!=="reset"||t!=null))return;n=n!=null?""+nn(n):"",t=t!=null?""+nn(t):n,x||t===e.value||(e.value=t),e.defaultValue=t}i=i??o,i=typeof i!="function"&&typeof i!="symbol"&&!!i,e.checked=x?e.checked:!!i,e.defaultChecked=!!i,v!=null&&typeof v!="function"&&typeof v!="symbol"&&typeof v!="boolean"&&(e.name=v)}function Io(e,t,n){t==="number"&&Ir(e.ownerDocument)===e||e.defaultValue===""+n||(e.defaultValue=""+n)}function _l(e,t,n,i){if(e=e.options,t){t={};for(var o=0;o"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),au=!1;if(qn)try{var Ei={};Object.defineProperty(Ei,"passive",{get:function(){au=!0}}),window.addEventListener("test",Ei,Ei),window.removeEventListener("test",Ei,Ei)}catch{au=!1}var ua=null,lu=null,es=null;function Od(){if(es)return es;var e,t=lu,n=t.length,i,o="value"in ua?ua.value:ua.textContent,c=o.length;for(e=0;e=wi),Hd=" ",Vd=!1;function kd(e,t){switch(e){case"keyup":return Bg.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function qd(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Dl=!1;function Vg(e,t){switch(e){case"compositionend":return qd(t);case"keypress":return t.which!==32?null:(Vd=!0,Hd);case"textInput":return e=t.data,e===Hd&&Vd?null:e;default:return null}}function kg(e,t){if(Dl)return e==="compositionend"||!uu&&kd(e,t)?(e=Od(),es=lu=ua=null,Dl=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=i}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Fd(n)}}function Jd(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Jd(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Id(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ir(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ir(e.document)}return t}function du(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}var Zg=qn&&"documentMode"in document&&11>=document.documentMode,Rl=null,hu=null,Ri=null,mu=!1;function Wd(e,t,n){var i=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;mu||Rl==null||Rl!==Ir(i)||(i=Rl,"selectionStart"in i&&du(i)?i={start:i.selectionStart,end:i.selectionEnd}:(i=(i.ownerDocument&&i.ownerDocument.defaultView||window).getSelection(),i={anchorNode:i.anchorNode,anchorOffset:i.anchorOffset,focusNode:i.focusNode,focusOffset:i.focusOffset}),Ri&&Di(Ri,i)||(Ri=i,i=Xs(hu,"onSelect"),0>=v,o-=v,Gn=1<<32-Ct(t)+o|n<c?c:8;var v=D.T,x={};D.T=x,Wu(e,!1,t,n);try{var w=o(),B=D.S;if(B!==null&&B(x,w),w!==null&&typeof w=="object"&&typeof w.then=="function"){var G=a1(w,i);Xi(e,t,G,Jt(e))}else Xi(e,t,i,Jt(e))}catch(Z){Xi(e,t,{then:function(){},status:"rejected",reason:Z},Jt())}finally{O.p=c,D.T=v}}function o1(){}function Ju(e,t,n,i){if(e.tag!==5)throw Error(s(476));var o=em(e).queue;Wh(e,o,t,X,n===null?o1:function(){return tm(e),n(i)})}function em(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:X,baseState:X,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Kn,lastRenderedState:X},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Kn,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function tm(e){var t=em(e).next.queue;Xi(e,t,{},Jt())}function Iu(){return Dt(or)}function nm(){return dt().memoizedState}function am(){return dt().memoizedState}function u1(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=Jt();e=da(n);var i=ha(t,e,n);i!==null&&(It(i,t,n),Hi(i,t,n)),t={cache:Du()},e.payload=t;return}t=t.return}}function c1(e,t,n){var i=Jt();n={lane:i,revertLane:0,action:n,hasEagerState:!1,eagerState:null,next:null},_s(e)?im(t,n):(n=gu(e,t,n,i),n!==null&&(It(n,e,i),rm(n,t,i)))}function lm(e,t,n){var i=Jt();Xi(e,t,n,i)}function Xi(e,t,n,i){var o={lane:i,revertLane:0,action:n,hasEagerState:!1,eagerState:null,next:null};if(_s(e))im(t,o);else{var c=e.alternate;if(e.lanes===0&&(c===null||c.lanes===0)&&(c=t.lastRenderedReducer,c!==null))try{var v=t.lastRenderedState,x=c(v,n);if(o.hasEagerState=!0,o.eagerState=x,Qt(x,v))return ss(e,t,o,0),Ke===null&&rs(),!1}catch{}finally{}if(n=gu(e,t,o,i),n!==null)return It(n,e,i),rm(n,t,i),!0}return!1}function Wu(e,t,n,i){if(i={lane:2,revertLane:Mc(),action:i,hasEagerState:!1,eagerState:null,next:null},_s(e)){if(t)throw Error(s(479))}else t=gu(e,n,i,2),t!==null&&It(t,e,2)}function _s(e){var t=e.alternate;return e===De||t!==null&&t===De}function im(e,t){Vl=gs=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function rm(e,t,n){if((n&4194048)!==0){var i=t.lanes;i&=e.pendingLanes,n|=i,t.lanes=n,Za(e,n)}}var ws={readContext:Dt,use:xs,useCallback:it,useContext:it,useEffect:it,useImperativeHandle:it,useLayoutEffect:it,useInsertionEffect:it,useMemo:it,useReducer:it,useRef:it,useState:it,useDebugValue:it,useDeferredValue:it,useTransition:it,useSyncExternalStore:it,useId:it,useHostTransitionStatus:it,useFormState:it,useActionState:it,useOptimistic:it,useMemoCache:it,useCacheRefresh:it},sm={readContext:Dt,use:xs,useCallback:function(e,t){return Ht().memoizedState=[e,t===void 0?null:t],e},useContext:Dt,useEffect:Xh,useImperativeHandle:function(e,t,n){n=n!=null?n.concat([e]):null,Cs(4194308,4,Zh.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Cs(4194308,4,e,t)},useInsertionEffect:function(e,t){Cs(4,2,e,t)},useMemo:function(e,t){var n=Ht();t=t===void 0?null:t;var i=e();if(rl){yn(!0);try{e()}finally{yn(!1)}}return n.memoizedState=[i,t],i},useReducer:function(e,t,n){var i=Ht();if(n!==void 0){var o=n(t);if(rl){yn(!0);try{n(t)}finally{yn(!1)}}}else o=t;return i.memoizedState=i.baseState=o,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:o},i.queue=e,e=e.dispatch=c1.bind(null,De,e),[i.memoizedState,e]},useRef:function(e){var t=Ht();return e={current:e},t.memoizedState=e},useState:function(e){e=Ku(e);var t=e.queue,n=lm.bind(null,De,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Fu,useDeferredValue:function(e,t){var n=Ht();return $u(n,e,t)},useTransition:function(){var e=Ku(!1);return e=Wh.bind(null,De,e.queue,!0,!1),Ht().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var i=De,o=Ht();if(He){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),Ke===null)throw Error(s(349));(ze&124)!==0||Dh(i,t,n)}o.memoizedState=n;var c={value:n,getSnapshot:t};return o.queue=c,Xh(Ah.bind(null,i,c,e),[e]),i.flags|=2048,ql(9,Es(),Rh.bind(null,i,c,n,t),null),n},useId:function(){var e=Ht(),t=Ke.identifierPrefix;if(He){var n=Xn,i=Gn;n=(i&~(1<<32-Ct(i)-1)).toString(32)+n,t="«"+t+"R"+n,n=bs++,0Ce?(xt=ye,ye=null):xt=ye.sibling;var Le=H(N,ye,U[Ce],Q);if(Le===null){ye===null&&(ye=xt);break}e&&ye&&Le.alternate===null&&t(N,ye),M=c(Le,M,Ce),Re===null?oe=Le:Re.sibling=Le,Re=Le,ye=xt}if(Ce===U.length)return n(N,ye),He&&el(N,Ce),oe;if(ye===null){for(;CeCe?(xt=ye,ye=null):xt=ye.sibling;var Aa=H(N,ye,Le.value,Q);if(Aa===null){ye===null&&(ye=xt);break}e&&ye&&Aa.alternate===null&&t(N,ye),M=c(Aa,M,Ce),Re===null?oe=Aa:Re.sibling=Aa,Re=Aa,ye=xt}if(Le.done)return n(N,ye),He&&el(N,Ce),oe;if(ye===null){for(;!Le.done;Ce++,Le=U.next())Le=Z(N,Le.value,Q),Le!==null&&(M=c(Le,M,Ce),Re===null?oe=Le:Re.sibling=Le,Re=Le);return He&&el(N,Ce),oe}for(ye=i(ye);!Le.done;Ce++,Le=U.next())Le=k(ye,N,Ce,Le.value,Q),Le!==null&&(e&&Le.alternate!==null&&ye.delete(Le.key===null?Ce:Le.key),M=c(Le,M,Ce),Re===null?oe=Le:Re.sibling=Le,Re=Le);return e&&ye.forEach(function(db){return t(N,db)}),He&&el(N,Ce),oe}function Pe(N,M,U,Q){if(typeof U=="object"&&U!==null&&U.type===j&&U.key===null&&(U=U.props.children),typeof U=="object"&&U!==null){switch(U.$$typeof){case E:e:{for(var oe=U.key;M!==null;){if(M.key===oe){if(oe=U.type,oe===j){if(M.tag===7){n(N,M.sibling),Q=o(M,U.props.children),Q.return=N,N=Q;break e}}else if(M.elementType===oe||typeof oe=="object"&&oe!==null&&oe.$$typeof===J&&um(oe)===M.type){n(N,M.sibling),Q=o(M,U.props),Qi(Q,U),Q.return=N,N=Q;break e}n(N,M);break}else t(N,M);M=M.sibling}U.type===j?(Q=Ia(U.props.children,N.mode,Q,U.key),Q.return=N,N=Q):(Q=us(U.type,U.key,U.props,null,N.mode,Q),Qi(Q,U),Q.return=N,N=Q)}return v(N);case _:e:{for(oe=U.key;M!==null;){if(M.key===oe)if(M.tag===4&&M.stateNode.containerInfo===U.containerInfo&&M.stateNode.implementation===U.implementation){n(N,M.sibling),Q=o(M,U.children||[]),Q.return=N,N=Q;break e}else{n(N,M);break}else t(N,M);M=M.sibling}Q=Su(U,N.mode,Q),Q.return=N,N=Q}return v(N);case J:return oe=U._init,U=oe(U._payload),Pe(N,M,U,Q)}if(se(U))return _e(N,M,U,Q);if(re(U)){if(oe=re(U),typeof oe!="function")throw Error(s(150));return U=oe.call(U),Ee(N,M,U,Q)}if(typeof U.then=="function")return Pe(N,M,js(U),Q);if(U.$$typeof===Y)return Pe(N,M,hs(N,U),Q);Ts(N,U)}return typeof U=="string"&&U!==""||typeof U=="number"||typeof U=="bigint"?(U=""+U,M!==null&&M.tag===6?(n(N,M.sibling),Q=o(M,U),Q.return=N,N=Q):(n(N,M),Q=xu(U,N.mode,Q),Q.return=N,N=Q),v(N)):n(N,M)}return function(N,M,U,Q){try{Pi=0;var oe=Pe(N,M,U,Q);return Yl=null,oe}catch(ye){if(ye===Ui||ye===ps)throw ye;var Re=Kt(29,ye,null,N.mode);return Re.lanes=Q,Re.return=N,Re}finally{}}}var Gl=cm(!0),fm=cm(!1),un=be(null),Tn=null;function pa(e){var t=e.alternate;ie(vt,vt.current&1),ie(un,e),Tn===null&&(t===null||Hl.current!==null||t.memoizedState!==null)&&(Tn=e)}function dm(e){if(e.tag===22){if(ie(vt,vt.current),ie(un,e),Tn===null){var t=e.alternate;t!==null&&t.memoizedState!==null&&(Tn=e)}}else va()}function va(){ie(vt,vt.current),ie(un,un.current)}function Zn(e){ve(un),Tn===e&&(Tn=null),ve(vt)}var vt=be(0);function Ds(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||Gc(n)))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&128)!==0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}function ec(e,t,n,i){t=e.memoizedState,n=n(i,t),n=n==null?t:b({},t,n),e.memoizedState=n,e.lanes===0&&(e.updateQueue.baseState=n)}var tc={enqueueSetState:function(e,t,n){e=e._reactInternals;var i=Jt(),o=da(i);o.payload=t,n!=null&&(o.callback=n),t=ha(e,o,i),t!==null&&(It(t,e,i),Hi(t,e,i))},enqueueReplaceState:function(e,t,n){e=e._reactInternals;var i=Jt(),o=da(i);o.tag=1,o.payload=t,n!=null&&(o.callback=n),t=ha(e,o,i),t!==null&&(It(t,e,i),Hi(t,e,i))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var n=Jt(),i=da(n);i.tag=2,t!=null&&(i.callback=t),t=ha(e,i,n),t!==null&&(It(t,e,n),Hi(t,e,n))}};function hm(e,t,n,i,o,c,v){return e=e.stateNode,typeof e.shouldComponentUpdate=="function"?e.shouldComponentUpdate(i,c,v):t.prototype&&t.prototype.isPureReactComponent?!Di(n,i)||!Di(o,c):!0}function mm(e,t,n,i){e=t.state,typeof t.componentWillReceiveProps=="function"&&t.componentWillReceiveProps(n,i),typeof t.UNSAFE_componentWillReceiveProps=="function"&&t.UNSAFE_componentWillReceiveProps(n,i),t.state!==e&&tc.enqueueReplaceState(t,t.state,null)}function sl(e,t){var n=t;if("ref"in t){n={};for(var i in t)i!=="ref"&&(n[i]=t[i])}if(e=e.defaultProps){n===t&&(n=b({},n));for(var o in e)n[o]===void 0&&(n[o]=e[o])}return n}var Rs=typeof reportError=="function"?reportError:function(e){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof e=="object"&&e!==null&&typeof e.message=="string"?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",e);return}console.error(e)};function pm(e){Rs(e)}function vm(e){console.error(e)}function ym(e){Rs(e)}function As(e,t){try{var n=e.onUncaughtError;n(t.value,{componentStack:t.stack})}catch(i){setTimeout(function(){throw i})}}function gm(e,t,n){try{var i=e.onCaughtError;i(n.value,{componentStack:n.stack,errorBoundary:t.tag===1?t.stateNode:null})}catch(o){setTimeout(function(){throw o})}}function nc(e,t,n){return n=da(n),n.tag=3,n.payload={element:null},n.callback=function(){As(e,t)},n}function bm(e){return e=da(e),e.tag=3,e}function xm(e,t,n,i){var o=n.type.getDerivedStateFromError;if(typeof o=="function"){var c=i.value;e.payload=function(){return o(c)},e.callback=function(){gm(t,n,i)}}var v=n.stateNode;v!==null&&typeof v.componentDidCatch=="function"&&(e.callback=function(){gm(t,n,i),typeof o!="function"&&(Ea===null?Ea=new Set([this]):Ea.add(this));var x=i.stack;this.componentDidCatch(i.value,{componentStack:x!==null?x:""})})}function d1(e,t,n,i,o){if(n.flags|=32768,i!==null&&typeof i=="object"&&typeof i.then=="function"){if(t=n.alternate,t!==null&&Ni(t,n,o,!0),n=un.current,n!==null){switch(n.tag){case 13:return Tn===null?jc():n.alternate===null&&at===0&&(at=3),n.flags&=-257,n.flags|=65536,n.lanes=o,i===Mu?n.flags|=16384:(t=n.updateQueue,t===null?n.updateQueue=new Set([i]):t.add(i),Dc(e,i,o)),!1;case 22:return n.flags|=65536,i===Mu?n.flags|=16384:(t=n.updateQueue,t===null?(t={transitions:null,markerInstances:null,retryQueue:new Set([i])},n.updateQueue=t):(n=t.retryQueue,n===null?t.retryQueue=new Set([i]):n.add(i)),Dc(e,i,o)),!1}throw Error(s(435,n.tag))}return Dc(e,i,o),jc(),!1}if(He)return t=un.current,t!==null?((t.flags&65536)===0&&(t.flags|=256),t.flags|=65536,t.lanes=o,i!==_u&&(e=Error(s(422),{cause:i}),Oi(ln(e,n)))):(i!==_u&&(t=Error(s(423),{cause:i}),Oi(ln(t,n))),e=e.current.alternate,e.flags|=65536,o&=-o,e.lanes|=o,i=ln(i,n),o=nc(e.stateNode,i,o),zu(e,o),at!==4&&(at=2)),!1;var c=Error(s(520),{cause:i});if(c=ln(c,n),Wi===null?Wi=[c]:Wi.push(c),at!==4&&(at=2),t===null)return!0;i=ln(i,n),n=t;do{switch(n.tag){case 3:return n.flags|=65536,e=o&-o,n.lanes|=e,e=nc(n.stateNode,i,e),zu(n,e),!1;case 1:if(t=n.type,c=n.stateNode,(n.flags&128)===0&&(typeof t.getDerivedStateFromError=="function"||c!==null&&typeof c.componentDidCatch=="function"&&(Ea===null||!Ea.has(c))))return n.flags|=65536,o&=-o,n.lanes|=o,o=bm(o),xm(o,e,n,i),zu(n,o),!1}n=n.return}while(n!==null);return!1}var Sm=Error(s(461)),gt=!1;function _t(e,t,n,i){t.child=e===null?fm(t,null,n,i):Gl(t,e.child,n,i)}function Em(e,t,n,i,o){n=n.render;var c=t.ref;if("ref"in i){var v={};for(var x in i)x!=="ref"&&(v[x]=i[x])}else v=i;return ll(t),i=Vu(e,t,n,v,c,o),x=ku(),e!==null&&!gt?(qu(e,t,o),Fn(e,t,o)):(He&&x&&Eu(t),t.flags|=1,_t(e,t,i,o),t.child)}function Cm(e,t,n,i,o){if(e===null){var c=n.type;return typeof c=="function"&&!bu(c)&&c.defaultProps===void 0&&n.compare===null?(t.tag=15,t.type=c,_m(e,t,c,i,o)):(e=us(n.type,null,i,t,t.mode,o),e.ref=t.ref,e.return=t,t.child=e)}if(c=e.child,!cc(e,o)){var v=c.memoizedProps;if(n=n.compare,n=n!==null?n:Di,n(v,i)&&e.ref===t.ref)return Fn(e,t,o)}return t.flags|=1,e=Yn(c,i),e.ref=t.ref,e.return=t,t.child=e}function _m(e,t,n,i,o){if(e!==null){var c=e.memoizedProps;if(Di(c,i)&&e.ref===t.ref)if(gt=!1,t.pendingProps=i=c,cc(e,o))(e.flags&131072)!==0&&(gt=!0);else return t.lanes=e.lanes,Fn(e,t,o)}return ac(e,t,n,i,o)}function wm(e,t,n){var i=t.pendingProps,o=i.children,c=e!==null?e.memoizedState:null;if(i.mode==="hidden"){if((t.flags&128)!==0){if(i=c!==null?c.baseLanes|n:n,e!==null){for(o=t.child=e.child,c=0;o!==null;)c=c|o.lanes|o.childLanes,o=o.sibling;t.childLanes=c&~i}else t.childLanes=0,t.child=null;return jm(e,t,i,n)}if((n&536870912)!==0)t.memoizedState={baseLanes:0,cachePool:null},e!==null&&ms(t,c!==null?c.cachePool:null),c!==null?_h(t,c):Uu(),dm(t);else return t.lanes=t.childLanes=536870912,jm(e,t,c!==null?c.baseLanes|n:n,n)}else c!==null?(ms(t,c.cachePool),_h(t,c),va(),t.memoizedState=null):(e!==null&&ms(t,null),Uu(),va());return _t(e,t,o,n),t.child}function jm(e,t,n,i){var o=Au();return o=o===null?null:{parent:pt._currentValue,pool:o},t.memoizedState={baseLanes:n,cachePool:o},e!==null&&ms(t,null),Uu(),dm(t),e!==null&&Ni(e,t,i,!0),null}function Ms(e,t){var n=t.ref;if(n===null)e!==null&&e.ref!==null&&(t.flags|=4194816);else{if(typeof n!="function"&&typeof n!="object")throw Error(s(284));(e===null||e.ref!==n)&&(t.flags|=4194816)}}function ac(e,t,n,i,o){return ll(t),n=Vu(e,t,n,i,void 0,o),i=ku(),e!==null&&!gt?(qu(e,t,o),Fn(e,t,o)):(He&&i&&Eu(t),t.flags|=1,_t(e,t,n,o),t.child)}function Tm(e,t,n,i,o,c){return ll(t),t.updateQueue=null,n=jh(t,i,n,o),wh(e),i=ku(),e!==null&&!gt?(qu(e,t,c),Fn(e,t,c)):(He&&i&&Eu(t),t.flags|=1,_t(e,t,n,c),t.child)}function Dm(e,t,n,i,o){if(ll(t),t.stateNode===null){var c=Nl,v=n.contextType;typeof v=="object"&&v!==null&&(c=Dt(v)),c=new n(i,c),t.memoizedState=c.state!==null&&c.state!==void 0?c.state:null,c.updater=tc,t.stateNode=c,c._reactInternals=t,c=t.stateNode,c.props=i,c.state=t.memoizedState,c.refs={},Ou(t),v=n.contextType,c.context=typeof v=="object"&&v!==null?Dt(v):Nl,c.state=t.memoizedState,v=n.getDerivedStateFromProps,typeof v=="function"&&(ec(t,n,v,i),c.state=t.memoizedState),typeof n.getDerivedStateFromProps=="function"||typeof c.getSnapshotBeforeUpdate=="function"||typeof c.UNSAFE_componentWillMount!="function"&&typeof c.componentWillMount!="function"||(v=c.state,typeof c.componentWillMount=="function"&&c.componentWillMount(),typeof c.UNSAFE_componentWillMount=="function"&&c.UNSAFE_componentWillMount(),v!==c.state&&tc.enqueueReplaceState(c,c.state,null),ki(t,i,c,o),Vi(),c.state=t.memoizedState),typeof c.componentDidMount=="function"&&(t.flags|=4194308),i=!0}else if(e===null){c=t.stateNode;var x=t.memoizedProps,w=sl(n,x);c.props=w;var B=c.context,G=n.contextType;v=Nl,typeof G=="object"&&G!==null&&(v=Dt(G));var Z=n.getDerivedStateFromProps;G=typeof Z=="function"||typeof c.getSnapshotBeforeUpdate=="function",x=t.pendingProps!==x,G||typeof c.UNSAFE_componentWillReceiveProps!="function"&&typeof c.componentWillReceiveProps!="function"||(x||B!==v)&&mm(t,c,i,v),fa=!1;var H=t.memoizedState;c.state=H,ki(t,i,c,o),Vi(),B=t.memoizedState,x||H!==B||fa?(typeof Z=="function"&&(ec(t,n,Z,i),B=t.memoizedState),(w=fa||hm(t,n,w,i,H,B,v))?(G||typeof c.UNSAFE_componentWillMount!="function"&&typeof c.componentWillMount!="function"||(typeof c.componentWillMount=="function"&&c.componentWillMount(),typeof c.UNSAFE_componentWillMount=="function"&&c.UNSAFE_componentWillMount()),typeof c.componentDidMount=="function"&&(t.flags|=4194308)):(typeof c.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=i,t.memoizedState=B),c.props=i,c.state=B,c.context=v,i=w):(typeof c.componentDidMount=="function"&&(t.flags|=4194308),i=!1)}else{c=t.stateNode,Nu(e,t),v=t.memoizedProps,G=sl(n,v),c.props=G,Z=t.pendingProps,H=c.context,B=n.contextType,w=Nl,typeof B=="object"&&B!==null&&(w=Dt(B)),x=n.getDerivedStateFromProps,(B=typeof x=="function"||typeof c.getSnapshotBeforeUpdate=="function")||typeof c.UNSAFE_componentWillReceiveProps!="function"&&typeof c.componentWillReceiveProps!="function"||(v!==Z||H!==w)&&mm(t,c,i,w),fa=!1,H=t.memoizedState,c.state=H,ki(t,i,c,o),Vi();var k=t.memoizedState;v!==Z||H!==k||fa||e!==null&&e.dependencies!==null&&ds(e.dependencies)?(typeof x=="function"&&(ec(t,n,x,i),k=t.memoizedState),(G=fa||hm(t,n,G,i,H,k,w)||e!==null&&e.dependencies!==null&&ds(e.dependencies))?(B||typeof c.UNSAFE_componentWillUpdate!="function"&&typeof c.componentWillUpdate!="function"||(typeof c.componentWillUpdate=="function"&&c.componentWillUpdate(i,k,w),typeof c.UNSAFE_componentWillUpdate=="function"&&c.UNSAFE_componentWillUpdate(i,k,w)),typeof c.componentDidUpdate=="function"&&(t.flags|=4),typeof c.getSnapshotBeforeUpdate=="function"&&(t.flags|=1024)):(typeof c.componentDidUpdate!="function"||v===e.memoizedProps&&H===e.memoizedState||(t.flags|=4),typeof c.getSnapshotBeforeUpdate!="function"||v===e.memoizedProps&&H===e.memoizedState||(t.flags|=1024),t.memoizedProps=i,t.memoizedState=k),c.props=i,c.state=k,c.context=w,i=G):(typeof c.componentDidUpdate!="function"||v===e.memoizedProps&&H===e.memoizedState||(t.flags|=4),typeof c.getSnapshotBeforeUpdate!="function"||v===e.memoizedProps&&H===e.memoizedState||(t.flags|=1024),i=!1)}return c=i,Ms(e,t),i=(t.flags&128)!==0,c||i?(c=t.stateNode,n=i&&typeof n.getDerivedStateFromError!="function"?null:c.render(),t.flags|=1,e!==null&&i?(t.child=Gl(t,e.child,null,o),t.child=Gl(t,null,n,o)):_t(e,t,n,o),t.memoizedState=c.state,e=t.child):e=Fn(e,t,o),e}function Rm(e,t,n,i){return Mi(),t.flags|=256,_t(e,t,n,i),t.child}var lc={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function ic(e){return{baseLanes:e,cachePool:vh()}}function rc(e,t,n){return e=e!==null?e.childLanes&~n:0,t&&(e|=cn),e}function Am(e,t,n){var i=t.pendingProps,o=!1,c=(t.flags&128)!==0,v;if((v=c)||(v=e!==null&&e.memoizedState===null?!1:(vt.current&2)!==0),v&&(o=!0,t.flags&=-129),v=(t.flags&32)!==0,t.flags&=-33,e===null){if(He){if(o?pa(t):va(),He){var x=nt,w;if(w=x){e:{for(w=x,x=jn;w.nodeType!==8;){if(!x){x=null;break e}if(w=xn(w.nextSibling),w===null){x=null;break e}}x=w}x!==null?(t.memoizedState={dehydrated:x,treeContext:Wa!==null?{id:Gn,overflow:Xn}:null,retryLane:536870912,hydrationErrors:null},w=Kt(18,null,null,0),w.stateNode=x,w.return=t,t.child=w,Mt=t,nt=null,w=!0):w=!1}w||nl(t)}if(x=t.memoizedState,x!==null&&(x=x.dehydrated,x!==null))return Gc(x)?t.lanes=32:t.lanes=536870912,null;Zn(t)}return x=i.children,i=i.fallback,o?(va(),o=t.mode,x=Os({mode:"hidden",children:x},o),i=Ia(i,o,n,null),x.return=t,i.return=t,x.sibling=i,t.child=x,o=t.child,o.memoizedState=ic(n),o.childLanes=rc(e,v,n),t.memoizedState=lc,i):(pa(t),sc(t,x))}if(w=e.memoizedState,w!==null&&(x=w.dehydrated,x!==null)){if(c)t.flags&256?(pa(t),t.flags&=-257,t=oc(e,t,n)):t.memoizedState!==null?(va(),t.child=e.child,t.flags|=128,t=null):(va(),o=i.fallback,x=t.mode,i=Os({mode:"visible",children:i.children},x),o=Ia(o,x,n,null),o.flags|=2,i.return=t,o.return=t,i.sibling=o,t.child=i,Gl(t,e.child,null,n),i=t.child,i.memoizedState=ic(n),i.childLanes=rc(e,v,n),t.memoizedState=lc,t=o);else if(pa(t),Gc(x)){if(v=x.nextSibling&&x.nextSibling.dataset,v)var B=v.dgst;v=B,i=Error(s(419)),i.stack="",i.digest=v,Oi({value:i,source:null,stack:null}),t=oc(e,t,n)}else if(gt||Ni(e,t,n,!1),v=(n&e.childLanes)!==0,gt||v){if(v=Ke,v!==null&&(i=n&-n,i=(i&42)!==0?1:bi(i),i=(i&(v.suspendedLanes|n))!==0?0:i,i!==0&&i!==w.retryLane))throw w.retryLane=i,Ol(e,i),It(v,e,i),Sm;x.data==="$?"||jc(),t=oc(e,t,n)}else x.data==="$?"?(t.flags|=192,t.child=e.child,t=null):(e=w.treeContext,nt=xn(x.nextSibling),Mt=t,He=!0,tl=null,jn=!1,e!==null&&(sn[on++]=Gn,sn[on++]=Xn,sn[on++]=Wa,Gn=e.id,Xn=e.overflow,Wa=t),t=sc(t,i.children),t.flags|=4096);return t}return o?(va(),o=i.fallback,x=t.mode,w=e.child,B=w.sibling,i=Yn(w,{mode:"hidden",children:i.children}),i.subtreeFlags=w.subtreeFlags&65011712,B!==null?o=Yn(B,o):(o=Ia(o,x,n,null),o.flags|=2),o.return=t,i.return=t,i.sibling=o,t.child=i,i=o,o=t.child,x=e.child.memoizedState,x===null?x=ic(n):(w=x.cachePool,w!==null?(B=pt._currentValue,w=w.parent!==B?{parent:B,pool:B}:w):w=vh(),x={baseLanes:x.baseLanes|n,cachePool:w}),o.memoizedState=x,o.childLanes=rc(e,v,n),t.memoizedState=lc,i):(pa(t),n=e.child,e=n.sibling,n=Yn(n,{mode:"visible",children:i.children}),n.return=t,n.sibling=null,e!==null&&(v=t.deletions,v===null?(t.deletions=[e],t.flags|=16):v.push(e)),t.child=n,t.memoizedState=null,n)}function sc(e,t){return t=Os({mode:"visible",children:t},e.mode),t.return=e,e.child=t}function Os(e,t){return e=Kt(22,e,null,t),e.lanes=0,e.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null},e}function oc(e,t,n){return Gl(t,e.child,null,n),e=sc(t,t.pendingProps.children),e.flags|=2,t.memoizedState=null,e}function Mm(e,t,n){e.lanes|=t;var i=e.alternate;i!==null&&(i.lanes|=t),ju(e.return,t,n)}function uc(e,t,n,i,o){var c=e.memoizedState;c===null?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:i,tail:n,tailMode:o}:(c.isBackwards=t,c.rendering=null,c.renderingStartTime=0,c.last=i,c.tail=n,c.tailMode=o)}function Om(e,t,n){var i=t.pendingProps,o=i.revealOrder,c=i.tail;if(_t(e,t,i.children,n),i=vt.current,(i&2)!==0)i=i&1|2,t.flags|=128;else{if(e!==null&&(e.flags&128)!==0)e:for(e=t.child;e!==null;){if(e.tag===13)e.memoizedState!==null&&Mm(e,n,t);else if(e.tag===19)Mm(e,n,t);else if(e.child!==null){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;e.sibling===null;){if(e.return===null||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}i&=1}switch(ie(vt,i),o){case"forwards":for(n=t.child,o=null;n!==null;)e=n.alternate,e!==null&&Ds(e)===null&&(o=n),n=n.sibling;n=o,n===null?(o=t.child,t.child=null):(o=n.sibling,n.sibling=null),uc(t,!1,o,n,c);break;case"backwards":for(n=null,o=t.child,t.child=null;o!==null;){if(e=o.alternate,e!==null&&Ds(e)===null){t.child=o;break}e=o.sibling,o.sibling=n,n=o,o=e}uc(t,!0,n,null,c);break;case"together":uc(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Fn(e,t,n){if(e!==null&&(t.dependencies=e.dependencies),Sa|=t.lanes,(n&t.childLanes)===0)if(e!==null){if(Ni(e,t,n,!1),(n&t.childLanes)===0)return null}else return null;if(e!==null&&t.child!==e.child)throw Error(s(153));if(t.child!==null){for(e=t.child,n=Yn(e,e.pendingProps),t.child=n,n.return=t;e.sibling!==null;)e=e.sibling,n=n.sibling=Yn(e,e.pendingProps),n.return=t;n.sibling=null}return t.child}function cc(e,t){return(e.lanes&t)!==0?!0:(e=e.dependencies,!!(e!==null&&ds(e)))}function h1(e,t,n){switch(t.tag){case 3:gl(t,t.stateNode.containerInfo),ca(t,pt,e.memoizedState.cache),Mi();break;case 27:case 5:ut(t);break;case 4:gl(t,t.stateNode.containerInfo);break;case 10:ca(t,t.type,t.memoizedProps.value);break;case 13:var i=t.memoizedState;if(i!==null)return i.dehydrated!==null?(pa(t),t.flags|=128,null):(n&t.child.childLanes)!==0?Am(e,t,n):(pa(t),e=Fn(e,t,n),e!==null?e.sibling:null);pa(t);break;case 19:var o=(e.flags&128)!==0;if(i=(n&t.childLanes)!==0,i||(Ni(e,t,n,!1),i=(n&t.childLanes)!==0),o){if(i)return Om(e,t,n);t.flags|=128}if(o=t.memoizedState,o!==null&&(o.rendering=null,o.tail=null,o.lastEffect=null),ie(vt,vt.current),i)break;return null;case 22:case 23:return t.lanes=0,wm(e,t,n);case 24:ca(t,pt,e.memoizedState.cache)}return Fn(e,t,n)}function Nm(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps)gt=!0;else{if(!cc(e,n)&&(t.flags&128)===0)return gt=!1,h1(e,t,n);gt=(e.flags&131072)!==0}else gt=!1,He&&(t.flags&1048576)!==0&&uh(t,fs,t.index);switch(t.lanes=0,t.tag){case 16:e:{e=t.pendingProps;var i=t.elementType,o=i._init;if(i=o(i._payload),t.type=i,typeof i=="function")bu(i)?(e=sl(i,e),t.tag=1,t=Dm(null,t,i,e,n)):(t.tag=0,t=ac(null,t,i,e,n));else{if(i!=null){if(o=i.$$typeof,o===P){t.tag=11,t=Em(null,t,i,e,n);break e}else if(o===K){t.tag=14,t=Cm(null,t,i,e,n);break e}}throw t=I(i)||i,Error(s(306,t,""))}}return t;case 0:return ac(e,t,t.type,t.pendingProps,n);case 1:return i=t.type,o=sl(i,t.pendingProps),Dm(e,t,i,o,n);case 3:e:{if(gl(t,t.stateNode.containerInfo),e===null)throw Error(s(387));i=t.pendingProps;var c=t.memoizedState;o=c.element,Nu(e,t),ki(t,i,null,n);var v=t.memoizedState;if(i=v.cache,ca(t,pt,i),i!==c.cache&&Tu(t,[pt],n,!0),Vi(),i=v.element,c.isDehydrated)if(c={element:i,isDehydrated:!1,cache:v.cache},t.updateQueue.baseState=c,t.memoizedState=c,t.flags&256){t=Rm(e,t,i,n);break e}else if(i!==o){o=ln(Error(s(424)),t),Oi(o),t=Rm(e,t,i,n);break e}else{switch(e=t.stateNode.containerInfo,e.nodeType){case 9:e=e.body;break;default:e=e.nodeName==="HTML"?e.ownerDocument.body:e}for(nt=xn(e.firstChild),Mt=t,He=!0,tl=null,jn=!0,n=fm(t,null,i,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling}else{if(Mi(),i===o){t=Fn(e,t,n);break e}_t(e,t,i,n)}t=t.child}return t;case 26:return Ms(e,t),e===null?(n=Bp(t.type,null,t.pendingProps,null))?t.memoizedState=n:He||(n=t.type,e=t.pendingProps,i=Qs(zt.current).createElement(n),i[F]=t,i[W]=e,jt(i,n,e),we(i),t.stateNode=i):t.memoizedState=Bp(t.type,e.memoizedProps,t.pendingProps,e.memoizedState),null;case 27:return ut(t),e===null&&He&&(i=t.stateNode=zp(t.type,t.pendingProps,zt.current),Mt=t,jn=!0,o=nt,wa(t.type)?(Xc=o,nt=xn(i.firstChild)):nt=o),_t(e,t,t.pendingProps.children,n),Ms(e,t),e===null&&(t.flags|=4194304),t.child;case 5:return e===null&&He&&((o=i=nt)&&(i=q1(i,t.type,t.pendingProps,jn),i!==null?(t.stateNode=i,Mt=t,nt=xn(i.firstChild),jn=!1,o=!0):o=!1),o||nl(t)),ut(t),o=t.type,c=t.pendingProps,v=e!==null?e.memoizedProps:null,i=c.children,kc(o,c)?i=null:v!==null&&kc(o,v)&&(t.flags|=32),t.memoizedState!==null&&(o=Vu(e,t,i1,null,null,n),or._currentValue=o),Ms(e,t),_t(e,t,i,n),t.child;case 6:return e===null&&He&&((e=n=nt)&&(n=Y1(n,t.pendingProps,jn),n!==null?(t.stateNode=n,Mt=t,nt=null,e=!0):e=!1),e||nl(t)),null;case 13:return Am(e,t,n);case 4:return gl(t,t.stateNode.containerInfo),i=t.pendingProps,e===null?t.child=Gl(t,null,i,n):_t(e,t,i,n),t.child;case 11:return Em(e,t,t.type,t.pendingProps,n);case 7:return _t(e,t,t.pendingProps,n),t.child;case 8:return _t(e,t,t.pendingProps.children,n),t.child;case 12:return _t(e,t,t.pendingProps.children,n),t.child;case 10:return i=t.pendingProps,ca(t,t.type,i.value),_t(e,t,i.children,n),t.child;case 9:return o=t.type._context,i=t.pendingProps.children,ll(t),o=Dt(o),i=i(o),t.flags|=1,_t(e,t,i,n),t.child;case 14:return Cm(e,t,t.type,t.pendingProps,n);case 15:return _m(e,t,t.type,t.pendingProps,n);case 19:return Om(e,t,n);case 31:return i=t.pendingProps,n=t.mode,i={mode:i.mode,children:i.children},e===null?(n=Os(i,n),n.ref=t.ref,t.child=n,n.return=t,t=n):(n=Yn(e.child,i),n.ref=t.ref,t.child=n,n.return=t,t=n),t;case 22:return wm(e,t,n);case 24:return ll(t),i=Dt(pt),e===null?(o=Au(),o===null&&(o=Ke,c=Du(),o.pooledCache=c,c.refCount++,c!==null&&(o.pooledCacheLanes|=n),o=c),t.memoizedState={parent:i,cache:o},Ou(t),ca(t,pt,o)):((e.lanes&n)!==0&&(Nu(e,t),ki(t,null,null,n),Vi()),o=e.memoizedState,c=t.memoizedState,o.parent!==i?(o={parent:i,cache:i},t.memoizedState=o,t.lanes===0&&(t.memoizedState=t.updateQueue.baseState=o),ca(t,pt,i)):(i=c.cache,ca(t,pt,i),i!==o.cache&&Tu(t,[pt],n,!0))),_t(e,t,t.pendingProps.children,n),t.child;case 29:throw t.pendingProps}throw Error(s(156,t.tag))}function $n(e){e.flags|=4}function zm(e,t){if(t.type!=="stylesheet"||(t.state.loading&4)!==0)e.flags&=-16777217;else if(e.flags|=16777216,!Yp(t)){if(t=un.current,t!==null&&((ze&4194048)===ze?Tn!==null:(ze&62914560)!==ze&&(ze&536870912)===0||t!==Tn))throw Bi=Mu,yh;e.flags|=8192}}function Ns(e,t){t!==null&&(e.flags|=4),e.flags&16384&&(t=e.tag!==22?Kr():536870912,e.lanes|=t,Kl|=t)}function Ki(e,t){if(!He)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;t!==null;)t.alternate!==null&&(n=t),t=t.sibling;n===null?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var i=null;n!==null;)n.alternate!==null&&(i=n),n=n.sibling;i===null?t||e.tail===null?e.tail=null:e.tail.sibling=null:i.sibling=null}}function et(e){var t=e.alternate!==null&&e.alternate.child===e.child,n=0,i=0;if(t)for(var o=e.child;o!==null;)n|=o.lanes|o.childLanes,i|=o.subtreeFlags&65011712,i|=o.flags&65011712,o.return=e,o=o.sibling;else for(o=e.child;o!==null;)n|=o.lanes|o.childLanes,i|=o.subtreeFlags,i|=o.flags,o.return=e,o=o.sibling;return e.subtreeFlags|=i,e.childLanes=n,t}function m1(e,t,n){var i=t.pendingProps;switch(Cu(t),t.tag){case 31:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return et(t),null;case 1:return et(t),null;case 3:return n=t.stateNode,i=null,e!==null&&(i=e.memoizedState.cache),t.memoizedState.cache!==i&&(t.flags|=2048),Qn(pt),ia(),n.pendingContext&&(n.context=n.pendingContext,n.pendingContext=null),(e===null||e.child===null)&&(Ai(t)?$n(t):e===null||e.memoizedState.isDehydrated&&(t.flags&256)===0||(t.flags|=1024,dh())),et(t),null;case 26:return n=t.memoizedState,e===null?($n(t),n!==null?(et(t),zm(t,n)):(et(t),t.flags&=-16777217)):n?n!==e.memoizedState?($n(t),et(t),zm(t,n)):(et(t),t.flags&=-16777217):(e.memoizedProps!==i&&$n(t),et(t),t.flags&=-16777217),null;case 27:pn(t),n=zt.current;var o=t.type;if(e!==null&&t.stateNode!=null)e.memoizedProps!==i&&$n(t);else{if(!i){if(t.stateNode===null)throw Error(s(166));return et(t),null}e=Ue.current,Ai(t)?ch(t):(e=zp(o,i,n),t.stateNode=e,$n(t))}return et(t),null;case 5:if(pn(t),n=t.type,e!==null&&t.stateNode!=null)e.memoizedProps!==i&&$n(t);else{if(!i){if(t.stateNode===null)throw Error(s(166));return et(t),null}if(e=Ue.current,Ai(t))ch(t);else{switch(o=Qs(zt.current),e){case 1:e=o.createElementNS("http://www.w3.org/2000/svg",n);break;case 2:e=o.createElementNS("http://www.w3.org/1998/Math/MathML",n);break;default:switch(n){case"svg":e=o.createElementNS("http://www.w3.org/2000/svg",n);break;case"math":e=o.createElementNS("http://www.w3.org/1998/Math/MathML",n);break;case"script":e=o.createElement("div"),e.innerHTML="
+
diff --git a/src/icon/server/hardware_processing/hardware_controller.py b/src/icon/server/hardware_processing/hardware_controller.py
index d972a57d..1dd27739 100644
--- a/src/icon/server/hardware_processing/hardware_controller.py
+++ b/src/icon/server/hardware_processing/hardware_controller.py
@@ -69,3 +69,29 @@ def run(self, *, sequence: str) -> ResultDict:
else {},
"shot_channels": results.shot_channels,
}
+
+ def get_ttl_masks(self) -> tuple[int, int]:
+ """Return (high_mask, low_mask) from the hardware via the ttlMasks RPC call."""
+ if not self.connected:
+ self.connect()
+ if not self.connected:
+ if HAS_TIQI_ZEDBOARD:
+ raise RuntimeError("Could not connect to the Zedboard")
+ raise RuntimeError(
+ "Tiqi zedboard package is not available. "
+ "Please use 'uv sync --all-extras' to install all dependencies"
+ )
+ return self._zedboard._invoke("ttlMasks") # type: ignore[union-attr]
+
+ def set_ttl_masks(self, high_mask: int, low_mask: int) -> None:
+ """Write (high_mask, low_mask) to the hardware via the setTTLMasks RPC call."""
+ if not self.connected:
+ self.connect()
+ if not self.connected:
+ if HAS_TIQI_ZEDBOARD:
+ raise RuntimeError("Could not connect to the Zedboard")
+ raise RuntimeError(
+ "Tiqi zedboard package is not available. "
+ "Please use 'uv sync --all-extras' to install all dependencies"
+ )
+ self._zedboard._invoke("setTTLMasks", high_mask, low_mask) # type: ignore[union-attr]
diff --git a/tests/config.yaml b/tests/config.yaml
index 01711590..afe1103d 100644
--- a/tests/config.yaml
+++ b/tests/config.yaml
@@ -11,6 +11,8 @@ databases:
ssl: false
username: tester
verify_ssl: false
+ sqlite:
+ file: /tmp/icon.db
date:
timezone: Europe/Zurich
experiment_library:
@@ -22,6 +24,7 @@ experiment_library:
update_interval: 30
hardware:
host: localhost
+ n_ttl_channels: 32
port: 6098
health_check:
interval_seconds: 5.0
diff --git a/tests/server/__init__.py b/tests/server/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/server/api/test_ttl_controller.py b/tests/server/api/test_ttl_controller.py
new file mode 100644
index 00000000..7d0d64c0
--- /dev/null
+++ b/tests/server/api/test_ttl_controller.py
@@ -0,0 +1,125 @@
+"""Tests for TTLController mask encoding/decoding and state management."""
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from icon.server.api.ttl_controller import (
+ TTLController,
+ _decode_state,
+ _encode_state,
+)
+
+
+class TestDecodeState:
+ def test_off_when_low_bit_set(self) -> None:
+ assert _decode_state(0, high_mask=0b00, low_mask=0b01) == 0
+
+ def test_on_when_high_bit_set(self) -> None:
+ assert _decode_state(0, high_mask=0b01, low_mask=0b00) == 1
+
+ def test_control_when_both_bits_clear(self) -> None:
+ assert _decode_state(0, high_mask=0b00, low_mask=0b00) == 2
+
+ def test_channel_31_off(self) -> None:
+ low_mask = 1 << 31
+ assert _decode_state(31, high_mask=0, low_mask=low_mask) == 0
+
+ def test_channel_31_on(self) -> None:
+ high_mask = 1 << 31
+ assert _decode_state(31, high_mask=high_mask, low_mask=0) == 1
+
+ def test_channel_31_control(self) -> None:
+ assert _decode_state(31, high_mask=0, low_mask=0) == 2
+
+ def test_independent_of_other_channels(self) -> None:
+ # Channel 0 is ON; channel 1 is in CONTROL — decoding ch1 should give 2
+ high_mask = 0b01 # ch0 ON
+ low_mask = 0b00
+ assert _decode_state(1, high_mask, low_mask) == 2
+
+
+class TestEncodeState:
+ def test_set_off(self) -> None:
+ high, low = _encode_state(0, state=0, high_mask=0b01, low_mask=0b00)
+ assert not (high & 1) # high bit cleared
+ assert low & 1 # low bit set
+
+ def test_set_on(self) -> None:
+ high, low = _encode_state(0, state=1, high_mask=0b00, low_mask=0b01)
+ assert high & 1 # high bit set
+ assert not (low & 1) # low bit cleared
+
+ def test_set_control(self) -> None:
+ high, low = _encode_state(0, state=2, high_mask=0b01, low_mask=0b01)
+ assert not (high & 1) # high bit cleared
+ assert not (low & 1) # low bit cleared
+
+ def test_does_not_affect_other_channels(self) -> None:
+ # Channel 1 is ON (bit 1 set in high_mask)
+ high, low = _encode_state(0, state=0, high_mask=0b10, low_mask=0b00)
+ assert high & 0b10 # channel 1 still ON
+
+ def test_invalid_state_raises(self) -> None:
+ with pytest.raises(ValueError, match="Invalid TTL state"):
+ _encode_state(0, state=3, high_mask=0, low_mask=0)
+
+ def test_channel_31(self) -> None:
+ high, low = _encode_state(31, state=1, high_mask=0, low_mask=0)
+ assert high == (1 << 31)
+ assert low == 0
+
+
+class TestTTLController:
+ def _make_controller(self, hw_masks=(0, 0)):
+ with (
+ patch("icon.server.api.ttl_controller.HardwareController") as MockHW,
+ patch("icon.server.api.ttl_controller.TTLRepository") as MockRepo,
+ patch("icon.server.api.ttl_controller.get_config") as MockConfig,
+ ):
+ MockConfig.return_value.hardware.n_ttl_channels = 32
+ hw = MockHW.return_value
+ hw.get_ttl_masks.return_value = hw_masks
+ repo = MockRepo.return_value
+ repo.get_masks.return_value = hw_masks
+ ctrl = TTLController()
+ ctrl._hw = hw
+ ctrl._repo = repo
+ ctrl._n_channels = 32
+ return ctrl, hw, repo
+
+ def test_get_states_all_control_when_masks_zero(self) -> None:
+ ctrl, hw, _ = self._make_controller((0, 0))
+ states = ctrl.get_states()
+ assert all(s == 2 for s in states)
+ assert len(states) == 32
+
+ def test_get_states_channel_0_on(self) -> None:
+ ctrl, hw, _ = self._make_controller((0b01, 0b00))
+ states = ctrl.get_states()
+ assert states[0] == 1
+ assert states[1] == 2
+
+ def test_get_states_falls_back_to_db_on_error(self) -> None:
+ ctrl, hw, repo = self._make_controller()
+ hw.get_ttl_masks.side_effect = RuntimeError("no hardware")
+ repo.get_masks.return_value = (0b01, 0b00)
+ states = ctrl.get_states()
+ assert states[0] == 1
+
+ def test_set_state_writes_to_hw_and_persists(self) -> None:
+ ctrl, hw, repo = self._make_controller((0, 0))
+ with patch("icon.server.api.ttl_controller.emit_queue"):
+ ctrl.set_state(0, 1)
+ hw.set_ttl_masks.assert_called_once_with(0b01, 0b00)
+ repo.save_masks.assert_called_once_with(0b01, 0b00)
+
+ def test_set_state_invalid_channel_raises(self) -> None:
+ ctrl, _, _ = self._make_controller()
+ with pytest.raises(ValueError, match="out of range"):
+ ctrl.set_state(32, 1)
+
+ def test_get_masks_returns_dict(self) -> None:
+ ctrl, hw, _ = self._make_controller((0xAB, 0xCD))
+ result = ctrl.get_masks()
+ assert result == {"high_mask": 0xAB, "low_mask": 0xCD}