Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
314131a
feat: python scripting for queue server type handling
JMit-dev Nov 25, 2025
409e448
fix: type conversion for unchecked parameters
JMit-dev Nov 26, 2025
0561325
fix: console output does not jumble text with progress bars
JMit-dev Nov 26, 2025
a3538b4
fix: running plan display persists throughout cycle
JMit-dev Nov 26, 2025
79b7fe0
style: removed focus from all ui components except tables
JMit-dev Dec 3, 2025
6202ac9
refactor: logging is consistent and respectfully to console output
JMit-dev Dec 3, 2025
4f4cab9
feat: added drag selection to history widget
JMit-dev Dec 3, 2025
ba634ef
fix: pane dragging is relative to mouse not parent
JMit-dev Dec 3, 2025
fd69b91
feat: added web socket message DTOs for status, info, and console output
JMit-dev Dec 5, 2025
96c8329
feat: generic queue server websocket client with factory methods in r…
JMit-dev Dec 5, 2025
cde53de
feat: add preferences for web sockets
JMit-dev Dec 5, 2025
a91df17
fix: autoscroll checkbox issue where it will uncheck with websockets …
JMit-dev Dec 5, 2025
5c8b54a
feat: added status websocket messages
JMit-dev Dec 5, 2025
38ab11a
fix: plan editor python type representation fixed
JMit-dev Dec 9, 2025
e5de83b
fix: preserve parameter schema order in plan queue item
JMit-dev Dec 9, 2025
f3d8b68
chore: update queue server api key name, add url env var, support env…
JMit-dev Dec 9, 2025
63bf834
docs: update queue server docs
JMit-dev Dec 9, 2025
e53c78f
feat: add polling intervals as preference
JMit-dev Dec 9, 2025
183397a
feat: throttle websocket ui updates as the same rate as polling
JMit-dev Dec 9, 2025
e9201e1
feat: timeout in connection manager
JMit-dev Dec 9, 2025
a16ccc9
fix: clear ui when disconnected, disable widgets if environment is cl…
JMit-dev Dec 9, 2025
4eb664f
feat: disable plan viewer/editor when environment is closed
JMit-dev Dec 9, 2025
102c08d
feat: added timeout while the client is connected to the queue server
JMit-dev Dec 9, 2025
b4e170d
fix: removed javafx thread blocking service calls on connection
JMit-dev Dec 9, 2025
82714be
fix: lazy intialization for jython to prevent javafx thread blocking
JMit-dev Dec 9, 2025
9a374bd
fix: python type converter javafx thread blocking fixed
JMit-dev Dec 19, 2025
c9fb1cd
fix: remove javafx thread blocking in plan loading and validation
JMit-dev Dec 19, 2025
f2b56e9
refactor: console monitor tab only open by default
JMit-dev Dec 19, 2025
13ac094
refactor: autoscroll checked by default in console monitor
JMit-dev Dec 19, 2025
fe5c94d
feat: autoscroll text turns red when disabled
JMit-dev Dec 19, 2025
9f1a139
fix: autoscroll uses events to check if user scrolled to disable
JMit-dev Dec 19, 2025
9bd6a16
feat: added auto connect manager widget
JMit-dev Dec 23, 2025
d583861
fix: status widgets are not cleared when offline
JMit-dev Dec 23, 2025
58b35b1
fix: auto connect on startup
JMit-dev Dec 23, 2025
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
142 changes: 76 additions & 66 deletions app/queue-server/README.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,110 @@
# Java Bluesky Interface (JBI)
# Phoebus Queue Server Application

Prototype Java client for the [Bluesky QueueServer](https://blueskyproject.io/bluesky-queueserver/) REST API.
It includes:
A JavaFX-based client for the [Bluesky QueueServer](https://blueskyproject.io/bluesky-queueserver/) that provides a graphical interface for managing experiment queues, monitoring status, and editing plans at beamlines and scientific facilities.

* **BlueskyHttpClient** – thread-safe singleton with rate-limiting, automatic retries and JUL logging
* **BlueskyService** – one blocking call per API route
* **CLI / REPL** – *testing utilities only*
* **JavaFX UI** – the part you actually launch in normal use
## Features

---
### Queue Management
- **View & Edit Queue**: See all queued plans with parameters, reorder items via drag-and-drop
- **Execute Plans**: Start, stop, pause, resume, and abort plan execution
- **Real-time Status**: Live updates via WebSocket connections showing queue state, running plans, and RE Manager status
- **History View**: Browse completed plans with execution results and metadata

## Prerequisites
### Plan Editing
- **Interactive Plan Editor**: Create and modify plans with type-safe parameter editing
- **Python Type Support**: Full support for Python types (strings, lists, dicts, booleans, numbers)
- **Schema Validation**: Parameters validated against plan schemas from the Queue Server
- **Live Preview**: See plan parameters as they will be sent to the server

* Java 17
* Maven
* Running Bluesky HTTP Server at `http://localhost:60610`
*single-user API key `a` assumed below*
### Plan Viewer
- **Plan Details**: View plan parameters, metadata, and execution results
- **Parameter Display**: All parameters shown with correct Python syntax and types
- **Copy to Queue**: Duplicate plans with one click

```bash
# 1) Start RE Manager
start-re-manager --use-ipython-kernel=ON --zmq-publish-console=ON
### Console Monitor
- **Live Console Output**: Real-time streaming of Queue Server console output
- **WebSocket Support**: Efficient streaming via WebSocket connections (fallback to HTTP polling)
- **Autoscroll**: Automatically scroll to latest output with toggle control

## Quick Start

### Prerequisites

# 2) Start HTTP Server
QSERVER_HTTP_SERVER_SINGLE_USER_API_KEY=a \
uvicorn --host localhost --port 60610 bluesky_httpserver.server:app
````
- **Java 17** or later
- **Maven** (for building from source)
- **Bluesky Queue Server** running and accessible

---
### Starting a Local Queue Server

## Configure
Use the provided Docker setup for local development:

```bash
export BLUESKY_API_KEY=a
cd services/bluesky-services
docker-compose --profile container-redis up -d
```

---
This starts:
- Bluesky Queue Server (RE Manager) on ports 60615/60625
- HTTP Server REST API on port 60610
- Redis database on port 6380

## Build & run
For details, see [services/bluesky-services/README.md](../../services/bluesky-services/README.md)

```bash
mvn clean javafx:run
```
### Configuration

### Verbose logging
Set environment variables to connect to your Queue Server:

```bash
mvn -Djava.util.logging.config.file=src/main/resources/logging.properties javafx:run
```
# Queue Server HTTP address (default: http://localhost:60610)
export QSERVER_HTTP_SERVER_URI=http://localhost:60610

---
# API Key authentication
export QSERVER_HTTP_SERVER_API_KEY=a

## CLI / REPL (testing only)
# Or use a file containing the API key
export QSERVER_HTTP_SERVER_API_KEYFILE=~/.phoebus/qserver_api_key.txt
```

| Tool | Start (quiet) | Start with request tracing (`FINE`) |
| -------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **CLI** | `mvn -q -Dexec.mainClass=util.org.phoebus.applications.queueserver.RunEngineCli -Dexec.args="STATUS" exec:java` | `mvn -Djava.util.logging.config.file=src/main/resources/logging.properties -q -Dexec.mainClass=util.org.phoebus.applications.queueserver.RunEngineCli -Dexec.args="STATUS" exec:java` |
| **REPL** | `mvn -q -Dexec.mainClass=util.org.phoebus.applications.queueserver.RunEngineRepl exec:java` | `mvn -Djava.util.logging.config.file=src/main/resources/logging.properties -q -Dexec.mainClass=util.org.phoebus.applications.queueserver.RunEngineRepl -Dexec.args="STATUS" exec:java` |
### Building & Running

*CLI examples*
#### As Part of Phoebus

```bash
# list endpoints
mvn -q -Dexec.mainClass=com.jbi.util.RunEngineClili -Dexec.args="list" exec:java

# start the queue
mvn -q -Dexec.mainClass=com.jbi.util.RunEngineClili -Dexec.args="QUEUE_START" exec:java
# From phoebus root directory
mvn clean install -DskipTests
cd phoebus-product/target
./phoebus
```

`ENDPOINT [body]` accepts a JSON literal, `@file.json`, or `@-` for stdin.
Then open **Applications → Queue Server** from the menu.

---
#### Standalone Build

## How logging works
```bash
# From queue-server directory
cd app/queue-server
mvn clean install
```

* Logger name: **`com.jbi.bluesky`**
* Levels
## Configuration Options

* `INFO` – API errors
* `WARNING` – transport retries
* `FINE` – each HTTP call + latency
* Enable by passing JVM flag
`-Djava.util.logging.config.file=src/main/resources/logging.properties`
Configuration via **Edit → Preferences → Queue Server** or environment variables:

---
| Preference | Environment Variable | Default | Description |
|-------------------|-----------------------------------|--------------------------|---------------------------------------|
| `queue_server_url`| `QSERVER_HTTP_SERVER_URI` | `http://localhost:60610` | Queue Server HTTP address |
| `api_key` | `QSERVER_HTTP_SERVER_API_KEY` | *(none)* | API key for authentication |
| `api_key_file` | `QSERVER_HTTP_SERVER_API_KEYFILE` | *(none)* | Path to file containing API key |
| `use_websockets` | *(none)* | `true` | Use WebSockets for streaming data |
| `connectTimeout` | *(none)* | `5000` | HTTP connection timeout (ms) |
| `debug` | *(none)* | `false` | Enable HTTP request/response logging |

## Tuning
## Contributing

```java
// rate limit (req/sec)
BlueskyHttpClient.initialize("http://localhost:60610",
System.getenv("BLUESKY_API_KEY"),
3.0);
When making changes:

// retry/back-off
// edit src/main/java/com/jbi/util/HttpSupport.java
MAX_RETRIES = 5;
INITIAL_BACKOFF_MS = 500;
BACKOFF_MULTIPLIER = 1.5;
```
1. Ensure proper Python type handling in parameter editor
2. Test with both WebSocket and HTTP fallback modes
3. Verify API key authentication works
4. Update documentation for new features
5. Follow existing code style and patterns
6 changes: 6 additions & 0 deletions app/queue-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.19.1</version>
</dependency>
<!-- Jython for Python scripting -->
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>${jython.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ public class Preferences {
@Preference
public static String api_key;

@Preference
public static String api_key_file;

@Preference
public static boolean debug;

@Preference
public static int connectTimeout;

@Preference
public static boolean use_websockets;

@Preference
public static int update_interval_ms;

static
{
AnnotatedPreferences.initialize(Preferences.class, "/queueserver_preferences.properties");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package org.phoebus.applications.queueserver;

import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.phoebus.applications.queueserver.client.RunEngineHttpClient;
Expand All @@ -16,7 +21,7 @@
@SuppressWarnings("nls")
public final class QueueServerApp implements AppResourceDescriptor {

public static final Logger LOGGER = Logger.getLogger(QueueServerApp.class.getPackageName());
public static final Logger logger = Logger.getLogger(QueueServerApp.class.getPackageName());

public static final String NAME = "queue-server";
private static final String DISPLAY_NAME = "Queue Server";
Expand All @@ -30,7 +35,20 @@ public final class QueueServerApp implements AppResourceDescriptor {

@Override public AppInstance create() {

RunEngineHttpClient.initialize(Preferences.queue_server_url, Preferences.api_key);
// Resolve server URL with default fallback
String serverUrl = Preferences.queue_server_url;
// Check if the preference wasn't expanded (still has $(VAR) syntax) or is empty
if (serverUrl == null || serverUrl.trim().isEmpty() || serverUrl.startsWith("$(")) {
serverUrl = "http://localhost:60610";
logger.log(Level.INFO, "Using default Queue Server URL: " + serverUrl);
}

// Resolve API key with priority:
// 1. Direct api_key preference (or QSERVER_HTTP_SERVER_API_KEY env var)
// 2. Read from api_key_file path (or QSERVER_HTTP_SERVER_API_KEYFILE env var)
String apiKey = resolveApiKey();

RunEngineHttpClient.initialize(serverUrl, apiKey);

Parent root = ViewFactory.APPLICATION.get();

Expand All @@ -42,6 +60,45 @@ public final class QueueServerApp implements AppResourceDescriptor {
return inst;
}

/**
* Resolve the API key using the same priority as Python bluesky-widgets:
* 1. Check QSERVER_HTTP_SERVER_API_KEY environment variable (via api_key preference)
* 2. If not set, check QSERVER_HTTP_SERVER_API_KEYFILE environment variable (via api_key_file preference)
* 3. If keyfile path is set, read the API key from that file
*
* @return The resolved API key, or null if not configured
*/
private static String resolveApiKey() {
// First priority: direct API key
String apiKey = Preferences.api_key;
// Check if the preference was expanded (not still $(VAR) syntax) and not empty
if (apiKey != null && !apiKey.trim().isEmpty() && !apiKey.startsWith("$(")) {
logger.log(Level.FINE, "Using API key from QSERVER_HTTP_SERVER_API_KEY");
return apiKey.trim();
}

// Second priority: read from keyfile
String keyFilePath = Preferences.api_key_file;
if (keyFilePath != null && !keyFilePath.trim().isEmpty() && !keyFilePath.startsWith("$(")) {
try {
Path path = Paths.get(keyFilePath.trim());
if (Files.exists(path)) {
apiKey = Files.readString(path).trim();
logger.log(Level.FINE, "Using API key from file: " + keyFilePath);
return apiKey;
} else {
logger.log(Level.WARNING, "API key file not found: " + keyFilePath);
}
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to read API key from file: " + keyFilePath, e);
}
}

logger.log(Level.WARNING, "No API key configured. Set QSERVER_HTTP_SERVER_API_KEY environment variable " +
"or QSERVER_HTTP_SERVER_API_KEYFILE to point to a file containing the API key.");
return null;
}

@Override public AppInstance create(java.net.URI resource) {
return ApplicationService.createInstance(NAME);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.phoebus.applications.queueserver.api;

import com.fasterxml.jackson.annotation.JsonProperty;

/**
* WebSocket message from /console_output/ws endpoint.
* Format: {"time": timestamp, "msg": text}
*/
public record ConsoleOutputWsMessage(
@JsonProperty("time") double time,
@JsonProperty("msg") String msg
) {
/**
* Get timestamp as milliseconds since epoch.
*/
public long timestampMillis() {
return (long) (time * 1000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.phoebus.applications.queueserver.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;

/**
* WebSocket message from /status/ws endpoint.
* Format: {"time": timestamp, "msg": {"status": {...}}}
*/
public record StatusWsMessage(
@JsonProperty("time") double time,
@JsonProperty("msg") Map<String, Object> msg
) {
/**
* Get timestamp as milliseconds since epoch.
*/
public long timestampMillis() {
return (long) (time * 1000);
}

/**
* Get the status payload from the message.
* Returns null if not present.
*/
@SuppressWarnings("unchecked")
public Map<String, Object> status() {
if (msg == null) return null;
return (Map<String, Object>) msg.get("status");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.phoebus.applications.queueserver.api;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;

/**
* WebSocket message from /info/ws endpoint.
* Format: {"time": timestamp, "msg": {msg-class: msg-content}}
* Currently includes status messages and may include additional system info in the future.
*/
public record SystemInfoWsMessage(
@JsonProperty("time") double time,
@JsonProperty("msg") Map<String, Object> msg
) {
/**
* Get timestamp as milliseconds since epoch.
*/
public long timestampMillis() {
return (long) (time * 1000);
}

/**
* Get the status payload if this is a status message.
* Returns null if not a status message.
*/
@SuppressWarnings("unchecked")
public Map<String, Object> status() {
if (msg == null) return null;
return (Map<String, Object>) msg.get("status");
}
}
Loading