Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
228c18d
HDF5/SQLite test data generator
antti-ethz Mar 19, 2026
4cf61fb
Curve fitting engine
antti-ethz Mar 19, 2026
6311196
Persist fit results in HDF5 and expose run_fit/delete_fit API
antti-ethz Mar 20, 2026
5228c80
Fit panel UI with plot overlay and click-to-guide
antti-ethz Mar 20, 2026
bda5d4e
Tests for curve fitting
antti-ethz Mar 23, 2026
af221cf
Improve fit panel
antti-ethz Mar 23, 2026
b4702e9
Move fit curve generation to backend, remove frontend model evaluation
antti-ethz Apr 14, 2026
d5eef5d
Auto-fit new jobs using the fit model from previous run of same exper…
antti-ethz Apr 14, 2026
ea2c535
npm run build
antti-ethz Apr 14, 2026
7ea6a52
fix: make hdf5 file access more robust
g-braeunlich May 8, 2026
878162c
adding device parameter group to ExperimentMetadata to be able to con…
FlorenceBerterottiere May 2, 2026
7c6f281
changes to ExperimentJobProxy to be able to live plot the data in a n…
FlorenceBerterottiere May 3, 2026
32ddd10
fix error 'sqlalchemy.exc.MultipleResultsFound: Multiple rows were fo…
May 6, 2026
e7d2bf3
Always write plot window metadata for shot and vector channel groups
antti-ethz Apr 27, 2026
067e16e
adding get parameter by id to parameters controller
May 6, 2026
4eac2f5
adding Antti's fix for experiments with different priorities
May 8, 2026
2865a85
Merge remote-tracking branch 'origin/87-experiments-sometimes-going-s…
May 14, 2026
06e1ec3
fixing hdf5 group param access error
May 8, 2026
f9c5c20
decoding data if recieved data is in bytes
May 12, 2026
923c6fc
adding a parameter controller class for the client to have control ov…
May 12, 2026
98a2e7c
map the display name of the parameters in icon client to shorter names
May 12, 2026
bb02fc1
tentative stalling fix, from _update_metadata_and_display_group being…
May 12, 2026
9a0694a
tentative stalling fix, from updating experiment metadata being calle…
May 12, 2026
0704a00
tentative stalling fix, trying to make loop more robust to cancelled …
May 12, 2026
8616e25
tentative stalling fix, trying to make loop more robust to cancelled …
May 12, 2026
8e17919
adding exception handling for method run of class Scheduler
May 12, 2026
d924563
fix ordering of the display groups
May 14, 2026
ad3b969
Merge remote-tracking branch 'origin/16-function-fitting-in-job-view'…
May 14, 2026
353794f
fix import errors from merge
May 14, 2026
fe32889
chore: update uv.lock to require Python >=3.12
May 18, 2026
154b827
feat(server): add TTL channel control via pydase API
May 18, 2026
6f6263b
feat(frontend): add TTL channel control page with ionizer-style two-b…
May 18, 2026
2deca61
fix(migrations): rebase TTL migration onto f60d837b7263
May 18, 2026
3c49b29
add back imports and removing duplicated lines added by mistake when …
May 19, 2026
1ea91d5
npm run build, warning this has the TTL tab + the fitting changes!
May 19, 2026
d989ca9
Regenerate scan values when pattern dropdown changes
antti-ethz May 15, 2026
226e622
npm run build. Warning this contains TTL tab + fitting branch
May 19, 2026
ce8f550
modif lock file
May 19, 2026
63af648
Merge remote-tracking branch 'origin/dev/srm' into test/fitting
May 20, 2026
e960472
npm run build
FlorenceBerterottiere May 20, 2026
f066f3b
make storing of nb of shots more flexible to accept some shots being …
May 20, 2026
6346ba2
fix filelock error from merging
May 20, 2026
ed182ef
fix: histogram memo never cached; remove stale LOCK_EXTENSION reference
May 20, 2026
78cf343
perf: eliminate O(N²) accumulation as live data grows
May 20, 2026
177d55a
Sort 1D scan data by parameter value for live display
May 26, 2026
f331dee
add color bar properly displayed, and with bounds updated in real tim…
May 26, 2026
f6ddf5b
Prevent histogram from freezing the browser on large/long shot channels
antti-ethz Jun 1, 2026
8e90d0e
added the option to scan either in start/stop mode, or with center/sp…
May 26, 2026
748417f
feat: add configurable hardware connection timeout
Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions docs/ttl_control.md
Original file line number Diff line number Diff line change
@@ -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).
128 changes: 128 additions & 0 deletions docs/ttl_server_implementation.md
Original file line number Diff line number Diff line change
@@ -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()`.
7 changes: 7 additions & 0 deletions frontend/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
testEnvironment: "node",
transform: {
"^.+.tsx?$": ["ts-jest", {}],
},
};
6 changes: 6 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -57,6 +58,11 @@ const NAVIGATION: Navigation = [
</SvgIcon>
),
},
{
segment: "ttl",
title: "TTL",
icon: <ToggleOnIcon />,
},
{
kind: "divider",
},
Expand Down
Loading