Skip to content

Commit 48b8c9c

Browse files
authored
Merge pull request #3606 from KIT-IBPT/issue-3603
Handle long values in EPICS Jackie PV support
2 parents 32ed721 + 4b36b42 commit 48b8c9c

File tree

6 files changed

+523
-43
lines changed

6 files changed

+523
-43
lines changed

core/pv-jackie/pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@
2222
<scope>test</scope>
2323
</dependency>
2424

25+
<dependency>
26+
<groupId>org.mockito</groupId>
27+
<artifactId>mockito-core</artifactId>
28+
<version>${mockito.version}</version>
29+
<scope>test</scope>
30+
</dependency>
31+
2532
<dependency>
2633
<groupId>org.epics</groupId>
2734
<artifactId>vtype</artifactId>

core/pv-jackie/src/main/java/org/phoebus/pv/jackie/JackiePV.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2017-2024 aquenos GmbH.
2+
* Copyright (c) 2017-2025 aquenos GmbH.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -261,10 +261,12 @@ public CompletableFuture<?> asyncWrite(Object new_value) throws Exception {
261261
// Use ca_put_callback.
262262
final var listenable_future = channel.put(
263263
ValueConverter.objectToChannelAccessSimpleOnlyValue(
264+
channel.getName(),
264265
new_value,
265266
channel.getClient().getConfiguration()
266267
.getCharset(),
267-
treat_char_as_long_string));
268+
treat_char_as_long_string,
269+
preferences.long_conversion_mode()));
268270
final var completable_future = new CompletableFuture<Void>();
269271
listenable_future.addCompletionListener((future) -> {
270272
try {
@@ -304,10 +306,12 @@ public void write(Object new_value) throws Exception {
304306
// Use ca_put without a callback.
305307
channel.putNoCallback(
306308
ValueConverter.objectToChannelAccessSimpleOnlyValue(
309+
channel.getName(),
307310
new_value,
308311
channel.getClient().getConfiguration()
309312
.getCharset(),
310-
treat_char_as_long_string));
313+
treat_char_as_long_string,
314+
preferences.long_conversion_mode()));
311315
}
312316
case YES -> {
313317
// Wait for the write operation to complete. This allows code

core/pv-jackie/src/main/java/org/phoebus/pv/jackie/JackiePreferences.java

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024 aquenos GmbH.
2+
* Copyright (c) 2024-2025 aquenos GmbH.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -133,10 +133,73 @@ public record JackiePreferences(
133133
boolean dbe_property_supported,
134134
boolean honor_zero_precision,
135135
String hostname,
136+
LongConversionMode long_conversion_mode,
136137
ChannelAccessEventMask monitor_mask,
137138
boolean rtyp_value_only,
138139
String username) {
139140

141+
/**
142+
* Mode for handling integer numbers that are too large to fit into the
143+
* {@link Integer} type.
144+
*/
145+
public enum LongConversionMode {
146+
/**
147+
* Limit the {@link Long} value to the {@link Integer} range.
148+
*
149+
* A value that is greater than {@link Integer#MAX_VALUE} is converted
150+
* to {@link Integer#MAX_VALUE}, and a value that is less than
151+
* {@link Integer#MIN_VALUE} is converted to {@link Integer#MIN_VALUE}.
152+
*/
153+
COERCE,
154+
155+
/**
156+
* Limit the {@link Long} value to the {@link Integer} range and log a
157+
* warning.
158+
*
159+
* This essentially is the same behavior as {@link #COERCE}, but a
160+
* warning message is written to the log.
161+
*/
162+
COERCE_AND_WARN,
163+
164+
/**
165+
* Convert the {@link Long} value to a {@link Double}.
166+
*
167+
* This means that some precision is lost.
168+
*/
169+
CONVERT,
170+
171+
/**
172+
* Convert the {@link Long} value to a {@link Double} and log a
173+
* warning.
174+
*
175+
* This essentially is the same behavior as {@link #CONVERT}, but a
176+
* warning message is written to the log.
177+
*/
178+
CONVERT_AND_WARN,
179+
180+
/**
181+
* Raise an {@link IllegalArgumentException}.
182+
*/
183+
FAIL,
184+
185+
/**
186+
* Cast the {@link Long} value to an {@link Integer}.
187+
*
188+
* This means that the value will overflow (e.g.
189+
* <code>Integer.MAX_VALUE + 1</code> becomes
190+
* <code>Integer.MIN_VALUE</code>.
191+
*/
192+
TRUNCATE,
193+
194+
/**
195+
* Cast the {@link Long} value to an {@link Integer} and log a warning.
196+
*
197+
* This essentially is the same behavior as {@link #CAST}, but a
198+
* warning message is written to the log.
199+
*/
200+
TRUNCATE_AND_WARN,
201+
}
202+
140203
private final static JackiePreferences DEFAULT_INSTANCE;
141204

142205
static {
@@ -216,6 +279,18 @@ private static JackiePreferences loadPreferences() {
216279
if (hostname.isEmpty()) {
217280
hostname = null;
218281
}
282+
final var long_conversion_mode_string = preference_reader.get(
283+
"long_conversion_mode");
284+
LongConversionMode long_conversion_mode;
285+
try {
286+
long_conversion_mode = LongConversionMode.valueOf(
287+
long_conversion_mode_string);
288+
} catch (IllegalArgumentException|NullPointerException e) {
289+
logger.severe(
290+
"Invalid long conversion mode: "
291+
+ long_conversion_mode_string);
292+
long_conversion_mode = LongConversionMode.COERCE_AND_WARN;
293+
}
219294
final var monitor_mask_string = preference_reader.get("monitor_mask");
220295
ChannelAccessEventMask monitor_mask;
221296
try {
@@ -493,6 +568,7 @@ private static JackiePreferences loadPreferences() {
493568
logger.config("dbe_property_supported = " + dbe_property_supported);
494569
logger.config("honor_zero_precision = " + honor_zero_precision);
495570
logger.config("hostname = " + hostname);
571+
logger.config("long_conversion_mode = " + long_conversion_mode);
496572
logger.config("monitor_mask = " + monitor_mask);
497573
logger.config("rtyp_value_only = " + rtyp_value_only);
498574
logger.config("use_env = " + use_env);
@@ -513,6 +589,7 @@ private static JackiePreferences loadPreferences() {
513589
dbe_property_supported,
514590
honor_zero_precision,
515591
hostname,
592+
long_conversion_mode,
516593
monitor_mask,
517594
rtyp_value_only,
518595
username);

core/pv-jackie/src/main/java/org/phoebus/pv/jackie/util/ValueConverter.java

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2024 aquenos GmbH.
2+
* Copyright (c) 2024-2025 aquenos GmbH.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -52,6 +52,9 @@
5252
import org.epics.vtype.VStringArray;
5353
import org.epics.vtype.VType;
5454
import org.phoebus.core.vtypes.VTypeHelper;
55+
import org.phoebus.pv.jackie.JackiePreferences;
56+
import org.slf4j.Logger;
57+
import org.slf4j.LoggerFactory;
5558

5659
import java.nio.ByteBuffer;
5760
import java.nio.DoubleBuffer;
@@ -75,6 +78,14 @@ public final class ValueConverter {
7578
*/
7679
public static final long OFFSET_EPICS_TO_UNIX_EPOCH_SECONDS = 631152000L;
7780

81+
/**
82+
* Logger used by this class.
83+
*
84+
* The field is package private, so that unit-tests can inject a custom
85+
* implementation.
86+
*/
87+
static Logger log = LoggerFactory.getLogger(ValueConverter.class);
88+
7889
private ValueConverter() {
7990
}
8091

@@ -316,6 +327,9 @@ public static VType channelAccessToVType(
316327
* <code>int</code>, and <code>short</code>, arrays of these primitive
317328
* types, {@link String}, and arrays of {@link String}.
318329
*
330+
* @param pv_name
331+
* name of the process variable for which the value is converted. This is
332+
* only used for logging purposes.
319333
* @param object
320334
* object to be converted.
321335
* @param charset
@@ -324,15 +338,20 @@ public static VType channelAccessToVType(
324338
* indicates whether a {@link String} or single element
325339
* <code>String[]</code> array should be converted to a
326340
* <code>DBR_CHAR</code> instead of a <code>DBR_STRING</code>.
341+
* @param long_conversion_mode
342+
* behavior when converting a {@link Long} value that does not fit into an
343+
* {@link Integer}.
327344
* @return
328345
* the converted value.
329346
* @throws IllegalArgumentException
330347
* if <code>object</code> cannot be converted.
331348
*/
332349
public static ChannelAccessSimpleOnlyValue<?> objectToChannelAccessSimpleOnlyValue(
350+
String pv_name,
333351
Object object,
334352
Charset charset,
335-
boolean convert_string_as_long_string) {
353+
boolean convert_string_as_long_string,
354+
JackiePreferences.LongConversionMode long_conversion_mode) {
336355
if (object instanceof VType vtype) {
337356
var converted_object = VTypeHelper.toObject(vtype);
338357
// VTypeHelper.toObject returns null if it does not know how to
@@ -367,6 +386,110 @@ public static ChannelAccessSimpleOnlyValue<?> objectToChannelAccessSimpleOnlyVal
367386
if (object instanceof int[] value) {
368387
return ChannelAccessValueFactory.createLong(value);
369388
}
389+
if (object instanceof Long value) {
390+
if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) {
391+
return ChannelAccessValueFactory.createLong(
392+
new int[]{value.intValue()});
393+
}
394+
return switch (long_conversion_mode) {
395+
case COERCE -> ChannelAccessValueFactory.createLong(
396+
new int[]{coerceToInt(value)});
397+
case COERCE_AND_WARN -> {
398+
final var coerced_value = coerceToInt(value);
399+
log.warn(
400+
"Writing long {} as int {} for PV {}",
401+
value,
402+
coerced_value,
403+
pv_name);
404+
yield ChannelAccessValueFactory.createLong(
405+
new int[]{coerced_value});
406+
}
407+
case CONVERT -> ChannelAccessValueFactory.createDouble(
408+
new double[]{value.doubleValue()});
409+
case CONVERT_AND_WARN -> {
410+
final var converted_value = value.doubleValue();
411+
log.warn(
412+
"Writing long {} as double {} for PV {}",
413+
value,
414+
converted_value,
415+
pv_name);
416+
yield ChannelAccessValueFactory.createDouble(
417+
new double[]{converted_value});
418+
}
419+
case FAIL -> throw new IllegalArgumentException(
420+
"Cannot write long value " + value
421+
+ ". The value is outside the range of a "
422+
+ "32-bit signed integer.");
423+
case TRUNCATE -> ChannelAccessValueFactory.createLong(
424+
new int[]{value.intValue()});
425+
case TRUNCATE_AND_WARN -> {
426+
final var truncated_value = value.intValue();
427+
log.warn(
428+
"Writing long {} as int {} for PV {}",
429+
value,
430+
truncated_value,
431+
pv_name);
432+
yield ChannelAccessValueFactory.createLong(
433+
new int[]{truncated_value});
434+
}
435+
};
436+
}
437+
if (object instanceof long[] value) {
438+
boolean limit_exceeded = false;
439+
for (long element : value) {
440+
if (element < Integer.MIN_VALUE
441+
|| element > Integer.MAX_VALUE) {
442+
limit_exceeded = true;
443+
break;
444+
}
445+
}
446+
if (!limit_exceeded) {
447+
return ChannelAccessValueFactory.createLong(
448+
longArrayToIntArray(value));
449+
}
450+
return switch (long_conversion_mode) {
451+
case COERCE -> ChannelAccessValueFactory.createLong(
452+
coerceToInt(value));
453+
case COERCE_AND_WARN -> {
454+
final var coerced_value = coerceToInt(value);
455+
log.warn(
456+
"Writing long[] {} as int[] {} for PV {}",
457+
Arrays.toString(value),
458+
Arrays.toString(coerced_value),
459+
pv_name);
460+
yield ChannelAccessValueFactory.createLong(
461+
coerced_value);
462+
}
463+
case CONVERT -> ChannelAccessValueFactory.createDouble(
464+
longArrayToDoubleArray(value));
465+
case CONVERT_AND_WARN -> {
466+
final var converted_value = longArrayToDoubleArray(value);
467+
log.warn(
468+
"Writing long[] {} as double[] {} for PV {}",
469+
Arrays.toString(value),
470+
Arrays.toString(converted_value),
471+
pv_name);
472+
yield ChannelAccessValueFactory.createDouble(
473+
converted_value);
474+
}
475+
case FAIL -> throw new IllegalArgumentException(
476+
"Cannot write long[] value " + Arrays.toString(value)
477+
+ ". The value is outside the range of a "
478+
+ "32-bit signed integer.");
479+
case TRUNCATE -> ChannelAccessValueFactory.createLong(
480+
longArrayToIntArray(value));
481+
case TRUNCATE_AND_WARN -> {
482+
final var truncated_value = longArrayToIntArray(value);
483+
log.warn(
484+
"Writing long[] {} as int[] {} for PV {}",
485+
Arrays.toString(value),
486+
Arrays.toString(truncated_value),
487+
pv_name);
488+
yield ChannelAccessValueFactory.createLong(
489+
truncated_value);
490+
}
491+
};
492+
}
370493
if (object instanceof Short value) {
371494
return ChannelAccessValueFactory.createShort(new short[] {value});
372495
}
@@ -389,7 +512,7 @@ public static ChannelAccessSimpleOnlyValue<?> objectToChannelAccessSimpleOnlyVal
389512
// conversion if the array has a single element.
390513
if (value.length == 1 && convert_string_as_long_string) {
391514
return objectToChannelAccessSimpleOnlyValue(
392-
value[0], charset, true);
515+
pv_name, value[0], charset, true, long_conversion_mode);
393516
}
394517
return ChannelAccessValueFactory.createString(
395518
Arrays.asList(value), charset);
@@ -415,6 +538,21 @@ public int size() {
415538
};
416539
}
417540

541+
private static int coerceToInt(long value) {
542+
if (value > Integer.MAX_VALUE) {
543+
return Integer.MAX_VALUE;
544+
} else if (value < Integer.MIN_VALUE) {
545+
return Integer.MIN_VALUE;
546+
} else {
547+
return (int) value;
548+
}
549+
}
550+
551+
private static int[] coerceToInt(long[] array) {
552+
return Arrays.stream(array).mapToInt(
553+
ValueConverter::coerceToInt).toArray();
554+
}
555+
418556
private static Alarm convertAlarm(ChannelAccessTimeValue<?> time_value) {
419557
AlarmSeverity severity = switch (time_value.getAlarmSeverity()) {
420558
case NO_ALARM -> AlarmSeverity.NONE;
@@ -539,6 +677,16 @@ public int size() {
539677
};
540678
}
541679

680+
private static double[] longArrayToDoubleArray(long[] array) {
681+
return Arrays.stream(array).mapToDouble(
682+
element -> (double) element).toArray();
683+
}
684+
685+
private static int[] longArrayToIntArray(long[] array) {
686+
return Arrays.stream(array).mapToInt(
687+
element -> (int) element).toArray();
688+
}
689+
542690
private static ListShort shortBufferToListShort(ShortBuffer buffer) {
543691
return new ListShort() {
544692
@Override

0 commit comments

Comments
 (0)