From 850cf5330681f5930f8852cb458bec87b56419c2 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 17 Apr 2025 15:44:01 +0200 Subject: [PATCH 01/15] Close gap to original pvws project --- .../org/phoebus/pvws/PvwsWebListener.java | 1 - .../java/org/phoebus/pvws/ws/Vtype2Json.java | 100 ++++++++++++++++- src/main/resources/static/js/pvws.js | 104 +++++++++++++++--- 3 files changed, 181 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/phoebus/pvws/PvwsWebListener.java b/src/main/java/org/phoebus/pvws/PvwsWebListener.java index b176565..8137e74 100644 --- a/src/main/java/org/phoebus/pvws/PvwsWebListener.java +++ b/src/main/java/org/phoebus/pvws/PvwsWebListener.java @@ -38,7 +38,6 @@ public class PvwsWebListener implements ServletContextListener { @Override public void contextDestroyed(ServletContextEvent event) { - System.out.println("sdjhgfdfugh"); WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); sockets = (List) context.getBean("sockets"); if (sockets == null) { diff --git a/src/main/java/org/phoebus/pvws/ws/Vtype2Json.java b/src/main/java/org/phoebus/pvws/ws/Vtype2Json.java index 5cecaf1..7170d77 100644 --- a/src/main/java/org/phoebus/pvws/ws/Vtype2Json.java +++ b/src/main/java/org/phoebus/pvws/ws/Vtype2Json.java @@ -17,7 +17,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.DoubleBuffer; +import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.nio.ShortBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; @@ -58,15 +60,15 @@ else if (value instanceof VString) else if (value instanceof VEnum) handleEnum(g, (VEnum) value, last_value); else if (value instanceof VByteArray) - handleLongString(g, (VByteArray) value); + handleBytes(g, (VNumberArray) value, last_value); // Serialize double and float arrays as b64dbl else if (value instanceof VDoubleArray) handleDoubles(g, (VNumberArray) value, last_value); else if (value instanceof VFloatArray) - handleDoubles(g, (VNumberArray) value, last_value); + handleFloats(g, (VNumberArray) value, last_value); - // Serialize remaining number arrays (int, short) as b64int + // Serialize remaining number arrays (int) as b64int else if (value instanceof VNumberArray) handleInts(g, (VNumberArray) value, last_value); @@ -239,7 +241,7 @@ private static void handleInts(final JsonGenerator g, final VNumberArray value, { // Add severity if it changed if ((last_value instanceof VNumber) && - ((VNumber) last_value).getAlarm().getSeverity() != severity) + ((VNumber) last_value).getAlarm().getSeverity() != severity) g.writeStringField("severity", severity.name()); } @@ -256,6 +258,7 @@ private static void handleInts(final JsonGenerator g, final VNumberArray value, } + private static void handleEnum(final JsonGenerator g, final VEnum value, final VType last_value) throws Exception { final AlarmSeverity severity = value.getAlarm().getSeverity(); @@ -275,11 +278,98 @@ private static void handleEnum(final JsonGenerator g, final VEnum value, final V { // Add severity if it changed if ((last_value instanceof VNumber) && - ((VNumber) last_value).getAlarm().getSeverity() != severity) + ((VNumber) last_value).getAlarm().getSeverity() != severity) g.writeStringField("severity", severity.name()); } g.writeNumberField("value", value.getIndex()); g.writeStringField("text", value.getValue()); } + + private static void handleFloats(final JsonGenerator g, final VNumberArray value, final VType last_value) throws Exception + { + final AlarmSeverity severity = value.getAlarm().getSeverity(); + if (last_value == null) + { + // Initially, add complete metadata + g.writeStringField("vtype", VType.typeOf(value).getSimpleName()); + handleDisplay(g, value.getDisplay()); + // Initial severity + g.writeStringField("severity", severity.name()); + } + else + { + // Add severity if it changed + if ((last_value instanceof VNumber) && + ((VNumber) last_value).getAlarm().getSeverity() != severity) + g.writeStringField("severity", severity.name()); + } + + final ListNumber data = value.getData(); + final int N = data.size(); + final ByteBuffer buf = ByteBuffer.allocate(N * Float.BYTES); + buf.order(ByteOrder.LITTLE_ENDIAN); + final FloatBuffer fltbuf = buf.asFloatBuffer(); + for (int i=0; i this.handleConnection(); + this.socket.onopen = event => this.handleConnection(event); this.socket.onmessage = event => this.handleMessage(event.data); this.socket.onclose = event => this.handleClose(event); this.socket.onerror = event => this.handleError(event); } - - handleConnection() + + handleConnection(event) { console.log("Connected to " + this.url); this.connect_handler(true); + + // Start idle check + if (this.idle_timer == null) + this.idle_timer = setInterval(() => this.checkIdleTimeout(), this.idle_check_ms); } - + + checkIdleTimeout() + { + if (this.idle) + { + // console.log("Idle connection " + this.url); + this.ping(); + } + else + { + // console.log("Active connection " + this.url); + // Reset to detect new messages + this.idle = true; + } + } + + stopIdleCheck() + { + if (this.idle_timer != null) + clearInterval(this.idle_timer); + this.idle_timer = null; + } + handleMessage(message) { // console.log("Received Message: " + message); + this.idle = false; let jm = JSON.parse(message); if (jm.type === "update") { @@ -62,6 +103,22 @@ class PVWS // console.log(JSON.stringify(jm.value)); delete jm.b64dbl; } + else if (jm.b64flt !== undefined) + { + let bytes = toByteArray(jm.b64flt); + jm.value = new Float32Array(bytes.buffer); + // Convert to plain array + jm.value = Array.prototype.slice.call(jm.value); + delete jm.b64flt; + } + else if (jm.b64srt !== undefined) + { + let bytes = toByteArray(jm.b64srt); + jm.value = new Int16Array(bytes.buffer); + // Convert to plain array + jm.value = Array.prototype.slice.call(jm.value); + delete jm.b64srt; + } else if (jm.b64int !== undefined) { let bytes = toByteArray(jm.b64int); @@ -70,14 +127,22 @@ class PVWS jm.value = Array.prototype.slice.call(jm.value); delete jm.b64int; } - + else if (jm.b64byt !== undefined) + { + let bytes = toByteArray(jm.b64byt); + jm.value = new Uint8Array(bytes.buffer); + // Convert to plain array, if necessary + jm.value = Array.prototype.slice.call(jm.value); + delete jm.b64byt; + } + // Merge received data with last known value let value = this.values[jm.pv]; // No previous value: // Default to read-only, no data if (value === undefined) value = { pv: jm.pv, readonly: true }; - + // Update cached value with received changes Object.assign(value, jm); this.values[jm.pv] = value; @@ -94,9 +159,10 @@ class PVWS console.error(event); this.close(); } - + handleClose(event) { + this.stopIdleCheck(); this.connect_handler(false); let message = "Web socket closed (" + event.code ; if (event.reason) @@ -114,9 +180,10 @@ class PVWS */ ping() { + console.log("Sending ping to " + this.url); this.socket.send(JSON.stringify({ type: "ping" })) } - + /** Subscribe to one or more PVs * @param pvs PV name or array of PV names */ @@ -124,7 +191,6 @@ class PVWS { if (pvs.constructor !== Array) pvs = [ pvs ]; - // TODO Remember all PVs so we can re-subscribe after close/re-open this.socket.send(JSON.stringify({ type: "subscribe", pvs: pvs })); } @@ -135,21 +201,20 @@ class PVWS { if (pvs.constructor !== Array) pvs = [ pvs ]; - // TODO Forget PVs so we don't re-subscribe after close/re-open this.socket.send(JSON.stringify({ type: "clear", pvs: pvs })); - + // Remove entry for cleared PVs from this.values let pv; for (pv of pvs) delete this.values[pv]; } - + /** Request list of PVs */ list() { this.socket.send(JSON.stringify({ type: "list" })); } - + /** Write to PV * @param pvs PV name * @param value number or string @@ -158,17 +223,20 @@ class PVWS { this.socket.send(JSON.stringify({ type: "write", pv: pv, value: value })); } - + /** Close the web socket. - * + * *

Socket will automatically re-open, * similar to handling an error. */ close() { + this.stopIdleCheck(); this.socket.close(); } } -// TODO Larger timeout for production setup -PVWS.prototype.reconnect_ms = 5000; +// Attempt re-connect after 10 seconds +PVWS.prototype.reconnect_ms = 10000; +// Perform idle check every 30 secs +PVWS.prototype.idle_check_ms = 30000; \ No newline at end of file From 5554491c1603046f0e4e5ff15531187962fde49c Mon Sep 17 00:00:00 2001 From: georgweiss Date: Thu, 17 Apr 2025 16:25:58 +0200 Subject: [PATCH 02/15] Update README --- README.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/README.md b/README.md index 5889798..411d203 100644 --- a/README.md +++ b/README.md @@ -1 +1,72 @@ This is an attempt to port the original work https://github.com/ornl-epics/pvws to a Spring Boot-based web application. While the code is adapted to the web socket APIs of Spring Boot, most of the code is essentially unchanged. + +Requirements +------------ + +To build: JDK 17, Maven 3.8+ + +To run: JRE 17 + +Build +===== + +Executable jar +-------------- + +To build an executable Spring Boot jar (embedded Tomcat): + +``>mvn clean install`` + +Output is ``pvws.jar`` in the ``target`` directory. + +Tomcat war +---------- + +To build war for deployment in Tomcat container: + +``>mvn -Pwar clean install`` + +Output is ``pvws.war`` in the ``target`` directory. + +Run +=== + +Settings +-------- + +Bundled ``application.properties`` defines a few settings (e.g. default protocol). To define other settings or override +settings, one may create a file named exactly ``application.properties`` and edit as needed. This +file must be readable by the user account owning the application process. + +Executable jar +-------------- + +Launch like so: + +``>java -Dspring.config.location=file:/path/to/directory/ -jar /path/to/pvws.jar`` + +where ``/path/to/directory/`` is the directory holding ``application.properties``. Note that +this string **must** end in a slash ("/"). + +It is also possible to define properties on the command line, e.g. + +``>java -Dspring.config.location=file:/path/to/directory/ -DEPICS_PVA_ADDR_LIST=1.2.3.4 -jar /path/to/pvws.jar`` + +Combining ``application.properties`` with JVM options is possible. +Command line options will override definitions in ``application.properties``. + +Tomcat war +---------- + +**NOTE:** Verified on Tomcat 9 only. + +Copy ``pvws.war`` to ``TOMCAT_ROOT/webapps``. Set environment variable: + +``>export JAVA_OPTS=-Dspring.config.location=file:/path/to/directory/`` + +and launch Tomcat. + + + + + From 07d6b703db79750f7a0a23b90a8229a955187ab5 Mon Sep 17 00:00:00 2001 From: Georg Weiss <35602960+georgweiss@users.noreply.github.com> Date: Thu, 17 Apr 2025 16:32:15 +0200 Subject: [PATCH 03/15] Update README.md Clarification on ca settings --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 411d203..0b5bbe9 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,9 @@ Bundled ``application.properties`` defines a few settings (e.g. default protocol settings, one may create a file named exactly ``application.properties`` and edit as needed. This file must be readable by the user account owning the application process. +**NOTE:** If "ca" (channel access) is selected as default protocol, associated settings (e.g. EPICS_CA_ADDR_LIST) **must** +be defined as environment variables. + Executable jar -------------- From 5ecb50d4cababf6a06927672e79dc0189893a6cc Mon Sep 17 00:00:00 2001 From: Kunal Shroff Date: Tue, 8 Jul 2025 13:02:09 -0400 Subject: [PATCH 04/15] Create Dockerfile Create a docker image for the pvws-springboot service --- Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6eaed47 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1 + +FROM eclipse-temurin:17-jdk AS builder +WORKDIR /build +RUN apt-get update && apt-get install -y maven +COPY . . +RUN mvn --batch-mode --update-snapshots clean package -DskipTests + +FROM eclipse-temurin:17-jre AS runner +WORKDIR /app +COPY --from=builder /build/target/pvws*.jar ./pvws.jar +CMD ["java", "-jar", "/app/pvws.jar", "--spring.config.name=application"] From c1e7f4596c5483f8c54452b5f295be6c03665572 Mon Sep 17 00:00:00 2001 From: Kunal Shroff Date: Tue, 8 Jul 2025 14:19:15 -0400 Subject: [PATCH 05/15] Update Dockerfile Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6eaed47..e1ae646 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM eclipse-temurin:17-jdk AS builder WORKDIR /build -RUN apt-get update && apt-get install -y maven +RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/* COPY . . RUN mvn --batch-mode --update-snapshots clean package -DskipTests From 6f0839e39a2af3de29f3cda52db2a5d5c67dfecd Mon Sep 17 00:00:00 2001 From: Kunal Shroff Date: Thu, 10 Jul 2025 15:08:01 -0400 Subject: [PATCH 06/15] Add a git ignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f187c67 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +.classpath +.project +.settings +.idea \ No newline at end of file From c413bd1ac98c736309e498c8d927304592acdac6 Mon Sep 17 00:00:00 2001 From: Kunal Shroff Date: Thu, 10 Jul 2025 15:08:31 -0400 Subject: [PATCH 07/15] Adding a compose file --- docker-compose.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8db3631 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ + +services: + pvws: + build: + context: . + dockerfile: Dockerfile + image: pvws-springboot:latest + network_mode: "host" + environment: + - EPICS_CA_AUTO_ADDR_LIST=YES + - EPICS_PVA_AUTO_ADDR_LIST=YES From 593affc91dee414f574fdceee412e3f07b281edb Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 11 Jul 2025 10:16:35 +0200 Subject: [PATCH 08/15] Change order when setting EPICS preferences --- src/main/java/org/phoebus/pvws/EpicsConfiguration.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/phoebus/pvws/EpicsConfiguration.java b/src/main/java/org/phoebus/pvws/EpicsConfiguration.java index 246773d..cb44aa2 100644 --- a/src/main/java/org/phoebus/pvws/EpicsConfiguration.java +++ b/src/main/java/org/phoebus/pvws/EpicsConfiguration.java @@ -65,8 +65,6 @@ public class EpicsConfiguration { public void init() { logger.log(Level.INFO, "==========================================="); logger.log(Level.INFO, contextPath + " started"); - logger.log(Level.INFO, "Supported PV types: " + PVPool.getSupportedPrefixes()); - // Set default type in preferences before PVPool reads the preferences String default_type = System.getenv("PV_DEFAULT_TYPE"); if (default_type != null && !default_type.isEmpty()) { @@ -77,6 +75,7 @@ else if (pvDefaultType != null && !pvDefaultType.isEmpty()) { Preferences.userRoot().node("/org/phoebus/pv").put("default", pvDefaultType); } logger.log(Level.INFO, "PV_DEFAULT_TYPE=" + Preferences.userRoot().node("/org/phoebus/pv").get("default", null)); + logger.log(Level.INFO, "Supported PV types: " + PVPool.getSupportedPrefixes()); Preferences.userRoot().node("/org/phoebus/pv/pva").put("epics_pva_addr_list", epicsPvaAddrList); From 1c24f6a6a25cd01de266885814d69c0c74e70448 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Tue, 23 Sep 2025 15:11:31 -0500 Subject: [PATCH 09/15] Add GET endpoint for one-shot pv value reads --- .../pvws/EpicsWebSocketServerApplication.java | 37 +++++++++++++++++++ .../java/org/phoebus/pvws/ws/WebSocketPV.java | 13 ++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java index 1b5c418..15c62a6 100644 --- a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java +++ b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java @@ -18,19 +18,28 @@ package org.phoebus.pvws; +import org.epics.vtype.VType; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.pv.RefCountMap; +import org.phoebus.pvws.ws.Vtype2Json; import org.phoebus.pvws.ws.WebSocket; +import org.phoebus.pvws.ws.WebSocketPV; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.ApplicationContext; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import java.util.List; @SpringBootApplication +@RestController public class EpicsWebSocketServerApplication extends SpringBootServletInitializer { public static void main(String[] args) { @@ -53,4 +62,32 @@ public static void main(String[] args) { protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(EpicsWebSocketServerApplication.class); } + + @GetMapping(value= "/pvget", produces = MediaType.APPLICATION_JSON_VALUE) + public String pvget(@RequestParam String name) { + final WebSocketPV pv = new WebSocketPV(name, null); + String ret; + try { + + // TODO make these configurable? + int maxAttempts = 100; + int retryDelay = 50; + + VType lastValue = null; + + for (int i = 0; i< maxAttempts; i++) { + Thread.sleep(retryDelay); + lastValue = pv.get(); + if (lastValue != null) break; + System.out.println("value is" + lastValue + "attempt no " + i); + } + + System.out.println("value is" + lastValue); + ret = Vtype2Json.toJson(name, lastValue, null, true, true); + } catch (final Exception ex) { + ret = String.format("Unable to get PV value for %s - exception %s", name, ex); + } + pv.dispose(); + return ret; + } } diff --git a/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java b/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java index d968e7d..b5e8b52 100644 --- a/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java +++ b/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java @@ -11,8 +11,6 @@ import org.epics.vtype.VType; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -171,6 +169,17 @@ public void write(Object new_value) throws Exception throw new Exception("PV_WRITE_SUPPORT is disabled"); } + /** + * Get current value by calling read() on the PV + * @return the current value + * @throws Exception on error + */ + public VType get() throws Exception + { + pv = PVPool.getPV(name); + return pv.read(); + } + /** Close PV */ public void dispose() { From 9bbc647384be734e5be31b23a171e6c0315aca8e Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Tue, 23 Sep 2025 20:49:06 -0500 Subject: [PATCH 10/15] remove prints and add docs --- README.md | 39 +++++++++++++++++++ .../pvws/EpicsWebSocketServerApplication.java | 6 --- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0b5bbe9..9dc9261 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,44 @@ This is an attempt to port the original work https://github.com/ornl-epics/pvws to a Spring Boot-based web application. While the code is adapted to the web socket APIs of Spring Boot, most of the code is essentially unchanged. +Endpoints +========= + + +`HTTP`/`HTTPS` +------ +`/pvws` + +This shows a webpage with documentation and test utilities. + +`pvws/info` + +This returns JSON information with general server info. + +`pvws/summary` + +This returns JSON with a summary of all active web sockets. + +`pvws/socket` + +This returns JSON with details on all active web sockets and their PVs. + +`pvws/pool` + +This returns JSON with a listing of all PVs in the PV connection pool. + +`pvws/pvget` + +This returns a single value read from a PV in the same JSON format as a websocket message. + +Takes the fully qualified PV address as the `name` parameter. + +`WS`/`WSS` +---------- + +`pvws/pv` + +This is the main websocket connection endpoint. See `/pvws` for information on commands. + Requirements ------------ diff --git a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java index 15c62a6..16b5ba3 100644 --- a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java +++ b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java @@ -68,21 +68,15 @@ public String pvget(@RequestParam String name) { final WebSocketPV pv = new WebSocketPV(name, null); String ret; try { - - // TODO make these configurable? int maxAttempts = 100; int retryDelay = 50; VType lastValue = null; - for (int i = 0; i< maxAttempts; i++) { Thread.sleep(retryDelay); lastValue = pv.get(); if (lastValue != null) break; - System.out.println("value is" + lastValue + "attempt no " + i); } - - System.out.println("value is" + lastValue); ret = Vtype2Json.toJson(name, lastValue, null, true, true); } catch (final Exception ex) { ret = String.format("Unable to get PV value for %s - exception %s", name, ex); From 4c3e56d7af64a3da530c499c1164662f23f285a1 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Tue, 23 Sep 2025 20:58:23 -0500 Subject: [PATCH 11/15] add github actions --- .github/workflows/build-and-test.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/build-and-test.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..baae61b --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,20 @@ +name: Build and test +on: [pull_request, workflow_call] + +jobs: + tests: + runs-on: {matrix.os} + strategy: + matrix: + os: [ "ubuntu-latest", "windows-latest"] + version: [21, 25] + fail-fast: false + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v4 + with: + java-version: {{matrix.version}} + distribution: 'temurin' + architecture: x64 + - name: Build with maven + run: mvn clean install From f0efc06b80b474a5620b16ddcf9a069b24a74832 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Tue, 23 Sep 2025 20:59:31 -0500 Subject: [PATCH 12/15] use double braces --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index baae61b..f893d11 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -3,7 +3,7 @@ on: [pull_request, workflow_call] jobs: tests: - runs-on: {matrix.os} + runs-on: ${{matrix.os}} strategy: matrix: os: [ "ubuntu-latest", "windows-latest"] From 6452628e36646922a0e45426c022db166448210c Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Tue, 23 Sep 2025 21:00:15 -0500 Subject: [PATCH 13/15] use dollar with version --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f893d11..5801f2e 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-java@v4 with: - java-version: {{matrix.version}} + java-version: ${{matrix.version}} distribution: 'temurin' architecture: x64 - name: Build with maven From f0b35450e93627b63e70617b35cbbc45377f6ed5 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Wed, 24 Sep 2025 10:28:51 -0500 Subject: [PATCH 14/15] move to rest controller, use isDisconnected() waiter, add configurable PV_READ_TIMEOUT --- .../org/phoebus/pvws/EpicsConfiguration.java | 5 +++ .../pvws/EpicsWebSocketServerApplication.java | 30 ----------------- .../pvws/controllers/PvwsRestController.java | 32 +++++++++++++++++++ .../java/org/phoebus/pvws/ws/WebSocketPV.java | 11 ------- src/main/resources/application.properties | 2 ++ 5 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/phoebus/pvws/EpicsConfiguration.java b/src/main/java/org/phoebus/pvws/EpicsConfiguration.java index cb44aa2..b48e9ad 100644 --- a/src/main/java/org/phoebus/pvws/EpicsConfiguration.java +++ b/src/main/java/org/phoebus/pvws/EpicsConfiguration.java @@ -61,6 +61,9 @@ public class EpicsConfiguration { @Value("${PV_WRITE_SUPPORT:true}") private String pvWriteSupport; + @Value("${PV_READ_TIMEOUT:5000}") + private String pvReadTimeout; + @PostConstruct public void init() { logger.log(Level.INFO, "==========================================="); @@ -89,6 +92,7 @@ else if (pvDefaultType != null && !pvDefaultType.isEmpty()) { System.setProperty("PV_THROTTLE_MS", pvThrottleMs); System.setProperty("PV_ARRAY_THROTTLE_MS", pvArrayThrottleMs); System.setProperty("PV_WRITE_SUPPORT", pvWriteSupport); + System.setProperty("PV_READ_TIMEOUT", pvReadTimeout); // Configure JCA/CAJ to use environment vars, not java properties or preferences System.setProperty("jca.use_env", "true"); @@ -98,6 +102,7 @@ else if (pvDefaultType != null && !pvDefaultType.isEmpty()) { logger.log(Level.INFO, "EPICS_CA_MAX_ARRAY_BYTES=" + System.getenv("EPICS_CA_MAX_ARRAY_BYTES")); logger.log(Level.INFO, "EPICS_PVA_ADDR_LIST=" + epicsPvaAddrList); logger.log(Level.INFO, "EPICS_PVA_AUTO_ADDR_LIST=" + epicsPvaAutoAddrList); + logger.log(Level.INFO, "PV_READ_TIMEOUT=" + pvReadTimeout); logger.log(Level.INFO, "==========================================="); diff --git a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java index 16b5ba3..d36bfcf 100644 --- a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java +++ b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java @@ -18,28 +18,20 @@ package org.phoebus.pvws; -import org.epics.vtype.VType; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.pv.RefCountMap; -import org.phoebus.pvws.ws.Vtype2Json; import org.phoebus.pvws.ws.WebSocket; -import org.phoebus.pvws.ws.WebSocketPV; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.ApplicationContext; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; import java.util.List; @SpringBootApplication -@RestController public class EpicsWebSocketServerApplication extends SpringBootServletInitializer { public static void main(String[] args) { @@ -62,26 +54,4 @@ public static void main(String[] args) { protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(EpicsWebSocketServerApplication.class); } - - @GetMapping(value= "/pvget", produces = MediaType.APPLICATION_JSON_VALUE) - public String pvget(@RequestParam String name) { - final WebSocketPV pv = new WebSocketPV(name, null); - String ret; - try { - int maxAttempts = 100; - int retryDelay = 50; - - VType lastValue = null; - for (int i = 0; i< maxAttempts; i++) { - Thread.sleep(retryDelay); - lastValue = pv.get(); - if (lastValue != null) break; - } - ret = Vtype2Json.toJson(name, lastValue, null, true, true); - } catch (final Exception ex) { - ret = String.format("Unable to get PV value for %s - exception %s", name, ex); - } - pv.dispose(); - return ret; - } } diff --git a/src/main/java/org/phoebus/pvws/controllers/PvwsRestController.java b/src/main/java/org/phoebus/pvws/controllers/PvwsRestController.java index b000db6..e00db6f 100644 --- a/src/main/java/org/phoebus/pvws/controllers/PvwsRestController.java +++ b/src/main/java/org/phoebus/pvws/controllers/PvwsRestController.java @@ -22,19 +22,25 @@ import org.epics.util.array.ListInteger; import org.epics.vtype.Array; import org.epics.vtype.VType; +import org.phoebus.core.vtypes.VTypeHelper; import org.phoebus.pv.PV; import org.phoebus.pv.PVPool; import org.phoebus.pv.RefCountMap; import org.phoebus.pvws.model.*; +import org.phoebus.pvws.ws.Vtype2Json; import org.phoebus.pvws.ws.WebSocket; import org.phoebus.pvws.ws.WebSocketPV; import org.phoebus.util.time.TimestampFormats; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; @@ -146,4 +152,30 @@ public InfoData info(@RequestParam(name = "env", defaultValue = "false") boolean } return infoData; } + + @GetMapping(value= "/pvget", produces = MediaType.APPLICATION_JSON_VALUE) + public String pvget(@RequestParam String name) { + CountDownLatch countDownLatch = new CountDownLatch(1); + AtomicReference value = new AtomicReference<>(null); + try { + int pvReadTimeout = Integer.parseInt(System.getProperty("PV_READ_TIMEOUT")); + PV pv = PVPool.getPV(name); + pv.onValueEvent().subscribe(vtype -> { + if (!VTypeHelper.isDisconnected(vtype)) { + value.set(pv.read()); + } + countDownLatch.countDown(); + }); + countDownLatch.await(pvReadTimeout, TimeUnit.MILLISECONDS); + PVPool.releasePV(pv); + if(value.get() == null){ + logger.info("PV " + name + " never connected."); + return null; + } + return Vtype2Json.toJson(name, value.get(), null, true, true); + } catch (Exception e) { + logger.warning("Exception when reading PV " + name + " Exception: " + e); + return null; + } + } } diff --git a/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java b/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java index b5e8b52..0f48bdd 100644 --- a/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java +++ b/src/main/java/org/phoebus/pvws/ws/WebSocketPV.java @@ -169,17 +169,6 @@ public void write(Object new_value) throws Exception throw new Exception("PV_WRITE_SUPPORT is disabled"); } - /** - * Get current value by calling read() on the PV - * @return the current value - * @throws Exception on error - */ - public VType get() throws Exception - { - pv = PVPool.getPV(name); - return pv.read(); - } - /** Close PV */ public void dispose() { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7848f60..50c7c67 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,3 +14,5 @@ EPICS_PVA_ADDR_LIST= EPICS_PVA_AUTO_ADDR_LIST=YES EPICS_PVA_BROADCAST_PORT=5076 EPICS_PVA_NAME_SERVERS= + +PV_READ_TIMEOUT=5000 From 9c3f66f8520d0c0d9e3278f1c0d248954587bea1 Mon Sep 17 00:00:00 2001 From: Jack Harper Date: Wed, 24 Sep 2025 10:30:47 -0500 Subject: [PATCH 15/15] tidy up --- src/main/java/org/phoebus/pvws/EpicsConfiguration.java | 1 - .../java/org/phoebus/pvws/EpicsWebSocketServerApplication.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/java/org/phoebus/pvws/EpicsConfiguration.java b/src/main/java/org/phoebus/pvws/EpicsConfiguration.java index b48e9ad..1c8bfd6 100644 --- a/src/main/java/org/phoebus/pvws/EpicsConfiguration.java +++ b/src/main/java/org/phoebus/pvws/EpicsConfiguration.java @@ -102,7 +102,6 @@ else if (pvDefaultType != null && !pvDefaultType.isEmpty()) { logger.log(Level.INFO, "EPICS_CA_MAX_ARRAY_BYTES=" + System.getenv("EPICS_CA_MAX_ARRAY_BYTES")); logger.log(Level.INFO, "EPICS_PVA_ADDR_LIST=" + epicsPvaAddrList); logger.log(Level.INFO, "EPICS_PVA_AUTO_ADDR_LIST=" + epicsPvaAutoAddrList); - logger.log(Level.INFO, "PV_READ_TIMEOUT=" + pvReadTimeout); logger.log(Level.INFO, "==========================================="); diff --git a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java index d36bfcf..1b5c418 100644 --- a/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java +++ b/src/main/java/org/phoebus/pvws/EpicsWebSocketServerApplication.java @@ -28,7 +28,6 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.ApplicationContext; - import java.util.List; @SpringBootApplication