The class also supports highlighting specific byte positions using ANSI color codes,
+ * which is useful for debugging and visualizing specific parts of binary data.
+ */
+public class Hex {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Hex.class);
+
+ /**
+ * Default width for hex dump output in characters.
+ * This value (46) allows approximately 10 bytes per line for arrays with less than 999 bytes.
+ */
+ public static final int DefaultWidth = 46;
+
+ /**
+ * Overhead per line when the hex dump is rendered inside ASCII boxes.
+ * Accounts for left and right border characters.
+ */
+ public static final int boxLineOverheat = 1 + 1;
+
+ /**
+ * Width of a single blank/space character.
+ */
+ public static final int blankWidth = 1;
+
+ /**
+ * Width required to display one byte: 2 hex digits + 1 space separator.
+ */
+ public static final int byteWidth = 2 + 1;
+
+ /**
+ * Width of the pipe character '|' used as separator between index and hex data.
+ */
+ public static final int pipeWidth = 1;
+
+ /**
+ * Debug flag. When set to {@code true}, enables detailed debug logging
+ * of the hex dump calculation process.
+ */
+ public static boolean DebugHex;
+
+ // Private constructor to prevent instantiation of utility class
+ private Hex() {
+ }
+
+ /**
+ * Creates a hex dump of the given byte array using the default width.
+ *
+ * @param data the byte array to dump; may be {@code null} or empty
+ * @return a formatted hex dump string, or empty string if data is null/empty
+ * @see #dump(byte[], int, int...)
+ */
+ public static String dump(byte[] data) {
+ return dump(data, DefaultWidth);
+ }
+
+ /**
+ * Creates a hex dump of the given byte array with configurable width and optional highlighting.
+ *
+ *
The output format for each row is:
+ *
+ * INDEX|HH HH HH ... 'ASCII'
+ *
+ * Where:
+ *
+ *
INDEX is the zero-padded byte offset
+ *
HH are two-digit lowercase hex values separated by spaces
+ *
ASCII is the printable character representation (non-printable chars shown as '.')
+ *
+ *
+ * @param data the byte array to dump; may be {@code null} or empty
+ * @param desiredCharWidth the desired output width in characters (minimum 18)
+ * @param highlights optional byte indices to highlight with ANSI red color
+ * @return a formatted hex dump string, or empty string if data is null/empty
+ */
+ public static String dump(byte[] data, int desiredCharWidth, int... highlights) {
+ // Handle null or empty input
+ if (data == null || data.length < 1) {
+ return "";
+ }
+
+ // Convert highlight indices to a Set for O(1) lookup
+ Set highlightsSet = Arrays.stream(highlights).boxed().collect(Collectors.toSet());
+
+ // Copy the array to avoid mutating the original during maskString()
+ data = Arrays.copyOf(data, data.length);
+
+ StringBuilder hexString = new StringBuilder();
+
+ // Calculate how many bytes fit per row and the width needed for the index column
+ Map.Entry rowIndexCalculation = calculateBytesPerRowAndIndexWidth(data.length, desiredCharWidth);
+ int maxBytesPerRow = rowIndexCalculation.getKey();
+ int indexWidth = rowIndexCalculation.getValue();
+
+ // Iterate through the data, processing one row at a time
+ for (int byteIndex = 0, rowIndex = 0; byteIndex < data.length; byteIndex = byteIndex + maxBytesPerRow, rowIndex = rowIndex + 1) {
+
+ // Build the index prefix (e.g., "000|" or "0012|")
+ String indexString = String.format("%1$" + indexWidth + "s|", byteIndex).replace(' ', '0');
+ hexString.append(indexString);
+
+ // Output each byte in this row as hex
+ for (int columnIndex = 0; columnIndex < maxBytesPerRow; columnIndex++) {
+ int absoluteIndex = byteIndex + columnIndex;
+
+ if (absoluteIndex < data.length) {
+ // Apply ANSI red color for highlighted bytes
+ if (highlightsSet.contains(absoluteIndex)) {
+ hexString.append("\033[0;31m"); // ANSI red
+ }
+
+ // Append the hex value (2 digits + space)
+ hexString.append(String.format("%02x ", data[absoluteIndex]));
+
+ // Reset color after highlighted byte
+ if (highlightsSet.contains(absoluteIndex)) {
+ hexString.append("\033[0m"); // ANSI reset
+ }
+ } else {
+ // Pad with spaces in case of an incomplete last row
+ hexString.append(" ".repeat(byteWidth));
+ }
+ }
+
+ // Calculate the end index for the ASCII representation
+ int endIndex = Math.min(byteIndex + maxBytesPerRow, data.length);
+
+ // Create ASCII representation (non-printable chars replaced with '.')
+ String stringRepresentation = maskString(Arrays.copyOfRange(data, byteIndex, endIndex));
+
+ // Pad the ASCII representation if this is a partial row
+ if (stringRepresentation.length() < maxBytesPerRow) {
+ stringRepresentation += StringUtils.repeat(" ", (maxBytesPerRow - stringRepresentation.length()) % maxBytesPerRow);
+ }
+
+ // Append the quoted ASCII representation
+ hexString.append(String.format("'%s'\n", stringRepresentation));
+ }
+
+ // Remove the trailing newline
+ return hexString.substring(0, hexString.length() - 1);
+ }
+
+ /**
+ * Calculates the optimal number of bytes per row and the index column width.
+ *
+ *
This method determines how to best fit the hex dump within the desired width by:
+ *
+ *
Calculating the number of digits needed for the row index
+ *
Determining the minimum required width for a valid output
+ *
Solving for the maximum bytes that can fit per row
+ *
+ *
+ *
The layout formula accounts for:
+ *
+ *
Index column: variable width based on total byte count
+ *
Pipe separator: 1 character
+ *
Hex bytes: 3 characters each (2 hex digits + space)
+ *
ASCII column: 1 character per byte + 2 quote characters
+ *
+ *
+ * @param numberOfBytes total number of bytes in the data
+ * @param desiredStringWidth desired output width in characters
+ * @return a Map.Entry where key = bytes per row, value = index digit width
+ */
+ static Map.Entry calculateBytesPerRowAndIndexWidth(int numberOfBytes, int desiredStringWidth) {
+ if (DebugHex) {
+ LOGGER.debug("Calculating max row and index for {} number of bytes and a desired string width of {}", numberOfBytes, desiredStringWidth);
+ }
+
+ // Calculate how many digits we need for the index (e.g., 3 digits for 100-999 bytes)
+ int indexDigits = (int) (Math.log10(numberOfBytes) + 1);
+ int requiredIndexWidth = indexDigits + pipeWidth;
+
+ if (DebugHex) {
+ LOGGER.debug("index width {} for indexDigits {} for bytes {}", requiredIndexWidth, indexDigits, numberOfBytes);
+ }
+
+ // Account for the quote characters around the ASCII representation
+ int quoteRune = 1;
+ int potentialStringRenderRune = 1;
+
+ // Calculate the minimum number of spaces needed for at least one byte: "0|00 '.'"
+ int availableSpace = requiredIndexWidth + byteWidth + quoteRune + potentialStringRenderRune + quoteRune;
+
+ if (DebugHex) {
+ LOGGER.debug("calculated {} minimal width for number of bytes {}", availableSpace, numberOfBytes);
+ }
+
+ // Use the larger of desired width or minimum required width
+ if (desiredStringWidth >= availableSpace) {
+ availableSpace = desiredStringWidth;
+ } else {
+ if (DebugHex) {
+ LOGGER.debug("Overflow by {} runes", desiredStringWidth - availableSpace);
+ }
+ }
+
+ if (DebugHex) {
+ LOGGER.debug("Actual space {}", availableSpace);
+ }
+
+ // Solve for maximum bytes per row using the layout equation:
+ // totalWidth = indexWidth + (bytesPerRow * byteWidth) + quote + (bytesPerRow * 1) + quote
+ // Rearranging: bytesPerRow = (totalWidth - indexWidth - 2*quote) / (byteWidth + 1)
+ double z = availableSpace; // total available width
+ double y = requiredIndexWidth; // index column width
+ double a = byteWidth; // width per byte in hex section (3)
+ double b = quoteRune; // quote character width (1)
+
+ // Formula: x = (-2*b - y + z) / (a + 1) where x = bytesPerRow
+ double x = ((-2 * b) - y + z) / (a + 1);
+
+ if (DebugHex) {
+ LOGGER.debug("Calculated number of bytes per row {} in int {}", x, (int) x);
+ }
+
+ return new AbstractMap.SimpleEntry<>((int) x, indexDigits);
+ }
+
+ /**
+ * Converts a byte array to a printable ASCII string.
+ *
+ *
Non-printable characters (bytes outside the range 32-126) are replaced
+ * with a period ('.') character. This is the standard convention for hex dump
+ * ASCII representations.
+ *
+ *
Note: This method modifies the input array in place.
+ *
+ * @param data the byte array to convert (will be modified)
+ * @return a String representation with non-printable characters masked
+ */
+ static String maskString(byte[] data) {
+ for (int i = 0; i < data.length; i++) {
+ // ASCII printable range is 32 (space) to 126 (~)
+ if (data[i] < 32 || data[i] > 126) {
+ data[i] = '.';
+ }
+ }
+ return new String(data);
+ }
+
+ /**
+ * Serializes a Java object to a byte array using standard Java serialization.
+ *
+ *
This utility method can be used to convert objects to bytes for hex dumping.
+ *
+ * @param obj the object to serialize (must implement {@link java.io.Serializable})
+ * @return the serialized byte array
+ * @throws RuntimeException if serialization fails due to an IOException
+ */
+ static byte[] toBytes(Object obj) {
+ ByteArrayOutputStream boas = new ByteArrayOutputStream();
+ try (ObjectOutputStream ois = new ObjectOutputStream(boas)) {
+ ois.writeObject(obj);
+ return boas.toByteArray();
+ } catch (IOException ioe) {
+ throw new RuntimeException(ioe);
+ }
+ }
+
+}
diff --git a/plc4j/spi/buffers/ascii-box/src/main/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/utils/StringUtils.java b/plc4j/spi/buffers/ascii-box/src/main/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/utils/StringUtils.java
new file mode 100644
index 00000000000..6da0c26d37a
--- /dev/null
+++ b/plc4j/spi/buffers/ascii-box/src/main/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/utils/StringUtils.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.asciiboxbased.utils.utils;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * Simplified string utilities - uses Java built-in methods where possible.
+ */
+public final class StringUtils {
+
+ private StringUtils() {
+ // Utility class
+ }
+
+ /**
+ * Checks if a CharSequence is empty (""), null or whitespace only.
+ */
+ public static boolean isBlank(final CharSequence cs) {
+ if (cs == null || cs.isEmpty()) {
+ return true;
+ }
+ // For String, use built-in isBlank() which is optimized
+ if (cs instanceof String s) {
+ return s.isBlank();
+ }
+ // Fallback for other CharSequence implementations
+ for (int i = 0; i < cs.length(); i++) {
+ if (!Character.isWhitespace(cs.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Repeats a String n times to form a new String.
+ */
+ public static String repeat(final String str, final int count) {
+ if (str == null) {
+ return null;
+ }
+ if (count <= 0 || str.isEmpty()) {
+ return "";
+ }
+ return str.repeat(count);
+ }
+
+ /**
+ * Joins the elements of the provided array into a single String.
+ */
+ public static String join(final Object[] array, final String delimiter) {
+ if (array == null) {
+ return null;
+ }
+ return Arrays.stream(array)
+ .map(Objects::toString)
+ .collect(Collectors.joining(delimiter != null ? delimiter : ""));
+ }
+
+ /**
+ * Joins the elements of the provided Iterable into a single String.
+ */
+ public static String join(final Iterable> iterable, final String delimiter) {
+ if (iterable == null) {
+ return null;
+ }
+ return StreamSupport.stream(iterable.spliterator(), false)
+ .map(Objects::toString)
+ .collect(Collectors.joining(delimiter != null ? delimiter : ""));
+ }
+
+ /**
+ * Removes leading and trailing whitespace.
+ */
+ public static String trim(final String str) {
+ return str == null ? null : str.trim();
+ }
+
+ /**
+ * Checks if the CharSequence contains any character from the searchChars.
+ */
+ public static boolean containsAny(final CharSequence cs, final CharSequence searchChars) {
+ if (cs == null || cs.isEmpty() || searchChars == null || searchChars.isEmpty()) {
+ return false;
+ }
+ for (int i = 0; i < cs.length(); i++) {
+ char ch = cs.charAt(i);
+ for (int j = 0; j < searchChars.length(); j++) {
+ if (ch == searchChars.charAt(j)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/WriteBufferAsciiBoxBasedTest.java b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/WriteBufferAsciiBoxBasedTest.java
new file mode 100644
index 00000000000..8b73b22f40f
--- /dev/null
+++ b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/WriteBufferAsciiBoxBasedTest.java
@@ -0,0 +1,384 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.asciiboxbased;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.asciiboxbased.utils.ascii.AsciiBox;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WriteBufferAsciiBoxBasedTest {
+
+ @Test
+ void testDefaultConstructor() {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testConstructorWithOptions() {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased(true, true);
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteBit() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeBit(true, WithOption.WithName("testBit"));
+ buffer.writeBit(false, WithOption.WithName("testBit2"));
+
+ // Just verify no exception is thrown
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteBitWithAdditionalRepresentation() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeBit(true, WithOption.WithName("testBit"), WithOption.WithAdditionalStringRepresentation("enabled"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteBitWithoutName() {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+
+ assertThrows(BufferException.class, () -> buffer.writeBit(true));
+ }
+
+ @Test
+ void testWriteBits() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeBits(8, new byte[]{(byte) 0xFF}, WithOption.WithName("testBits"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteBitsWithAdditionalRepresentation() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeBits(8, new byte[]{(byte) 0xFF}, WithOption.WithName("testBits"), WithOption.WithAdditionalStringRepresentation("all ones"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteUnsignedByte() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeUnsignedByte(8, (byte) 42, WithOption.WithName("testByte"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteUnsignedShort() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeUnsignedShort(16, (short) 1234, WithOption.WithName("testShort"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteUnsignedInt() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeUnsignedInt(32, 123456, WithOption.WithName("testInt"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteUnsignedLong() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeUnsignedLong(64, 1234567890L, WithOption.WithName("testLong"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteUnsignedBigInteger() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeUnsignedBigInteger(64, BigInteger.valueOf(1234567890), WithOption.WithName("testBigInt"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteSignedByte() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeSignedByte(8, (byte) -42, WithOption.WithName("testByte"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteSignedShort() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeSignedShort(16, (short) -1234, WithOption.WithName("testShort"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteSignedInt() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeSignedInt(32, -123456, WithOption.WithName("testInt"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteSignedLong() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeSignedLong(64, -1234567890L, WithOption.WithName("testLong"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteSignedBigInteger() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeSignedBigInteger(64, BigInteger.valueOf(-1234567890), WithOption.WithName("testBigInt"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteFloat() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeFloat(32, 3.14f, WithOption.WithName("testFloat"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteDouble() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeDouble(64, 3.14159265359, WithOption.WithName("testDouble"));
+
+ assertNotNull(buffer);
+ }
+
+ // Note: testWriteBigDecimal is skipped because there's a bug in the production code
+ // that tries to format BigDecimal as hex which throws IllegalFormatConversionException
+
+ @Test
+ void testWriteString() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeString(80, "Hello World", WithOption.WithName("testString"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testPushAndPopContext() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.pushContext(WithOption.WithName("outer"));
+ buffer.writeString(40, "content", WithOption.WithName("inner"));
+ buffer.popContext(WithOption.WithName("outer"));
+
+ AsciiBox box = buffer.getBox();
+ assertNotNull(box);
+ assertTrue(box.toString().contains("outer"));
+ }
+
+ @Test
+ void testNestedContexts() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.pushContext(WithOption.WithName("level1"));
+ buffer.pushContext(WithOption.WithName("level2"));
+ buffer.writeString(40, "deep content", WithOption.WithName("data"));
+ buffer.popContext(WithOption.WithName("level2"));
+ buffer.popContext(WithOption.WithName("level1"));
+
+ AsciiBox box = buffer.getBox();
+ assertNotNull(box);
+ }
+
+ @Test
+ void testMergeSingleBoxes() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased(true, false);
+ buffer.pushContext(WithOption.WithName("parent"));
+ buffer.writeString(40, "content", WithOption.WithName("child"));
+ buffer.popContext(WithOption.WithName("parent"));
+
+ AsciiBox box = buffer.getBox();
+ assertNotNull(box);
+ // With mergeSingleBoxes=true, name should be combined
+ assertTrue(box.getBoxName().contains("/"));
+ }
+
+ @Test
+ void testOmitEmptyBoxes() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased(false, true);
+ buffer.pushContext(WithOption.WithName("parent"));
+ // Empty context - should be omitted
+ buffer.popContext(WithOption.WithName("parent"));
+
+ // Should not throw, empty boxes are omitted
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualString() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual("test value", WithOption.WithName("virtual"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualFloat() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual(3.14f, WithOption.WithName("floatVal"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualDouble() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual(3.14159, WithOption.WithName("doubleVal"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualNumber() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual(42, WithOption.WithName("intVal"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualBoolean() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual(true, WithOption.WithName("boolVal"));
+ buffer.writeVirtual(false, WithOption.WithName("boolVal2"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualEnum() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual(TestEnum.VALUE_A, WithOption.WithName("enumVal"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualUnknownType() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual(new Object(), WithOption.WithName("unknownVal"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteVirtualWithAdditionalRepresentation() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ buffer.writeVirtual("test", WithOption.WithName("val"), WithOption.WithAdditionalStringRepresentation("extra"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testGetPositionInBits() {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ assertEquals(0, buffer.getPositionInBits());
+ }
+
+ @Test
+ void testGetRemainingBits() {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ assertEquals(0, buffer.getRemainingBits());
+ }
+
+ @Test
+ void testGetBytes() {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ byte[] bytes = buffer.getBytes();
+
+ assertNotNull(bytes);
+ assertEquals(0, bytes.length);
+ }
+
+ @Test
+ void testCreateSubBuffer() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ assertNull(buffer.createSubBuffer(8));
+ }
+
+ @Test
+ void testComplexStructure() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+
+ buffer.pushContext(WithOption.WithName("header"));
+ buffer.writeUnsignedByte(8, (byte) 1, WithOption.WithName("version"));
+ buffer.writeUnsignedShort(16, (short) 100, WithOption.WithName("length"));
+ buffer.popContext(WithOption.WithName("header"));
+
+ buffer.pushContext(WithOption.WithName("body"));
+ buffer.writeString(80, "payload data", WithOption.WithName("payload"));
+ buffer.popContext(WithOption.WithName("body"));
+
+ buffer.pushContext(WithOption.WithName("footer"));
+ buffer.writeUnsignedInt(32, 0xDEADBEEF, WithOption.WithName("checksum"));
+ buffer.popContext(WithOption.WithName("footer"));
+
+ // All wrapped in outer context
+ buffer.pushContext(WithOption.WithName("message"));
+ buffer.popContext(WithOption.WithName("message"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testWriteWithSmallBitLength() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+ // Test with bit length < 4 (edge case for hex formatting)
+ buffer.writeUnsignedByte(1, (byte) 1, WithOption.WithName("singleBit"));
+ buffer.writeUnsignedByte(2, (byte) 3, WithOption.WithName("twoBits"));
+ buffer.writeUnsignedByte(3, (byte) 7, WithOption.WithName("threeBits"));
+
+ assertNotNull(buffer);
+ }
+
+ @Test
+ void testMultipleWritesWithoutContext() throws Exception {
+ WriteBufferAsciiBoxBased buffer = new WriteBufferAsciiBoxBased();
+
+ buffer.writeBit(true, WithOption.WithName("bit1"));
+ buffer.writeBit(false, WithOption.WithName("bit2"));
+ buffer.writeUnsignedByte(8, (byte) 0x42, WithOption.WithName("byte1"));
+ buffer.writeString(40, "test", WithOption.WithName("str1"));
+
+ assertNotNull(buffer);
+ }
+
+ private enum TestEnum {
+ VALUE_A,
+ VALUE_B
+ }
+}
diff --git a/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/ascii/AsciiBoxTest.java b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/ascii/AsciiBoxTest.java
new file mode 100644
index 00000000000..e9b41a6329d
--- /dev/null
+++ b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/ascii/AsciiBoxTest.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.asciiboxbased.utils.ascii;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AsciiBoxTest {
+
+ @Test
+ void testWidth() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "hello", 0);
+
+ assertTrue(box.width() > 0);
+ }
+
+ @Test
+ void testWidthWithAnsiCodes() {
+ // Create a box with ANSI color codes
+ AsciiBox box = new AsciiBox("\033[31mred\033[0m");
+
+ // Width should not count ANSI codes
+ assertEquals(3, box.width());
+ }
+
+ @Test
+ void testWidthMultipleLines() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "short\nlongerline\nx", 0);
+
+ // Width should be the maximum line width
+ assertTrue(box.width() >= "longerline".length());
+ }
+
+ @Test
+ void testHeight() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "hello", 0);
+
+ // Box should have at least 3 lines (top border, content, bottom border)
+ assertTrue(box.height() >= 3);
+ }
+
+ @Test
+ void testHeightMultipleLines() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "line1\nline2\nline3", 0);
+
+ // Should have 5 lines (top border, 3 content lines, bottom border)
+ assertEquals(5, box.height());
+ }
+
+ @Test
+ void testLines() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "content", 0);
+
+ String[] lines = box.lines();
+ assertNotNull(lines);
+ assertTrue(lines.length > 0);
+ }
+
+ @Test
+ void testGetBoxName() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("myName", "content", 0);
+
+ assertEquals("myName", box.getBoxName());
+ }
+
+ @Test
+ void testGetBoxNameEmpty() {
+ AsciiBox box = new AsciiBox("no name box");
+
+ assertEquals("", box.getBoxName());
+ }
+
+ @Test
+ void testChangeBoxName() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("oldName", "content", 0);
+ AsciiBox renamed = box.changeBoxName("newName");
+
+ assertEquals("newName", renamed.getBoxName());
+ }
+
+ @Test
+ void testChangeBoxNameNoBorders() {
+ AsciiBox box = new AsciiBox("simple text");
+ AsciiBox renamed = box.changeBoxName("newName");
+
+ assertEquals("newName", renamed.getBoxName());
+ }
+
+ @Test
+ void testIsEmptyFalse() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "content", 0);
+
+ assertFalse(box.isEmpty());
+ }
+
+ @Test
+ void testIsEmptyTrue() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "", 0);
+
+ assertTrue(box.isEmpty());
+ }
+
+ @Test
+ void testIsEmptyNoBorders() {
+ AsciiBox empty = new AsciiBox("");
+ AsciiBox blank = new AsciiBox(" ");
+ AsciiBox notEmpty = new AsciiBox("text");
+
+ assertTrue(empty.isEmpty());
+ assertTrue(blank.isEmpty());
+ assertFalse(notEmpty.isEmpty());
+ }
+
+ @Test
+ void testToString() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "hello", 0);
+
+ String result = box.toString();
+ assertNotNull(result);
+ assertTrue(result.contains("test"));
+ assertTrue(result.contains("hello"));
+ }
+
+ @Test
+ void testEquals() {
+ AsciiBox box1 = new AsciiBox("same data");
+ AsciiBox box2 = new AsciiBox("same data");
+ AsciiBox box3 = new AsciiBox("different data");
+
+ assertEquals(box1, box2);
+ assertNotEquals(box1, box3);
+ }
+
+ @Test
+ void testEqualsItself() {
+ AsciiBox box = new AsciiBox("data");
+
+ assertEquals(box, box);
+ }
+
+ @Test
+ void testEqualsNull() {
+ AsciiBox box = new AsciiBox("data");
+
+ assertNotEquals(null, box);
+ }
+
+ @Test
+ void testEqualsDifferentType() {
+ AsciiBox box = new AsciiBox("data");
+
+ assertNotEquals("data", box);
+ }
+
+ @Test
+ void testHashCode() {
+ AsciiBox box1 = new AsciiBox("same data");
+ AsciiBox box2 = new AsciiBox("same data");
+
+ assertEquals(box1.hashCode(), box2.hashCode());
+ }
+
+ @Test
+ void testConstructorWithNullThrows() {
+ assertThrows(NullPointerException.class, () -> new AsciiBox(null));
+ }
+
+ @Test
+ void testChangeBoxNameWithLongerName() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("a", "content", 0);
+ AsciiBox renamed = box.changeBoxName("muchLongerName");
+
+ assertEquals("muchLongerName", renamed.getBoxName());
+ // Box should expand to fit new name
+ assertTrue(renamed.width() >= "muchLongerName".length());
+ }
+
+ @Test
+ void testChangeBoxNameWithShorterName() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("veryLongName", "content", 0);
+ AsciiBox renamed = box.changeBoxName("x");
+
+ assertEquals("x", renamed.getBoxName());
+ }
+
+ @Test
+ void testWidthEmptyBox() {
+ AsciiBox box = new AsciiBox("");
+
+ assertEquals(0, box.width());
+ }
+
+ @Test
+ void testHeightEmptyBox() {
+ AsciiBox box = new AsciiBox("");
+
+ assertEquals(1, box.height()); // Split on empty string returns array of length 1
+ }
+
+ @Test
+ void testLinesEmptyBox() {
+ AsciiBox box = new AsciiBox("");
+
+ String[] lines = box.lines();
+ assertEquals(1, lines.length);
+ assertEquals("", lines[0]);
+ }
+
+ @Test
+ void testBoxWithSpecialCharacters() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "special: @#$%^&*()", 0);
+
+ String result = box.toString();
+ assertTrue(result.contains("@#$%^&*()"));
+ }
+
+ @Test
+ void testBoxWithUnicodeContent() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("unicode", "Hello 世界", 0);
+
+ String result = box.toString();
+ assertTrue(result.contains("世界"));
+ }
+}
diff --git a/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/ascii/AsciiBoxWriterTest.java b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/ascii/AsciiBoxWriterTest.java
new file mode 100644
index 00000000000..6afab87a5af
--- /dev/null
+++ b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/ascii/AsciiBoxWriterTest.java
@@ -0,0 +1,371 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.asciiboxbased.utils.ascii;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class AsciiBoxWriterTest {
+
+ @Test
+ void testDefaultWriter() {
+ assertNotNull(AsciiBoxWriter.DEFAULT);
+ }
+
+ @Test
+ void testLightWriter() {
+ assertNotNull(AsciiBoxWriter.LIGHT);
+ }
+
+ @Test
+ void testBoxStringSimple() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "hello", 0);
+
+ assertNotNull(box);
+ String result = box.toString();
+ assertTrue(result.contains("test"));
+ assertTrue(result.contains("hello"));
+ }
+
+ @Test
+ void testBoxStringWithCharWidth() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "hi", 20);
+
+ assertNotNull(box);
+ assertTrue(box.width() >= 20);
+ }
+
+ @Test
+ void testBoxStringWithMultipleLines() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "line1\nline2\nline3", 0);
+
+ assertNotNull(box);
+ String result = box.toString();
+ assertTrue(result.contains("line1"));
+ assertTrue(result.contains("line2"));
+ assertTrue(result.contains("line3"));
+ }
+
+ @Test
+ void testBoxStringConvertsLineEndings() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "line1\r\nline2", 0);
+
+ assertNotNull(box);
+ // Should not contain \r
+ assertFalse(box.toString().contains("\r"));
+ }
+
+ @Test
+ void testBoxStringConvertsTabs() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "col1\tcol2", 0);
+
+ assertNotNull(box);
+ // Tab should be converted to spaces
+ assertTrue(box.toString().contains("col1 col2"));
+ }
+
+ @Test
+ void testBoxBox() {
+ AsciiBox innerBox = AsciiBoxWriter.DEFAULT.boxString("inner", "content", 0);
+ AsciiBox outerBox = AsciiBoxWriter.DEFAULT.boxBox("outer", innerBox, 0);
+
+ assertNotNull(outerBox);
+ String result = outerBox.toString();
+ assertTrue(result.contains("outer"));
+ assertTrue(result.contains("inner"));
+ assertTrue(result.contains("content"));
+ }
+
+ @Test
+ void testBoxSideBySide() {
+ AsciiBox box1 = AsciiBoxWriter.DEFAULT.boxString("left", "L", 0);
+ AsciiBox box2 = AsciiBoxWriter.DEFAULT.boxString("right", "R", 0);
+
+ AsciiBox combined = AsciiBoxWriter.DEFAULT.boxSideBySide(box1, box2);
+
+ assertNotNull(combined);
+ String result = combined.toString();
+ assertTrue(result.contains("left"));
+ assertTrue(result.contains("right"));
+ }
+
+ @Test
+ void testBoxSideBySideDifferentHeights() {
+ AsciiBox box1 = AsciiBoxWriter.DEFAULT.boxString("short", "A", 0);
+ AsciiBox box2 = AsciiBoxWriter.DEFAULT.boxString("tall", "B\nC\nD", 0);
+
+ AsciiBox combined = AsciiBoxWriter.DEFAULT.boxSideBySide(box1, box2);
+
+ assertNotNull(combined);
+ // Should have height of the taller box
+ assertTrue(combined.height() >= box2.height());
+ }
+
+ @Test
+ void testBoxBelowBox() {
+ AsciiBox box1 = AsciiBoxWriter.DEFAULT.boxString("top", "T", 0);
+ AsciiBox box2 = AsciiBoxWriter.DEFAULT.boxString("bottom", "B", 0);
+
+ AsciiBox combined = AsciiBoxWriter.DEFAULT.boxBelowBox(box1, box2);
+
+ assertNotNull(combined);
+ String[] lines = combined.lines();
+ // Should have lines from both boxes
+ assertTrue(lines.length > box1.height());
+ }
+
+ @Test
+ void testBoxBelowBoxDifferentWidths() {
+ AsciiBox box1 = AsciiBoxWriter.DEFAULT.boxString("narrow", "X", 0);
+ AsciiBox box2 = AsciiBoxWriter.DEFAULT.boxString("wide", "XXXXX XXXXX", 0);
+
+ AsciiBox combined = AsciiBoxWriter.DEFAULT.boxBelowBox(box1, box2);
+
+ assertNotNull(combined);
+ // All lines should have same width
+ String[] lines = combined.lines();
+ int firstWidth = lines[0].length();
+ for (String line : lines) {
+ assertEquals(firstWidth, line.length());
+ }
+ }
+
+ @Test
+ void testAlignBoxesEmpty() {
+ AsciiBox result = AsciiBoxWriter.DEFAULT.alignBoxes(Collections.emptyList(), 80);
+
+ assertNotNull(result);
+ assertEquals("", result.toString());
+ }
+
+ @Test
+ void testAlignBoxesSingle() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("single", "content", 0);
+ AsciiBox result = AsciiBoxWriter.DEFAULT.alignBoxes(Collections.singletonList(box), 80);
+
+ assertNotNull(result);
+ assertEquals(box.toString(), result.toString());
+ }
+
+ @Test
+ void testAlignBoxesMultiple() {
+ List boxes = Arrays.asList(
+ AsciiBoxWriter.DEFAULT.boxString("box1", "A", 0),
+ AsciiBoxWriter.DEFAULT.boxString("box2", "B", 0),
+ AsciiBoxWriter.DEFAULT.boxString("box3", "C", 0)
+ );
+
+ AsciiBox result = AsciiBoxWriter.DEFAULT.alignBoxes(boxes, 200);
+
+ assertNotNull(result);
+ String resultStr = result.toString();
+ assertTrue(resultStr.contains("box1"));
+ assertTrue(resultStr.contains("box2"));
+ assertTrue(resultStr.contains("box3"));
+ }
+
+ @Test
+ void testAlignBoxesNarrowWidth() {
+ List boxes = Arrays.asList(
+ AsciiBoxWriter.DEFAULT.boxString("box1", "AAAAAAA", 0),
+ AsciiBoxWriter.DEFAULT.boxString("box2", "BBBBBBB", 0)
+ );
+
+ // Narrow width forces boxes onto separate lines
+ AsciiBox result = AsciiBoxWriter.DEFAULT.alignBoxes(boxes, 15);
+
+ assertNotNull(result);
+ // Should have more lines due to stacking
+ assertTrue(result.height() > boxes.get(0).height());
+ }
+
+ @Test
+ void testHasBordersTrue() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "content", 0);
+
+ assertTrue(AsciiBoxWriter.DEFAULT.hasBorders(box));
+ }
+
+ @Test
+ void testHasBordersEmpty() {
+ AsciiBox box = new AsciiBox("");
+
+ assertFalse(AsciiBoxWriter.DEFAULT.hasBorders(box));
+ }
+
+ @Test
+ void testHasBordersNoBorder() {
+ // Create a box without proper borders
+ AsciiBox box = new AsciiBox("just text");
+
+ assertFalse(AsciiBoxWriter.DEFAULT.hasBorders(box));
+ }
+
+ @Test
+ void testUnwrap() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "content", 0);
+ AsciiBox unwrapped = AsciiBoxWriter.DEFAULT.unwrap(box);
+
+ assertNotNull(unwrapped);
+ assertEquals("content", unwrapped.toString());
+ }
+
+ @Test
+ void testUnwrapNoBorders() {
+ AsciiBox box = new AsciiBox("no borders");
+ AsciiBox unwrapped = AsciiBoxWriter.DEFAULT.unwrap(box);
+
+ assertEquals(box, unwrapped);
+ }
+
+ @Test
+ void testMergeHorizontalEmpty() {
+ AsciiBox result = AsciiBoxWriter.DEFAULT.mergeHorizontal(Collections.emptyList());
+
+ assertEquals("", result.toString());
+ }
+
+ @Test
+ void testMergeHorizontalSingle() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "A", 0);
+ AsciiBox result = AsciiBoxWriter.DEFAULT.mergeHorizontal(Collections.singletonList(box));
+
+ assertEquals(box, result);
+ }
+
+ @Test
+ void testMergeHorizontalTwo() {
+ List boxes = Arrays.asList(
+ AsciiBoxWriter.DEFAULT.boxString("box1", "A", 0),
+ AsciiBoxWriter.DEFAULT.boxString("box2", "B", 0)
+ );
+
+ AsciiBox result = AsciiBoxWriter.DEFAULT.mergeHorizontal(boxes);
+
+ assertNotNull(result);
+ String resultStr = result.toString();
+ assertTrue(resultStr.contains("box1"));
+ assertTrue(resultStr.contains("box2"));
+ }
+
+ @Test
+ void testMergeHorizontalThree() {
+ List boxes = Arrays.asList(
+ AsciiBoxWriter.DEFAULT.boxString("box1", "A", 0),
+ AsciiBoxWriter.DEFAULT.boxString("box2", "B", 0),
+ AsciiBoxWriter.DEFAULT.boxString("box3", "C", 0)
+ );
+
+ AsciiBox result = AsciiBoxWriter.DEFAULT.mergeHorizontal(boxes);
+
+ assertNotNull(result);
+ String resultStr = result.toString();
+ assertTrue(resultStr.contains("box1"));
+ assertTrue(resultStr.contains("box2"));
+ assertTrue(resultStr.contains("box3"));
+ }
+
+ @Test
+ void testExpandBox() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "A", 0);
+ int originalWidth = box.width();
+
+ AsciiBox expanded = AsciiBoxWriter.DEFAULT.expandBox(box, originalWidth + 10);
+
+ assertEquals(originalWidth + 10, expanded.width());
+ }
+
+ @Test
+ void testExpandBoxNoExpansionNeeded() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "A", 0);
+ int originalWidth = box.width();
+
+ AsciiBox expanded = AsciiBoxWriter.DEFAULT.expandBox(box, originalWidth - 5);
+
+ assertEquals(originalWidth, expanded.width());
+ }
+
+ @Test
+ void testCustomBoxWriter() {
+ AsciiBoxWriter custom = new AsciiBoxWriter("+", "+", "-", "|", "+", "+");
+ AsciiBox box = custom.boxString("test", "content", 0);
+
+ String result = box.toString();
+ assertTrue(result.contains("+"));
+ assertTrue(result.contains("-"));
+ assertTrue(result.contains("|"));
+ }
+
+ @Test
+ void testBoxStringWithLongContent() {
+ String longContent = "This is a very long content string that should cause the box to expand";
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", longContent, 10);
+
+ // Box should expand to fit content
+ assertTrue(box.width() > 10);
+ }
+
+ @Test
+ void testBoxStringEmptyContent() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("test", "", 0);
+
+ assertNotNull(box);
+ assertTrue(box.toString().contains("test"));
+ }
+
+ @Test
+ void testBoxStringEmptyName() {
+ AsciiBox box = AsciiBoxWriter.DEFAULT.boxString("", "content", 0);
+
+ assertNotNull(box);
+ assertTrue(box.toString().contains("content"));
+ }
+
+ @Test
+ void testLightWriterUsesLightChars() {
+ AsciiBox box = AsciiBoxWriter.LIGHT.boxString("test", "content", 0);
+
+ String result = box.toString();
+ // Light writer uses different unicode characters
+ assertTrue(result.contains("╭") || result.contains("┄"));
+ }
+
+ @Test
+ void testAlignBoxesWithOverflow() {
+ // Create boxes that exceed desired width
+ List boxes = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ boxes.add(AsciiBoxWriter.DEFAULT.boxString("box" + i, "content" + i, 0));
+ }
+
+ AsciiBox result = AsciiBoxWriter.DEFAULT.alignBoxes(boxes, 50);
+
+ assertNotNull(result);
+ // Should contain all boxes
+ String resultStr = result.toString();
+ assertTrue(resultStr.contains("box0"));
+ assertTrue(resultStr.contains("box4"));
+ }
+}
diff --git a/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/hex/HexTest.java b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/hex/HexTest.java
new file mode 100644
index 00000000000..52778ce89df
--- /dev/null
+++ b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/hex/HexTest.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.asciiboxbased.utils.hex;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class HexTest {
+
+ @Test
+ void testDumpNull() {
+ String result = Hex.dump(null);
+ assertEquals("", result);
+ }
+
+ @Test
+ void testDumpEmpty() {
+ String result = Hex.dump(new byte[0]);
+ assertEquals("", result);
+ }
+
+ @Test
+ void testDumpSingleByte() {
+ byte[] data = new byte[]{0x41}; // 'A'
+ String result = Hex.dump(data);
+
+ assertNotNull(result);
+ assertTrue(result.contains("41"));
+ // String representation is quoted
+ assertTrue(result.contains("'"));
+ }
+
+ @Test
+ void testDumpMultipleBytes() {
+ byte[] data = new byte[]{0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello"
+ String result = Hex.dump(data);
+
+ assertNotNull(result);
+ assertTrue(result.contains("48"));
+ assertTrue(result.contains("65"));
+ assertTrue(result.contains("6c"));
+ // Contains string representation with quotes
+ assertTrue(result.contains("'"));
+ }
+
+ @Test
+ void testDumpWithNonPrintableChars() {
+ byte[] data = new byte[]{0x00, 0x1F, 0x7F}; // Non-printable chars
+ String result = Hex.dump(data);
+
+ assertNotNull(result);
+ assertTrue(result.contains("00"));
+ assertTrue(result.contains("1f"));
+ assertTrue(result.contains("7f"));
+ // Non-printable chars should be masked as '.'
+ assertTrue(result.contains("."));
+ }
+
+ @Test
+ void testDumpWithHighlights() {
+ byte[] data = new byte[]{0x41, 0x42, 0x43}; // "ABC"
+ String result = Hex.dump(data, Hex.DefaultWidth, 1);
+
+ // Should contain ANSI escape codes for highlight
+ assertTrue(result.contains("\033[0;31m"));
+ assertTrue(result.contains("\033[0m"));
+ }
+
+ @Test
+ void testDumpWithCustomWidth() {
+ byte[] data = new byte[]{0x41, 0x42, 0x43, 0x44, 0x45};
+ String result = Hex.dump(data, 20);
+
+ assertNotNull(result);
+ assertTrue(result.contains("41"));
+ }
+
+ @Test
+ void testDumpLargeData() {
+ byte[] data = new byte[100];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i % 256);
+ }
+
+ String result = Hex.dump(data);
+ assertNotNull(result);
+ // Should have multiple lines
+ assertTrue(result.contains("\n"));
+ }
+
+ @Test
+ void testDumpWithMinimalWidth() {
+ byte[] data = new byte[]{0x41, 0x42};
+ // Very small width should still work
+ String result = Hex.dump(data, 10);
+
+ assertNotNull(result);
+ assertTrue(result.contains("41"));
+ }
+
+ @Test
+ void testConstants() {
+ assertEquals(46, Hex.DefaultWidth);
+ assertEquals(2, Hex.boxLineOverheat);
+ assertEquals(1, Hex.blankWidth);
+ assertEquals(3, Hex.byteWidth);
+ assertEquals(1, Hex.pipeWidth);
+ }
+
+ @Test
+ void testDumpWithDebugOff() {
+ boolean originalDebug = Hex.DebugHex;
+ try {
+ Hex.DebugHex = false;
+ byte[] data = new byte[]{0x41, 0x42, 0x43};
+ String result = Hex.dump(data);
+ assertNotNull(result);
+ } finally {
+ Hex.DebugHex = originalDebug;
+ }
+ }
+
+ @Test
+ void testDumpWithDebugOn() {
+ boolean originalDebug = Hex.DebugHex;
+ try {
+ Hex.DebugHex = true;
+ byte[] data = new byte[]{0x41, 0x42, 0x43};
+ String result = Hex.dump(data);
+ assertNotNull(result);
+ } finally {
+ Hex.DebugHex = originalDebug;
+ }
+ }
+
+ @Test
+ void testDumpPreservesOriginalArray() {
+ byte[] data = new byte[]{0x41, 0x42, 0x43};
+ byte[] originalCopy = data.clone();
+
+ Hex.dump(data);
+
+ assertArrayEquals(originalCopy, data);
+ }
+
+ @Test
+ void testDumpWithLargeArrayIndex() {
+ // Create data large enough to have multi-digit index
+ byte[] data = new byte[1000];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i % 256);
+ }
+
+ String result = Hex.dump(data);
+ assertNotNull(result);
+ // Should contain index like "000|" format
+ assertTrue(result.contains("|"));
+ }
+
+ @Test
+ void testDumpPrintableRange() {
+ // Test characters at boundaries of printable range (32-126)
+ byte[] data = new byte[]{31, 32, 126, 127};
+ String result = Hex.dump(data);
+
+ // 31 and 127 should be masked, 32 (' ') and 126 ('~') should be visible
+ assertTrue(result.contains("1f"));
+ assertTrue(result.contains("20"));
+ assertTrue(result.contains("7e"));
+ assertTrue(result.contains("7f"));
+ }
+}
diff --git a/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/utils/StringUtilsTest.java b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/utils/StringUtilsTest.java
new file mode 100644
index 00000000000..05f9d8aa99e
--- /dev/null
+++ b/plc4j/spi/buffers/ascii-box/src/test/java/org/apache/plc4x/java/spi/buffers/asciiboxbased/utils/utils/StringUtilsTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.asciiboxbased.utils.utils;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class StringUtilsTest {
+
+ @Test
+ void testIsBlankNull() {
+ assertTrue(StringUtils.isBlank(null));
+ }
+
+ @Test
+ void testIsBlankEmpty() {
+ assertTrue(StringUtils.isBlank(""));
+ }
+
+ @Test
+ void testIsBlankWhitespace() {
+ assertTrue(StringUtils.isBlank(" "));
+ assertTrue(StringUtils.isBlank("\t\n"));
+ }
+
+ @Test
+ void testIsBlankNotBlank() {
+ assertFalse(StringUtils.isBlank("a"));
+ assertFalse(StringUtils.isBlank(" a "));
+ }
+
+ @Test
+ void testIsBlankCharSequence() {
+ // Test with StringBuilder (non-String CharSequence)
+ assertTrue(StringUtils.isBlank(new StringBuilder()));
+ assertTrue(StringUtils.isBlank(new StringBuilder(" ")));
+ assertFalse(StringUtils.isBlank(new StringBuilder("abc")));
+ }
+
+ @Test
+ void testRepeatNull() {
+ assertNull(StringUtils.repeat(null, 3));
+ }
+
+ @Test
+ void testRepeatZero() {
+ assertEquals("", StringUtils.repeat("abc", 0));
+ assertEquals("", StringUtils.repeat("abc", -1));
+ }
+
+ @Test
+ void testRepeatEmpty() {
+ assertEquals("", StringUtils.repeat("", 5));
+ }
+
+ @Test
+ void testRepeatNormal() {
+ assertEquals("abcabcabc", StringUtils.repeat("abc", 3));
+ assertEquals("xxx", StringUtils.repeat("x", 3));
+ }
+
+ @Test
+ void testJoinArrayNull() {
+ assertNull(StringUtils.join((Object[]) null, ","));
+ }
+
+ @Test
+ void testJoinArrayEmpty() {
+ assertEquals("", StringUtils.join(new Object[]{}, ","));
+ }
+
+ @Test
+ void testJoinArrayNormal() {
+ assertEquals("a,b,c", StringUtils.join(new Object[]{"a", "b", "c"}, ","));
+ }
+
+ @Test
+ void testJoinArrayNullDelimiter() {
+ assertEquals("abc", StringUtils.join(new Object[]{"a", "b", "c"}, null));
+ }
+
+ @Test
+ void testJoinIterableNull() {
+ assertNull(StringUtils.join((Iterable>) null, ","));
+ }
+
+ @Test
+ void testJoinIterableEmpty() {
+ assertEquals("", StringUtils.join(Collections.emptyList(), ","));
+ }
+
+ @Test
+ void testJoinIterableNormal() {
+ assertEquals("a,b,c", StringUtils.join(Arrays.asList("a", "b", "c"), ","));
+ }
+
+ @Test
+ void testJoinIterableNullDelimiter() {
+ assertEquals("abc", StringUtils.join(Arrays.asList("a", "b", "c"), null));
+ }
+
+ @Test
+ void testTrimNull() {
+ assertNull(StringUtils.trim(null));
+ }
+
+ @Test
+ void testTrimNormal() {
+ assertEquals("abc", StringUtils.trim(" abc "));
+ assertEquals("abc", StringUtils.trim("abc"));
+ }
+
+ @Test
+ void testContainsAnyNull() {
+ assertFalse(StringUtils.containsAny(null, "abc"));
+ assertFalse(StringUtils.containsAny("abc", null));
+ }
+
+ @Test
+ void testContainsAnyEmpty() {
+ assertFalse(StringUtils.containsAny("", "abc"));
+ assertFalse(StringUtils.containsAny("abc", ""));
+ }
+
+ @Test
+ void testContainsAnyFound() {
+ assertTrue(StringUtils.containsAny("hello", "aeiou"));
+ assertTrue(StringUtils.containsAny("xyz", "z"));
+ }
+
+ @Test
+ void testContainsAnyNotFound() {
+ assertFalse(StringUtils.containsAny("bcd", "xyz"));
+ }
+}
diff --git a/plc4j/spi/buffers/byte/pom.xml b/plc4j/spi/buffers/byte/pom.xml
new file mode 100644
index 00000000000..92b231718ae
--- /dev/null
+++ b/plc4j/spi/buffers/byte/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.plc4x
+ plc4j-spi-buffers
+ 0.14.0-SNAPSHOT
+
+
+ plc4j-spi-buffers-byte
+
+ PLC4J: SPI: Buffers: Byte
+
+
+ 2024-02-16T14:53:02Z
+
+
+
+
+ org.apache.plc4x
+ plc4j-spi-buffers-api
+ 0.14.0-SNAPSHOT
+
+
+
+
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java
new file mode 100644
index 00000000000..111e5855b42
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/AbstractBufferByteBased.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased;
+
+import org.apache.plc4x.java.spi.buffers.api.AbstractBuffer;
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrder;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderBigEndian;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public abstract class AbstractBufferByteBased extends AbstractBuffer {
+
+ protected final byte[] buffer;
+ protected final int startBit;
+ protected final int sizeInBits;
+ protected int positionInBits;
+
+ private final ByteOrderManager byteOrderManager;
+ private final EncodingManager encodingManager;
+
+ public AbstractBufferByteBased(byte[] buffer, int startBit, int sizeInBits, ByteOrderManager byteOrderManager, EncodingManager encodingManager, WithOption[] options) {
+ super(options);
+ this.buffer = Objects.requireNonNull(buffer);
+ this.startBit = startBit;
+ this.sizeInBits = sizeInBits;
+ this.positionInBits = 0;
+ this.byteOrderManager = byteOrderManager;
+ this.encodingManager = encodingManager;
+
+ if (startBit < 0 || startBit > sizeInBits) {
+ throw new IllegalArgumentException("Start bit must be between 0 and sizeInBits");
+ }
+ if (sizeInBits > buffer.length * 8) {
+ throw new IllegalArgumentException("Size in bits must not exceed buffer size");
+ }
+ }
+
+ protected ByteOrder getByteOrder(WithOption... options) {
+ Optional byteOrderName = WithByteBasedOption.extractByteOrder(options, getContext());
+ if (byteOrderName.isPresent()) {
+ Optional byteOrder = byteOrderManager.getByteOrder(byteOrderName.get());
+ if (byteOrder.isPresent()) {
+ return byteOrder.get();
+ }
+ }
+ return new ByteOrderBigEndian();
+ }
+
+ protected Optional getUnsignedIntegerEncoding(WithOption... options) {
+ // First, try to get the encoding for this explicit type of field.
+ Optional encodingName = WithOption.extractUnsignedIntegerEncoding(options, getContext());
+ // If that fails, try to get the global encoder.
+ if (encodingName.isEmpty()) {
+ encodingName = WithOption.extractEncoding(options, getContext());
+ if(encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ } else {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ return Optional.empty();
+ }
+
+ protected Optional getSignedIntegerEncoding(WithOption... options) {
+ // First, try to get the encoding for this explicit type of field.
+ Optional encodingName = WithOption.extractSignedIntegerEncoding(options, getContext());
+ // If that fails, try to get the global encoder.
+ if (encodingName.isEmpty()) {
+ encodingName = WithOption.extractEncoding(options, getContext());
+ if(encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ }
+ if (encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ return Optional.empty();
+ }
+
+ protected Optional getFloatEncoding(WithOption... options) {
+ // First, try to get the encoding for this explicit type of field.
+ Optional encodingName = WithOption.extractFloatEncoding(options, getContext());
+ // If that fails, try to get the global encoder.
+ if (encodingName.isEmpty()) {
+ encodingName = WithOption.extractEncoding(options, getContext());
+ if(encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ }
+ if (encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ return Optional.empty();
+ }
+
+ protected Optional getStringEncoding(WithOption... options) {
+ // First, try to get the encoding for this explicit type of field.
+ Optional encodingName = WithOption.extractStringEncoding(options, getContext());
+ // If that fails, try to get the global encoder.
+ if (encodingName.isEmpty()) {
+ encodingName = WithOption.extractEncoding(options, getContext());
+ if(encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ }
+ if (encodingName.isPresent()) {
+ return encodingManager.getEncoding(encodingName.get());
+ }
+ return Optional.empty();
+ }
+
+ public byte[] getBytes() {
+ return buffer;
+ }
+
+ public int getPositionInBits() {
+ return positionInBits;
+ }
+
+ public int getRemainingBits() {
+ return sizeInBits - positionInBits;
+ }
+
+ public void setPositionInBits(int positionInBits) {
+ if (positionInBits < 0 || positionInBits > sizeInBits) {
+ throw new IllegalArgumentException("Position must be between 0 and sizeInBits");
+ }
+ this.positionInBits = positionInBits;
+ }
+
+ protected void ensureAvailable(int bitsNeeded) throws BufferException {
+ if (positionInBits + bitsNeeded > sizeInBits) {
+ throw new BufferException("Not enough bits left to read");
+ }
+ }
+
+ protected boolean isAligned() {
+ return (positionInBits % 8) == 0;
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java
new file mode 100644
index 00000000000..c4bd7df8eba
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferByteBased.java
@@ -0,0 +1,379 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased;
+
+import org.apache.plc4x.java.spi.buffers.api.ReadBuffer;
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingDefault;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingRaw;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Optional;
+
+public class ReadBufferByteBased extends AbstractBufferByteBased implements ReadBuffer, ReadBufferRaw {
+
+ public ReadBufferByteBased(byte[] buffer, WithOption... options) {
+ this(buffer, 0, buffer.length * 8, options);
+ }
+
+ private ReadBufferByteBased(byte[] buffer, int startBit, int sizeInBits, WithOption... options) {
+ super(buffer, startBit, sizeInBits,
+ WithByteBasedOption.extractByteOrderManager(options).orElse(new ByteOrderManager()),
+ WithByteBasedOption.extractEncodingManager(options).orElse(new EncodingManager()),
+ options);
+ }
+
+ @Override
+ public boolean readBit(WithOption... options) throws BufferException {
+ ensureAvailable(1);
+ int absoluteBitIndex = startBit + positionInBits;
+ int byteIndex = absoluteBitIndex / 8;
+ int bitIndex = absoluteBitIndex % 8;
+ boolean bit = (buffer[byteIndex] & (0x80 >> bitIndex)) != 0;
+ positionInBits++;
+ return bit;
+ }
+
+ @Override
+ public byte[] readBits(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 0) {
+ throw new BufferException("Number of bits must be positive");
+ } else if (numBits == 0) {
+ return new byte[0];
+ }
+ ensureAvailable(numBits);
+
+ // Calculate the number of bytes needed to store the bits
+ int numBytes = (numBits + 7) / 8;
+ byte[] result = new byte[numBytes];
+
+ // Simple case, in which we're reading full bytes and we are byte-aligned.
+ // Using this option is significantly quicker.
+ if (isAligned() && (numBits % 8 == 0)) {
+ // Fast path for byte-aligned reads
+ int byteIndex = (startBit + positionInBits) / 8;
+ System.arraycopy(buffer, byteIndex, result, 0, numBytes);
+ positionInBits += numBits;
+ return result;
+ } else {
+ // Potentially align when doing partial byte reads.
+ int resultBitIndex = (8 - (numBits % 8)) % 8;
+ for (int i = 0; i < numBits; i++) {
+ if (readBit()) {
+ int resultByteIndex = resultBitIndex / 8;
+ int resultBitPosition = 7 - (resultBitIndex % 8);
+ result[resultByteIndex] |= (byte) (1 << resultBitPosition);
+ }
+ resultBitIndex++;
+ }
+ return result;
+ }
+ }
+
+ @Override
+ public byte readUnsignedByte(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 7) {
+ throw new BufferException("Unsigned byte can only read between 1 and 7 bits");
+ }
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeByte(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeByte(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public short readUnsignedShort(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 15) {
+ throw new BufferException("Unsigned short can only read between 1 and 15 bits");
+ }
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeShort(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeShort(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public int readUnsignedInt(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 31) {
+ throw new BufferException("Unsigned int can only read between 1 and 31 bits");
+ }
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeInt(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeInt(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public long readUnsignedLong(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 63) {
+ throw new BufferException("Unsigned long can only read between 1 and 63 bits");
+ }
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeLong(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeLong(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public BigInteger readUnsignedBigInteger(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1) {
+ throw new BufferException("Unsigned BigInteger can only read 1 or more bits");
+ }
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeBigInteger(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeBigInteger(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public byte readSignedByte(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 8) {
+ throw new BufferException("Signed byte can only read between 1 and 8 bits");
+ }
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeByte(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeByte(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public short readSignedShort(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 16) {
+ throw new BufferException("Signed short can only read between 1 and 16 bits");
+ }
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeShort(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeShort(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public int readSignedInt(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 32) {
+ throw new BufferException("Signed int can only read between 1 and 32 bits");
+ }
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeInt(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeInt(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public long readSignedLong(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 64) {
+ throw new BufferException("Signed long can only read between 1 and 64 bits");
+ }
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeLong(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeLong(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public BigInteger readSignedBigInteger(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1) {
+ throw new BufferException("Signed BigInteger can only read 1 or more bits");
+ }
+
+ // For signed BigInteger, we can directly use the BigInteger constructor
+ // that takes a byte array, which interprets it as a two's-complement representation
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeBigInteger(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeBigInteger(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public float readFloat(int numBits, WithOption... options) throws BufferException {
+ if (numBits != 32) {
+ throw new BufferException("Float can only be read using 32 bits");
+ }
+
+ Optional encodingOptional = getFloatEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeFloat(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeFloat(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public double readDouble(int numBits, WithOption... options) throws BufferException {
+ if (numBits != 64) {
+ throw new BufferException("Double can only be read using 64 bits");
+ }
+
+ Optional encodingOptional = getFloatEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeDouble(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeDouble(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public BigDecimal readBigDecimal(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 1) {
+ throw new BufferException("BigDecimal can only be read using 1 or more bits");
+ }
+
+ Optional encodingOptional = getFloatEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ bytes = getByteOrder(options).process(bytes);
+ return encodingDefault.decodeBigDecimal(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeBigDecimal(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public String readString(int numBits, WithOption... options) throws BufferException {
+ if (numBits < 0) {
+ throw new BufferException("Number of bits must be non-negative");
+ }
+ if (numBits == 0) {
+ return "";
+ }
+
+ Optional encodingOptional = getStringEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for string values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = readBits(numBits);
+ // Do not apply byte order to UTF-8 strings as it would reverse the bytes
+ // and make the string unreadable
+ return encodingDefault.decodeString(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ return encodingRaw.decodeString(numBits, this);
+ }
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+
+ @Override
+ public ReadBufferByteBased createSubBuffer(int numBits, WithOption... options) throws BufferException {
+ ensureAvailable(numBits);
+ ReadBufferByteBased subBuffer = new ReadBufferByteBased(
+ buffer,
+ startBit + positionInBits,
+ numBits,
+ WithOption.AddOptions(getContext(), options)
+ );
+ positionInBits += numBits;
+ return subBuffer;
+ }
+
+}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoveredEvent.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferRaw.java
similarity index 67%
rename from plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoveredEvent.java
rename to plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferRaw.java
index 34e6417f4ba..74b8ee36627 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/events/DiscoveredEvent.java
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/ReadBufferRaw.java
@@ -16,18 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.plc4x.java.spi.events;
+package org.apache.plc4x.java.spi.buffers.bytebased;
-import org.apache.plc4x.java.spi.configuration.PlcConnectionConfiguration;
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
-public class DiscoveredEvent {
+public interface ReadBufferRaw {
- private final PlcConnectionConfiguration configuration;
+ boolean readBit(WithOption... options) throws BufferException;
- public DiscoveredEvent(PlcConnectionConfiguration c) {
- this.configuration = c;
- }
+ byte[] readBits(int numBits, WithOption... options) throws BufferException;
- public PlcConnectionConfiguration getConfiguration() { return configuration; }
+ int getRemainingBits();
}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WithByteBasedOption.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WithByteBasedOption.java
new file mode 100644
index 00000000000..b3d98251776
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WithByteBasedOption.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager;
+
+import java.util.Optional;
+
+public interface WithByteBasedOption extends WithOption {
+
+ static WithByteBasedOption WithByteOrder(String byteOrderName) {
+ return (withOptionByteOrder) () -> byteOrderName;
+ }
+
+ static WithByteBasedOption WithPaddingChar(String paddingChar) {
+ if (paddingChar.length() != 1) {
+ throw new IllegalArgumentException("Padding character length should be exactly 1");
+ }
+ return (withOptionPaddingChar) () -> paddingChar.charAt(0);
+ }
+
+ static Optional extractByteOrderManager(WithOption[] options) {
+ if (options != null) {
+ for (WithOption option : options) {
+ if (option instanceof withOptionByteOrderManager) {
+ return Optional.of(((withOptionByteOrderManager) option).byteOrderManager());
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ static Optional extractEncodingManager(WithOption[] options) {
+ if (options != null) {
+ for (WithOption option : options) {
+ if (option instanceof withOptionEncodingManager) {
+ return Optional.of(((withOptionEncodingManager) option).encodingManager());
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ static Optional extractByteOrder(WithOption[] options) {
+ if (options != null) {
+ for (WithOption option : options) {
+ if (option instanceof withOptionByteOrder) {
+ return Optional.of(((withOptionByteOrder) option).byteOrderName());
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ static Optional extractByteOrder(WithOption[] options, WithOption[] defaultOptions) {
+ Optional byteOrder = extractByteOrder(options);
+ if (byteOrder.isPresent()) {
+ return byteOrder;
+ }
+ return extractByteOrder(defaultOptions);
+ }
+
+ static Optional extractPaddingChar(WithOption[] options) {
+ if (options != null) {
+ for (WithOption option : options) {
+ if (option instanceof withOptionPaddingChar) {
+ return Optional.of(((withOptionPaddingChar) option).paddingChar());
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ static Optional extractPaddingChar(WithOption[] options, WithOption[] defaultOptions) {
+ Optional byteOrder = extractPaddingChar(options);
+ if (byteOrder.isPresent()) {
+ return byteOrder;
+ }
+ return extractPaddingChar(defaultOptions);
+ }
+}
+
+interface withOptionByteOrderManager extends WithByteBasedOption {
+ ByteOrderManager byteOrderManager();
+
+ default boolean isSticky() {
+ return true;
+ }
+}
+
+interface withOptionEncodingManager extends WithByteBasedOption {
+ EncodingManager encodingManager();
+
+ default boolean isSticky() {
+ return true;
+ }
+}
+
+interface withOptionByteOrder extends WithByteBasedOption {
+ String byteOrderName();
+
+ default boolean isSticky() {
+ return true;
+ }
+}
+
+interface withOptionPaddingChar extends WithByteBasedOption {
+ Character paddingChar();
+
+ default boolean isSticky() {
+ return true;
+ }
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java
new file mode 100644
index 00000000000..6cd62a372f6
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/WriteBufferByteBased.java
@@ -0,0 +1,494 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.api.WriteBuffer;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrder;
+import org.apache.plc4x.java.spi.buffers.bytebased.byteorder.ByteOrderManager;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.Encoding;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingDefault;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingManager;
+import org.apache.plc4x.java.spi.buffers.bytebased.encoding.EncodingRaw;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Optional;
+
+public class WriteBufferByteBased extends AbstractBufferByteBased implements WriteBuffer {
+
+ public WriteBufferByteBased(byte[] buffer, WithOption... defaultOptions) {
+ this(buffer, 0, buffer.length * 8, defaultOptions);
+ }
+
+ public WriteBufferByteBased(byte[] buffer, int startBit, int sizeInBits, WithOption... defaultOptions) {
+ super(buffer, startBit, sizeInBits,
+ WithByteBasedOption.extractByteOrderManager(defaultOptions).orElse(new ByteOrderManager()),
+ WithByteBasedOption.extractEncodingManager(defaultOptions).orElse(new EncodingManager()),
+ defaultOptions);
+ }
+
+ @Override
+ public void writeBit(boolean value, WithOption... options) throws BufferException {
+ ensureAvailable(1);
+ int byteIndex = positionInBits / 8;
+ int bitIndex = positionInBits % 8;
+ if (value) {
+ buffer[byteIndex] |= (byte) (0x80 >> bitIndex);
+ }
+ positionInBits++;
+ }
+
+ @Override
+ public void writeBits(int numBits, byte[] value, WithOption... options) throws BufferException {
+ if (numBits < 0) {
+ throw new BufferException("Number of bits must be positive");
+ } else if (numBits == 0) {
+ return;
+ }
+ ensureAvailable(numBits);
+
+ // Simple case, in which we're writing full bytes and we are byte-aligned.
+ if (isAligned() && (numBits % 8 == 0)) {
+ int byteIndex = (startBit + positionInBits) / 8;
+ System.arraycopy(value, 0, buffer, byteIndex, numBits / 8);
+ positionInBits += numBits;
+ } else {
+ int valueBitIndex = (8 - (numBits % 8)) % 8;
+ for (int i = 0; i < numBits; i++) {
+ int valueByteIndex = valueBitIndex / 8;
+ int valueBitPosition = 7 - (valueBitIndex % 8);
+ boolean bit = (value[valueByteIndex] & (1 << valueBitPosition)) != 0;
+ writeBit(bit);
+ valueBitIndex++;
+ }
+ }
+ }
+
+ @Override
+ public void writeUnsignedByte(int numBits, byte value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 7) {
+ throw new BufferException("Unsigned byte can only read between 1 and 7 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ int maxValue = numBits == 7 ? 0xFF : (1 << numBits) - 1;
+ if (value < 0 || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is 0 to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeByte(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeByte(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeUnsignedShort(int numBits, short value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 15) {
+ throw new BufferException("Unsigned short can only read between 1 and 15 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ long maxValue = numBits == 15 ? 0xFFFFL : (1L << numBits) - 1;
+ if (value < 0 || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is 0 to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeShort(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeShort(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeUnsignedInt(int numBits, int value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 31) {
+ throw new BufferException("Unsigned int can only read between 1 and 31 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ long maxValue = numBits == 31 ? 0xFFFFFFFFL : (1L << numBits) - 1;
+ if (value < 0 || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is 0 to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeInt(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeInt(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeUnsignedLong(int numBits, long value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 63) {
+ throw new BufferException("Unsigned long can only read between 1 and 63 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ long maxValue = numBits == 63 ? 0x7FFFFFFFFFFFFFFFL : (1L << numBits) - 1;
+ if (value < 0 || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is 0 to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeLong(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeLong(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeUnsignedBigInteger(int numBits, BigInteger value, WithOption... options) throws BufferException {
+ if (numBits < 1) {
+ throw new BufferException("Unsigned BigInteger can only read between 1 or more bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ BigInteger maxValue = BigInteger.ONE.shiftLeft(numBits).subtract(BigInteger.ONE);
+ if (value == null || value.compareTo(BigInteger.ZERO) < 0 || value.compareTo(maxValue) > 0) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is 0 to " + maxValue);
+ }
+
+ Optional encodingOptional = getUnsignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for unsigned integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeBigInteger(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeBigInteger(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeSignedByte(int numBits, byte value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 8) {
+ throw new BufferException("Signed byte can only read between 1 and 8 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ int minValue = numBits == 8 ? Byte.MIN_VALUE : -(1 << (numBits - 1));
+ int maxValue = numBits == 8 ? Byte.MAX_VALUE : (1 << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is " + minValue + " to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeByte(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeByte(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeSignedShort(int numBits, short value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 16) {
+ throw new BufferException("Signed short can only read between 1 and 16 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ int minValue = numBits == 16 ? Short.MIN_VALUE : -(1 << (numBits - 1));
+ int maxValue = numBits == 16 ? Short.MAX_VALUE : (1 << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is " + minValue + " to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeShort(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeShort(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeSignedInt(int numBits, int value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 32) {
+ throw new BufferException("Signed int can only read between 1 and 32 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ long minValue = numBits == 32 ? Integer.MIN_VALUE : -(1L << (numBits - 1));
+ long maxValue = numBits == 32 ? Integer.MAX_VALUE : (1L << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is " + minValue + " to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeInt(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeInt(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeSignedLong(int numBits, long value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 64) {
+ throw new BufferException("Signed long can only read between 1 and 64 bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ long minValue = numBits == 64 ? Long.MIN_VALUE : -(1L << (numBits - 1));
+ long maxValue = numBits == 64 ? Long.MAX_VALUE : (1L << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is " + minValue + " to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeLong(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeLong(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ public void writeSignedBigInteger(int numBits, BigInteger value, WithOption... options) throws BufferException {
+ if (numBits < 1) {
+ throw new BufferException("Signed BigInteger can only read between 1 or more bits");
+ }
+ // Check if the value is within the valid range for the given number of bits
+ BigInteger minValue = BigInteger.ONE.shiftLeft(numBits - 1).negate();
+ BigInteger maxValue = BigInteger.ONE.shiftLeft(numBits - 1).subtract(BigInteger.ONE);
+ if (value == null || value.compareTo(minValue) < 0 || value.compareTo(maxValue) > 0) {
+ throw new BufferException("Value " + value + " is out of range for " + numBits + " bits. Valid range is " + minValue + " to " + maxValue);
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getSignedIntegerEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for signed integer values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeBigInteger(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeBigInteger(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeFloat(int numBits, float value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 32) {
+ throw new BufferException("Float can only be written using between 1 and 32 bits");
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getFloatEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for floating point values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeFloat(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeFloat(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeDouble(int numBits, double value, WithOption... options) throws BufferException {
+ if (numBits < 1 || numBits > 64) {
+ throw new BufferException("Double can only be written using between 1 and 64 bits");
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getFloatEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for floating point values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeDouble(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeDouble(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeBigDecimal(int numBits, BigDecimal value, WithOption... options) throws BufferException {
+ if (numBits < 1) {
+ throw new BufferException("BigDecimal can only be written using 1 or more bits");
+ }
+ if (value == null) {
+ throw new BufferException("Value null is out of range for " + numBits + " bits.");
+ }
+ ensureAvailable(numBits);
+
+ Optional encodingOptional = getFloatEncoding(options);
+ Encoding encoding = encodingOptional.orElseThrow(() -> new BufferException("No encoding defined for floating point values"));
+ if(encoding instanceof EncodingDefault encodingDefault) {
+ ensureAvailable(numBits);
+ byte[] bytes = encodingDefault.encodeBigDecimal(numBits, value);
+ bytes = getByteOrder(options).process(bytes);
+ writeBits(numBits, bytes);
+ } else if(encoding instanceof EncodingRaw encodingRaw) {
+ byte[] bytes = encodingRaw.encodeBigDecimal(numBits, value);
+ writeBits(bytes.length * 8, bytes);
+ } else {
+ throw new BufferException("Unsupported encoding type: " + encoding.getClass().getName());
+ }
+ }
+
+ @Override
+ public void writeString(int numBits, String value, WithOption... options) throws BufferException {
+ if (numBits == 0) {
+ return;
+ }
+ if (numBits < 1) {
+ throw new BufferException("Number of bits must be written using 1 or more bits");
+ }
+ if (value == null) {
+ throw new BufferException("Value null is out of range for " + numBits + " bits.");
+ }
+ ensureAvailable(numBits);
+
+ // Convert string to bytes
+ Optional encoding = getStringEncoding(options);
+ // TODO: Possibly add support for the Raw encoding here ...
+ byte[] bytes = encoding.orElseThrow(() -> new BufferException("No encoding defined for string values")).encodeString(numBits, value);
+
+ // If padding is configured, update the 0x00 bytes to the provided character value
+ Optional paddingChar = WithByteBasedOption.extractPaddingChar(options, getContext());
+ if (paddingChar.isPresent()) {
+ Character c = paddingChar.get();
+ for (int i = 0; i < bytes.length; i++) {
+ if (bytes[i] == 0x00) {
+ bytes[i] = (byte) c.charValue();
+ }
+ }
+ }
+
+ // If the string's byte representation is longer than the specified number of bits,
+ // truncate it to fit
+ int maxBytes = (numBits + 7) / 8;
+ if (bytes.length > maxBytes) {
+ byte[] truncatedBytes = new byte[maxBytes];
+ System.arraycopy(bytes, 0, truncatedBytes, 0, maxBytes);
+ bytes = truncatedBytes;
+ }
+
+ // If the string's byte representation is shorter than the specified number of bits,
+ // pad it with zeros
+ if (bytes.length * 8 < numBits) {
+ int paddedLength = (numBits + 7) / 8;
+ byte[] paddedBytes = new byte[paddedLength];
+ System.arraycopy(bytes, 0, paddedBytes, 0, bytes.length);
+ bytes = paddedBytes;
+ }
+
+ // Do not apply byte order to UTF-8 strings as it would reverse the bytes
+ // and make the string unreadable
+ writeBits(numBits, bytes);
+ }
+
+ @Override
+ public WriteBuffer createSubBuffer(int numBits, WithOption... options) throws BufferException {
+ ensureAvailable(numBits);
+ int lengthInBytes = (numBits + 7) / 8;
+ ByteOrder byteOrder = getByteOrder(options);
+ WithOption byteOrderOption = WithByteBasedOption.WithByteOrder(byteOrder.getName());
+ byte[] subBufferBytes = new byte[lengthInBytes];
+ return new WriteBufferByteBased(subBufferBytes, byteOrderOption);
+ }
+
+ @Override
+ public boolean isByteBased() {
+ return true;
+ }
+
+}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/ByteOrder.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrder.java
similarity index 84%
rename from plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/ByteOrder.java
rename to plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrder.java
index f6d23bf803f..5c088c76ef0 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/generation/ByteOrder.java
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrder.java
@@ -16,9 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.plc4x.java.spi.generation;
+package org.apache.plc4x.java.spi.buffers.bytebased.byteorder;
+
+public interface ByteOrder {
+
+ String getName();
+
+ byte[] process(byte[] bytes);
-public enum ByteOrder {
- BIG_ENDIAN,
- LITTLE_ENDIAN
}
diff --git a/plc4j/spi/src/test/java/org/apache/plc4x/java/spi/configuration/config/ParameterConverterTypeConverter.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
similarity index 61%
rename from plc4j/spi/src/test/java/org/apache/plc4x/java/spi/configuration/config/ParameterConverterTypeConverter.java
rename to plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
index ab0d05a41ac..70a6d8c47a5 100644
--- a/plc4j/spi/src/test/java/org/apache/plc4x/java/spi/configuration/config/ParameterConverterTypeConverter.java
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderBigEndian.java
@@ -16,21 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
+package org.apache.plc4x.java.spi.buffers.bytebased.byteorder;
-package org.apache.plc4x.java.spi.configuration.config;
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.bytebased.WithByteBasedOption;
-import org.apache.plc4x.java.spi.configuration.ConfigurationParameterConverter;
+public class ByteOrderBigEndian implements ByteOrder {
-public class ParameterConverterTypeConverter implements ConfigurationParameterConverter {
+ public static final String NAME = "BIG_ENDIAN";
+
+ public static WithOption optionByteOrderBigEndian() {
+ return WithByteBasedOption.WithByteOrder(NAME);
+ }
@Override
- public Class getType() {
- return ParameterConverterType.class;
+ public String getName() {
+ return NAME;
}
@Override
- public ParameterConverterType convert(String value) {
- return new ParameterConverterType(value);
+ public byte[] process(byte[] bytes) {
+ return bytes;
}
}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderLittleEndian.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderLittleEndian.java
new file mode 100644
index 00000000000..17390b7c8d3
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderLittleEndian.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.byteorder;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.bytebased.WithByteBasedOption;
+
+public class ByteOrderLittleEndian implements ByteOrder {
+
+ public static final String NAME = "LITTLE_ENDIAN";
+
+ public static WithOption optionByteOrderLittleEndian() {
+ return WithByteBasedOption.WithByteOrder(NAME);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public byte[] process(byte[] bytes) {
+ int left = 0;
+ int right = bytes.length - 1;
+ byte temp;
+ while (left < right) {
+ temp = bytes[left];
+ bytes[left] = bytes[right];
+ bytes[right] = temp;
+ left++;
+ right--;
+ }
+ return bytes;
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderManager.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderManager.java
new file mode 100644
index 00000000000..b4aae29d945
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/byteorder/ByteOrderManager.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.byteorder;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ServiceLoader;
+
+public class ByteOrderManager {
+
+ private final Map byteOrderMap;
+
+ public ByteOrderManager() {
+ this(Thread.currentThread().getContextClassLoader());
+ }
+
+ public ByteOrderManager(ClassLoader classLoader) {
+ this.byteOrderMap = new HashMap<>();
+ ServiceLoader byteOrders = ServiceLoader.load(ByteOrder.class, classLoader);
+ for (ByteOrder byteOrder : byteOrders) {
+ if (byteOrderMap.containsKey(byteOrder.getName())) {
+ throw new IllegalStateException(
+ "Multiple byte order implementations available for byte order name '" +
+ byteOrder.getName() + "'");
+ }
+ byteOrderMap.put(byteOrder.getName(), byteOrder);
+ }
+ }
+
+ public Optional getByteOrder(String byteOrderName) {
+ ByteOrder byteOrder = byteOrderMap.get(byteOrderName);
+ return Optional.ofNullable(byteOrder);
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseEncodingDefault.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseEncodingDefault.java
new file mode 100644
index 00000000000..44dc1d6c8b4
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseEncodingDefault.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public abstract class BaseEncodingDefault implements EncodingDefault {
+
+ @Override
+ public byte[] encodeByte(int numBits, byte value) {
+ throw new UnsupportedOperationException("encoding byte is not supported by this encoding");
+ }
+
+ @Override
+ public byte decodeByte(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding byte is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeShort(int numBits, short value) {
+ throw new UnsupportedOperationException("encoding short is not supported by this encoding");
+ }
+
+ @Override
+ public short decodeShort(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding short is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeInt(int numBits, int value) {
+ throw new UnsupportedOperationException("encoding int is not supported by this encoding");
+ }
+
+ @Override
+ public int decodeInt(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding int is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeLong(int numBits, long value) {
+ throw new UnsupportedOperationException("encoding long is not supported by this encoding");
+ }
+
+ @Override
+ public long decodeLong(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding long is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeBigInteger(int numBits, BigInteger value) {
+ throw new UnsupportedOperationException("encoding BigInteger is not supported by this encoding");
+ }
+
+ @Override
+ public BigInteger decodeBigInteger(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding BigInteger is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeFloat(int numBits, float value) {
+ throw new UnsupportedOperationException("encoding float is not supported by this encoding");
+ }
+
+ @Override
+ public float decodeFloat(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding float is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeDouble(int numBits, double value) {
+ throw new UnsupportedOperationException("encoding double is not supported by this encoding");
+ }
+
+ @Override
+ public double decodeDouble(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding double is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeBigDecimal(int numBits, BigDecimal value) {
+ throw new UnsupportedOperationException("encoding string is not supported by this encoding");
+ }
+
+ @Override
+ public BigDecimal decodeBigDecimal(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding string is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeString(int numBits, String value) {
+ throw new UnsupportedOperationException("encoding string is not supported by this encoding");
+ }
+
+ @Override
+ public String decodeString(int numBits, byte[] bytes) {
+ throw new UnsupportedOperationException("decoding string is not supported by this encoding");
+ }
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseEncodingRaw.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseEncodingRaw.java
new file mode 100644
index 00000000000..6c3125f1245
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseEncodingRaw.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.bytebased.ReadBufferRaw;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public abstract class BaseEncodingRaw implements EncodingRaw {
+
+ @Override
+ public byte[] encodeByte(int numBits, byte value) {
+ throw new UnsupportedOperationException("encoding byte is not supported by this encoding");
+ }
+
+ @Override
+ public byte decodeByte(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding byte is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeShort(int numBits, short value) {
+ throw new UnsupportedOperationException("encoding short is not supported by this encoding");
+ }
+
+ @Override
+ public short decodeShort(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding short is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeInt(int numBits, int value) {
+ throw new UnsupportedOperationException("encoding int is not supported by this encoding");
+ }
+
+ @Override
+ public int decodeInt(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding int is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeLong(int numBits, long value) {
+ throw new UnsupportedOperationException("encoding long is not supported by this encoding");
+ }
+
+ @Override
+ public long decodeLong(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding long is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeBigInteger(int numBits, BigInteger value) {
+ throw new UnsupportedOperationException("encoding BigInteger is not supported by this encoding");
+ }
+
+ @Override
+ public BigInteger decodeBigInteger(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding BigInteger is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeFloat(int numBits, float value) {
+ throw new UnsupportedOperationException("encoding float is not supported by this encoding");
+ }
+
+ @Override
+ public float decodeFloat(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding float is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeDouble(int numBits, double value) {
+ throw new UnsupportedOperationException("encoding double is not supported by this encoding");
+ }
+
+ @Override
+ public double decodeDouble(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding double is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeBigDecimal(int numBits, BigDecimal value) {
+ throw new UnsupportedOperationException("encoding string is not supported by this encoding");
+ }
+
+ @Override
+ public BigDecimal decodeBigDecimal(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding string is not supported by this encoding");
+ }
+
+ @Override
+ public byte[] encodeString(int numBits, String value) {
+ throw new UnsupportedOperationException("encoding string is not supported by this encoding");
+ }
+
+ @Override
+ public String decodeString(int numBits, ReadBufferRaw readBuffer) {
+ throw new UnsupportedOperationException("decoding string is not supported by this encoding");
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseStringEncoding.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseStringEncoding.java
new file mode 100644
index 00000000000..49864e6cc91
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/BaseStringEncoding.java
@@ -0,0 +1,258 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferValueException;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.charset.Charset;
+
+public abstract class BaseStringEncoding implements EncodingDefault {
+
+ protected abstract int getBitsPerCharacter();
+
+ protected abstract Charset getCharset();
+
+ @Override
+ public byte[] encodeByte(int numBits, byte value) throws BufferException {
+ String stringValue = Byte.toString(value);
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public byte decodeByte(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return Byte.parseByte(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("Byte value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeShort(int numBits, short value) throws BufferException {
+ String stringValue = Short.toString(value);
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public short decodeShort(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return Short.parseShort(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("Short value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeInt(int numBits, int value) throws BufferException {
+ String stringValue = Integer.toString(value);
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public int decodeInt(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return Integer.parseInt(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("Integer value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeLong(int numBits, long value) throws BufferException {
+ String stringValue = Long.toString(value);
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public long decodeLong(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return Long.parseLong(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("Long value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeBigInteger(int numBits, BigInteger value) throws BufferException {
+ String stringValue = value.toString();
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public BigInteger decodeBigInteger(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return new BigInteger(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("BigInteger value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeFloat(int numBits, float value) throws BufferException {
+ String stringValue = Float.toString(value);
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public float decodeFloat(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return Float.parseFloat(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("Float value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeDouble(int numBits, double value) throws BufferException {
+ String stringValue = Double.toString(value);
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public double decodeDouble(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return Double.parseDouble(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("Double value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeBigDecimal(int numBits, BigDecimal value) throws BufferException {
+ String stringValue = value.toString();
+ return encodeString(numBits, stringValue, true);
+ }
+
+ @Override
+ public BigDecimal decodeBigDecimal(int numBits, byte[] bytes) throws BufferException {
+ String stringValue = decodeString(numBits, bytes);
+ try {
+ return new BigDecimal(stringValue.trim());
+ } catch (NumberFormatException e) {
+ throw new BufferValueException("BigDecimal value cannot be parsed from string: " + stringValue, bytes);
+ }
+ }
+
+ @Override
+ public byte[] encodeString(int numBits, String value) throws BufferException {
+ return encodeString(numBits, value, false);
+ }
+
+ public byte[] encodeString(int numBits, String value, boolean numericValue) throws BufferException {
+ if (value == null) {
+ throw new BufferException("value must not be null");
+ }
+ if (numBits % getBitsPerCharacter() != 0) {
+ throw new BufferException("numBits must be a multiple of " + getBitsPerCharacter());
+ }
+ int numBytes = numBits / 8;
+ byte[] bytes = value.getBytes(getCharset());
+
+ // Check if the string can be properly encoded in the given charset
+ String roundTrip = new String(bytes, getCharset());
+ if (!roundTrip.equals(value)) {
+ throw new BufferException(
+ String.format("String contains characters that cannot be encoded in %s",
+ getCharset().displayName()));
+ }
+
+ // If it's a negative value, strip the leading '-' character
+ boolean negative = numericValue && value.charAt(0) == '-';
+ if (negative) {
+ bytes = value.substring(1).getBytes(getCharset());
+ }
+
+ if (bytes.length > numBytes) {
+ throw new BufferException(
+ String.format("String requires %d bits but only %d bits available",
+ bytes.length * getBitsPerCharacter(), numBits));
+ }
+ byte[] newBytes = new byte[numBytes];
+ // Numeric values are left padded with 0s
+ if (numericValue) {
+ // Fill with the charset-specific encoding of a single space character
+ byte[] spaceBytes = "0".getBytes(getCharset());
+ for (int i = 0; i < numBytes; i += spaceBytes.length) {
+ System.arraycopy(spaceBytes, 0, newBytes, i, Math.min(spaceBytes.length, numBytes - i));
+ }
+ // Negative values must have their negativity sign in the first char.
+ if (negative) {
+ byte[] minusBytes = "-".getBytes(getCharset());
+ System.arraycopy(minusBytes, 0, newBytes, 0, minusBytes.length);
+ }
+ }
+ System.arraycopy(bytes, 0, newBytes, numericValue ? numBytes - bytes.length : 0, bytes.length);
+ return newBytes;
+ }
+
+ @Override
+ public String decodeString(int numBits, byte[] bytes) throws BufferException {
+ if (bytes == null) {
+ throw new BufferException("Cannot decode null byte array");
+ }
+ if (numBits % getBitsPerCharacter() != 0) {
+ throw new BufferException("numBits must be a multiple of " + getBitsPerCharacter());
+ }
+ int numBytes = numBits / 8;
+ if (bytes.length > numBytes) {
+ throw new BufferException("byte array is too long to be decoded using " + numBits + " bits. Byte array length is " + (bytes.length * 8) + " bits.");
+ } else if (bytes.length < numBytes) {
+ throw new BufferException("byte array is too short to be decoded using " + numBits + " bits. Byte array length is " + (bytes.length * 8) + " bits.");
+ }
+ // If the entire byte array is composed only of encoded space characters, keep it as-is (do not trim)
+ byte[] spaceBytes = " ".getBytes(getCharset());
+ boolean onlySpaces = bytes.length > 0 && (bytes.length % spaceBytes.length == 0);
+ if (onlySpaces) {
+ for (int i = 0; i < bytes.length; i += spaceBytes.length) {
+ for (int j = 0; j < spaceBytes.length; j++) {
+ if (i + j >= bytes.length || bytes[i + j] != spaceBytes[j]) {
+ onlySpaces = false;
+ break;
+ }
+ }
+ if (!onlySpaces) break;
+ }
+ }
+
+ String decoded = new String(bytes, getCharset());//.replace("\u0000", "");
+ if (onlySpaces) {
+ return decoded;
+ }
+ if(decoded.contains("\u0000")) {
+ decoded = decoded.substring(0, decoded.indexOf("\u0000"));
+ }
+ // Remove trailing padding spaces that may have been added during encoding
+ int end = decoded.length();
+ while (end > 0 && decoded.charAt(end - 1) == ' ') {
+ end--;
+ }
+ return ((end == decoded.length()) ? decoded : decoded.substring(0, end)).replace("\uFEFF", "");
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/Encoding.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/Encoding.java
new file mode 100644
index 00000000000..a249ecb4e67
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/Encoding.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * Both the normal and the raw encoders share the methods for encoding data.
+ */
+public interface Encoding {
+
+ String getName();
+
+ byte[] encodeByte(int numBits, byte value) throws BufferException;
+
+ byte[] encodeShort(int numBits, short value) throws BufferException;
+
+ byte[] encodeInt(int numBits, int value) throws BufferException;
+
+ byte[] encodeLong(int numBits, long value) throws BufferException;
+
+ byte[] encodeBigInteger(int numBits, BigInteger value) throws BufferException;
+
+ byte[] encodeFloat(int numBits, float value) throws BufferException;
+
+ byte[] encodeDouble(int numBits, double value) throws BufferException;
+
+ byte[] encodeBigDecimal(int numBits, BigDecimal value) throws BufferException;
+
+ byte[] encodeString(int numBits, String value) throws BufferException;
+
+}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/internal/DefaultContextHandler.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingASCII.java
similarity index 54%
rename from plc4j/spi/src/main/java/org/apache/plc4x/java/spi/internal/DefaultContextHandler.java
rename to plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingASCII.java
index a619dc4df3b..a623a654d00 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/internal/DefaultContextHandler.java
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingASCII.java
@@ -16,34 +16,38 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.plc4x.java.spi.internal;
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import org.apache.plc4x.java.spi.ConversationContext;
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
-class DefaultContextHandler implements ConversationContext.ContextHandler {
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
- private final Future awaitable;
- private final Runnable cancel;
+/**
+ * In ASCII encoding, numbers are simply represented as their ASCII-encoded string value
+ * Each value must have a bit-length that must be a multiple of 8.
+ */
+public class EncodingASCII extends BaseStringEncoding {
+
+ public static final String NAME = "ASCII";
- public DefaultContextHandler(Future awaitable, Runnable cancel) {
- this.awaitable = awaitable;
- this.cancel = cancel;
+ public static WithOption optionEncodingASCII() {
+ return WithOption.WithEncoding(NAME);
}
@Override
- public boolean isDone() {
- return this.awaitable.isDone();
+ public String getName() {
+ return NAME;
}
@Override
- public void cancel() {
- this.cancel.run();
+ protected int getBitsPerCharacter() {
+ return 8;
}
@Override
- public void await() throws InterruptedException, ExecutionException {
- this.awaitable.get();
+ protected Charset getCharset() {
+ return StandardCharsets.US_ASCII;
}
+
}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingBCD.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingBCD.java
new file mode 100644
index 00000000000..1f99aed5e64
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingBCD.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+
+import java.math.BigInteger;
+
+/**
+ * BCD = Binary Encoded Decimal (A decimal number is represented by a sequence of 4-bit hexadecimal values from 0-9)
+ * ...
+ */
+public class EncodingBCD extends BaseEncodingDefault {
+
+ public static final String NAME = "BCD";
+
+ public static WithOption optionEncodingBCD() {
+ return WithOption.WithEncoding(NAME);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public byte[] encodeByte(int numBits, byte value) {
+ return encodeInt(numBits, value & 0xFF);
+ }
+
+ @Override
+ public byte decodeByte(int numBits, byte[] bytes) {
+ int intValue = decodeInt(numBits, bytes);
+
+ if (intValue > 255) {
+ throw new IllegalArgumentException("Decoded value too large for byte: " + intValue);
+ }
+
+ return (byte) intValue;
+ }
+
+ @Override
+ public byte[] encodeShort(int numBits, short value) {
+ return encodeInt(numBits, value & 0xFFFF);
+ }
+
+ @Override
+ public short decodeShort(int numBits, byte[] bytes) {
+ int intValue = decodeInt(numBits, bytes);
+
+ if (intValue > 65535) {
+ throw new IllegalArgumentException("Decoded value too large for short: " + intValue);
+ }
+
+ return (short) intValue;
+ }
+
+ @Override
+ public byte[] encodeInt(int numBits, int value) {
+ if (numBits % 4 != 0) {
+ throw new IllegalArgumentException("numBits must be a multiple of 4");
+ }
+ if (value < 0) {
+ throw new IllegalArgumentException("BCD encoding only supports non-negative integers");
+ }
+
+ // Add value range validation
+ int numDigits = numBits / 4;
+ int maxValue = (int) Math.pow(10, numDigits);
+ if (value >= maxValue) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d BCD digits (max value: %d)",
+ value, numDigits, maxValue - 1));
+ }
+
+ // Extract decimal digits from least significant to most
+ byte[] digits = new byte[numDigits];
+ for (int i = numDigits - 1; i >= 0; i--) {
+ digits[i] = (byte) (value % 10);
+ value /= 10;
+ }
+
+ // Pack digits into bytes (two digits per byte)
+ byte[] result = new byte[(numDigits + 1) / 2];
+ for (int i = 0; i < numDigits; i += 2) {
+ int high = digits[i];
+ int low = (i + 1 < numDigits) ? digits[i + 1] : 0;
+ result[i / 2] = (byte) ((high << 4) | low);
+ }
+
+ return result;
+ }
+
+ @Override
+ public int decodeInt(int numBits, byte[] bytes) {
+ if (numBits % 4 != 0) {
+ throw new IllegalArgumentException("numBits must be a multiple of 4");
+ }
+
+ int numDigits = numBits / 4;
+ int value = 0;
+
+ for (int i = 0; i < numDigits; i++) {
+ int byteIndex = i / 2;
+ boolean evenDigitNumber = (i % 2 == 0);
+
+ int digit;
+ if (evenDigitNumber) {
+ digit = (bytes[byteIndex] >> 4) & 0x0F;
+ } else {
+ digit = bytes[byteIndex] & 0x0F;
+ }
+
+ if (digit > 9) {
+ throw new IllegalArgumentException("Invalid BCD digit: " + digit);
+ }
+
+ value = value * 10 + digit;
+ }
+ return value;
+ }
+
+ @Override
+ public byte[] encodeLong(int numBits, long value) {
+ if (numBits % 4 != 0) {
+ throw new IllegalArgumentException("numBits must be a multiple of 4");
+ }
+ if (value < 0) {
+ throw new IllegalArgumentException("BCD encoding only supports non-negative integers");
+ }
+
+ // Add value range validation
+ int numDigits = numBits / 4;
+ long maxValue = (long) Math.pow(10, numDigits);
+ if (value >= maxValue) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d BCD digits (max value: %d)",
+ value, numDigits, maxValue - 1));
+ }
+
+ // Extract decimal digits from least significant to most
+ byte[] digits = new byte[numDigits];
+ for (int i = numDigits - 1; i >= 0; i--) {
+ digits[i] = (byte) (value % 10);
+ value /= 10;
+ }
+
+ // Pack digits into bytes (two digits per byte)
+ byte[] result = new byte[(numDigits + 1) / 2];
+ for (int i = 0; i < numDigits; i += 2) {
+ int high = digits[i];
+ int low = (i + 1 < numDigits) ? digits[i + 1] : 0;
+ result[i / 2] = (byte) ((high << 4) | low);
+ }
+
+ return result;
+ }
+
+ @Override
+ public long decodeLong(int numBits, byte[] bytes) {
+ if (numBits % 4 != 0) {
+ throw new IllegalArgumentException("numBits must be a multiple of 4");
+ }
+
+ int numDigits = numBits / 4;
+ long value = 0;
+
+ for (int i = 0; i < numDigits; i++) {
+ int byteIndex = i / 2;
+ boolean evenDigitNumber = (i % 2 == 0);
+
+ int digit;
+ if (evenDigitNumber) {
+ digit = (bytes[byteIndex] >> 4) & 0x0F;
+ } else {
+ digit = bytes[byteIndex] & 0x0F;
+ }
+
+ if (digit > 9) {
+ throw new IllegalArgumentException("Invalid BCD digit: " + digit);
+ }
+
+ value = value * 10 + digit;
+ }
+ return value;
+ }
+
+
+ @Override
+ public byte[] encodeBigInteger(int numBits, BigInteger value) {
+ if (numBits % 4 != 0) {
+ throw new IllegalArgumentException("numBits must be a multiple of 4");
+ }
+ if (value.compareTo(BigInteger.ZERO) < 0) {
+ throw new IllegalArgumentException("BCD encoding only supports non-negative integers");
+ }
+
+ // Add value range validation
+ int numDigits = numBits / 4;
+ BigInteger maxValue = BigInteger.TEN.pow(numDigits);
+ if (value.compareTo(maxValue) >= 0) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d BCD digits (max value: %d)",
+ value, numDigits, maxValue.subtract(BigInteger.ONE)));
+ }
+
+ // Extract decimal digits from least significant to most
+ byte[] digits = new byte[numDigits];
+ for (int i = numDigits - 1; i >= 0; i--) {
+ digits[i] = value.mod(BigInteger.TEN).byteValue();
+ value = value.divide(BigInteger.TEN);
+ }
+
+ // Pack digits into bytes (two digits per byte)
+ byte[] result = new byte[(numDigits + 1) / 2];
+ for (int i = 0; i < numDigits; i += 2) {
+ int high = digits[i];
+ int low = (i + 1 < numDigits) ? digits[i + 1] : 0;
+ result[i / 2] = (byte) ((high << 4) | low);
+ }
+
+ return result;
+ }
+
+ @Override
+ public BigInteger decodeBigInteger(int numBits, byte[] bytes) {
+ if (numBits % 4 != 0) {
+ throw new IllegalArgumentException("numBits must be a multiple of 4");
+ }
+
+ int numDigits = numBits / 4;
+ BigInteger value = BigInteger.ZERO;
+
+ for (int i = 0; i < numDigits; i++) {
+ int byteIndex = i / 2;
+ boolean evenDigitNumber = (i % 2 == 0);
+
+ int digit;
+ if (evenDigitNumber) {
+ digit = (bytes[byteIndex] >> 4) & 0x0F;
+ } else {
+ digit = bytes[byteIndex] & 0x0F;
+ }
+
+ if (digit > 9) {
+ throw new IllegalArgumentException("Invalid BCD digit: " + digit);
+ }
+
+ value = value.multiply(BigInteger.TEN).add(BigInteger.valueOf(digit));
+ }
+ return value;
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingDefault.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingDefault.java
new file mode 100644
index 00000000000..7b4c87b089c
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingDefault.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * Interface for the decoding part of regular encodings.
+ */
+public interface EncodingDefault extends Encoding {
+
+ byte decodeByte(int numBits, byte[] bytes) throws BufferException;
+
+ short decodeShort(int numBits, byte[] bytes) throws BufferException;
+
+ int decodeInt(int numBits, byte[] bytes) throws BufferException;
+
+ long decodeLong(int numBits, byte[] bytes) throws BufferException;
+
+ BigInteger decodeBigInteger(int numBits, byte[] bytes) throws BufferException;
+
+ float decodeFloat(int numBits, byte[] bytes) throws BufferException;
+
+ double decodeDouble(int numBits, byte[] bytes) throws BufferException;
+
+ BigDecimal decodeBigDecimal(int numBits, byte[] bytes) throws BufferException;
+
+ String decodeString(int numBits, byte[] bytes) throws BufferException;
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingIEEE754.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingIEEE754.java
new file mode 100644
index 00000000000..ce83d0a0eb2
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingIEEE754.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.bytebased.ReadBufferByteBased;
+import org.apache.plc4x.java.spi.buffers.bytebased.WriteBufferByteBased;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+public class EncodingIEEE754 extends BaseEncodingDefault {
+
+ public static final String NAME = "IEEE754";
+
+ public static WithOption optionEncodingIEEE754() {
+ return WithOption.WithEncoding(NAME);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public byte[] encodeFloat(int numBits, float value) {
+ if (numBits == 32) {
+ // For 32-bit float, all values are supported as IEEE 754 can represent them
+ int bits = Float.floatToIntBits(value);
+ return new byte[]{
+ (byte) ((bits >>> 24) & 0xFF),
+ (byte) ((bits >>> 16) & 0xFF),
+ (byte) ((bits >>> 8) & 0xFF),
+ (byte) (bits & 0xFF)
+ };
+ } else if (numBits == 16) {
+ // For 16-bit float, check if value is within representable range
+ float absValue = Math.abs(value);
+ if (!Float.isNaN(value) && !Float.isInfinite(value) &&
+ absValue != 0.0f &&
+ (absValue < 6.104e-5f || absValue > 65504.0f)) {
+ throw new IllegalArgumentException(
+ String.format("Value %e cannot be represented as 16-bit float (valid range: ±6.104e-5 to ±65504)",
+ value));
+ }
+ return encodeHalfPrecision(value);
+ } else {
+ throw new IllegalArgumentException("Only 16-bit and 32-bit IEEE 754 floats are supported");
+ }
+ }
+
+ private byte[] encodeHalfPrecision(float value) {
+ int intBits = Float.floatToIntBits(value);
+ int sign = (intBits >>> 16) & 0x8000; // sign bit
+
+ int exponent = ((intBits >>> 23) & 0xFF) - 127 + 15;
+ int mantissa = (intBits >>> 13) & 0x3FF; // 10-bit mantissa
+
+ if (exponent <= 0) {
+ // Subnormal or underflow
+ if (exponent < -10) {
+ return new byte[]{(byte) (sign >>> 8), (byte) sign};
+ }
+ mantissa = (intBits & 0x7FFFFF) | 0x800000;
+ int shift = 1 - exponent;
+ mantissa = mantissa >>> (13 + shift);
+ return new byte[]{
+ (byte) ((sign | mantissa) >>> 8),
+ (byte) (sign | mantissa)
+ };
+ } else if (exponent >= 31) {
+ // Infinity or NaN
+ int half = sign | 0x7C00 | ((intBits & 0x7FFFFF) != 0 ? 0x200 : 0);
+ return new byte[]{(byte) (half >>> 8), (byte) half};
+ } else {
+ int half = sign | (exponent << 10) | mantissa;
+ return new byte[]{(byte) (half >>> 8), (byte) half};
+ }
+ }
+
+ @Override
+ public float decodeFloat(int numBits, byte[] bytes) {
+ if (numBits == 32) {
+ if (bytes.length < 4) {
+ throw new IllegalArgumentException("At least 4 bytes required for 32-bit float");
+ }
+ int bits = ((bytes[0] & 0xFF) << 24) |
+ ((bytes[1] & 0xFF) << 16) |
+ ((bytes[2] & 0xFF) << 8) |
+ (bytes[3] & 0xFF);
+ return Float.intBitsToFloat(bits);
+ } else if (numBits == 16) {
+ if (bytes.length < 2) {
+ throw new IllegalArgumentException("At least 2 bytes required for 16-bit float");
+ }
+ int bits = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);
+ return decodeHalfPrecision(bits);
+ } else {
+ throw new IllegalArgumentException("Only 16-bit and 32-bit IEEE 754 floats are supported");
+ }
+ }
+
+ private float decodeHalfPrecision(int halfBits) {
+ int sign = (halfBits >>> 15) & 0x1;
+ int exponent = (halfBits >>> 10) & 0x1F;
+ int mantissa = halfBits & 0x3FF;
+
+ int fullBits;
+ if (exponent == 0) {
+ if (mantissa == 0) {
+ // Zero
+ fullBits = sign << 31;
+ } else {
+ // Subnormal
+ exponent = 1;
+ while ((mantissa & 0x400) == 0) {
+ mantissa <<= 1;
+ exponent--;
+ }
+ mantissa &= 0x3FF;
+ exponent = exponent - 15 + 127;
+ fullBits = (sign << 31) | (exponent << 23) | (mantissa << 13);
+ }
+ } else if (exponent == 0x1F) {
+ // Infinity or NaN
+ fullBits = (sign << 31) | 0x7F800000 | (mantissa << 13);
+ } else {
+ // Normalized
+ exponent = exponent - 15 + 127;
+ fullBits = (sign << 31) | (exponent << 23) | (mantissa << 13);
+ }
+
+ return Float.intBitsToFloat(fullBits);
+ }
+
+ @Override
+ public byte[] encodeDouble(int numBits, double value) {
+ if (numBits != 64) {
+ throw new IllegalArgumentException("Only 64-bit IEEE 754 doubles are supported");
+ }
+
+ long bits = Double.doubleToLongBits(value);
+ return new byte[]{
+ (byte) ((bits >>> 56) & 0xFF),
+ (byte) ((bits >>> 48) & 0xFF),
+ (byte) ((bits >>> 40) & 0xFF),
+ (byte) ((bits >>> 32) & 0xFF),
+ (byte) ((bits >>> 24) & 0xFF),
+ (byte) ((bits >>> 16) & 0xFF),
+ (byte) ((bits >>> 8) & 0xFF),
+ (byte) (bits & 0xFF)
+ };
+ }
+
+ @Override
+ public double decodeDouble(int numBits, byte[] bytes) {
+ if (numBits != 64) {
+ throw new IllegalArgumentException("Only 64-bit IEEE 754 doubles are supported");
+ }
+ if (bytes.length < 8) {
+ throw new IllegalArgumentException("At least 8 bytes are required to decode a 64-bit double");
+ }
+
+ long bits = ((long) (bytes[0] & 0xFF) << 56) |
+ ((long) (bytes[1] & 0xFF) << 48) |
+ ((long) (bytes[2] & 0xFF) << 40) |
+ ((long) (bytes[3] & 0xFF) << 32) |
+ ((long) (bytes[4] & 0xFF) << 24) |
+ ((long) (bytes[5] & 0xFF) << 16) |
+ ((long) (bytes[6] & 0xFF) << 8) |
+ ((long) (bytes[7] & 0xFF));
+
+ return Double.longBitsToDouble(bits);
+ }
+
+ @Override
+ public byte[] encodeBigDecimal(int numBits, BigDecimal value) {
+ WriteBufferByteBased writeBuffer = new WriteBufferByteBased(new byte[(numBits + 7) / 8], EncodingTwosComplement.optionEncodingTwosComplement());
+
+ // Convert BigDecimal to BigInteger by scaling and then getting unscaled value
+ BigInteger unscaledValue = value.unscaledValue();
+ int scale = value.scale();
+
+ // First 32 bits for scale, rest for unscaled value
+ int scaleNumBits = Math.min(32, numBits);
+ int valueNumBits = numBits - scaleNumBits;
+
+ try {
+ // Write scale
+ writeBuffer.writeSignedInt(scaleNumBits, scale);
+
+ // Write unscaled value if there are bits left
+ if (valueNumBits > 0) {
+ writeBuffer.writeSignedBigInteger(valueNumBits, unscaledValue);
+ }
+ return writeBuffer.getBytes();
+ } catch (BufferException e) {
+ throw new RuntimeException("Error encoding BigDecimal", e);
+ }
+ }
+
+ @Override
+ public BigDecimal decodeBigDecimal(int numBits, byte[] bytes) {
+ ReadBufferByteBased readBuffer = new ReadBufferByteBased(bytes, EncodingTwosComplement.optionEncodingTwosComplement());
+
+ // First 32 bits for scale. Rest for unscaled value
+ int scaleNumBits = Math.min(32, numBits);
+ int valueNumBits = numBits - scaleNumBits;
+
+ try {
+ // Read scale
+ int scale = readBuffer.readSignedInt(scaleNumBits);
+
+ // Read unscaled value if there are bits left
+ BigInteger unscaledValue = BigInteger.ZERO;
+ if (valueNumBits > 0) {
+ unscaledValue = readBuffer.readSignedBigInteger(valueNumBits);
+ }
+
+ return new BigDecimal(unscaledValue, scale);
+ } catch (BufferException e) {
+ throw new RuntimeException("Error decoding BigDecimal", e);
+ }
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingIso88591.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingIso88591.java
new file mode 100644
index 00000000000..5faa835cfb0
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingIso88591.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public class EncodingIso88591 extends BaseStringEncoding {
+
+ public static final String NAME = "ISO-8859-1";
+
+ public static WithOption optionEncodingIso88591() {
+ return WithOption.WithEncoding(NAME);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ protected int getBitsPerCharacter() {
+ return 8; // Iso88591 is a single-byte encoding
+ }
+
+ @Override
+ protected Charset getCharset() {
+ return StandardCharsets.ISO_8859_1;
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingKnxFloat.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingKnxFloat.java
new file mode 100644
index 00000000000..42ef6fdf1b6
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingKnxFloat.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+
+/**
+ * TODO: This should be moved into the KNX driver
+ */
+public class EncodingKnxFloat extends BaseEncodingDefault {
+
+ public static final String NAME = "KNXFloat";
+
+ public static WithOption optionEncodingKNXFloat() {
+ return WithOption.WithEncoding(NAME);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public byte[] encodeFloat(int numBits, float value) {
+ if (numBits != 16) {
+ throw new IllegalArgumentException("Only 16 bit encoding supported");
+ }
+ if (Float.isNaN(value)) {
+ throw new IllegalArgumentException("KNX float does not support NaN");
+ }
+ if (Float.isInfinite(value)) {
+ throw new IllegalArgumentException("KNX float does not support Infinity");
+ }
+
+ // Calculate the maximum representable value
+ // Max mantissa is 0x07FF (2047) with max exponent 0x0F (15)
+ float maxValue = 2047.0f * (float) Math.pow(2, 15) / 100.0f; // Scale back from fixed-point
+ if (Math.abs(value) > maxValue) {
+ throw new IllegalArgumentException(
+ String.format("Value %e is outside the valid range for KNX float (±%e)",
+ value, maxValue));
+ }
+
+ // Scale the value to get the mantissa (fixed-point with 2 decimal places)
+ int raw = Math.round(value * 100.0f);
+
+ boolean sign = raw < 0;
+ int mantissa = Math.abs(raw);
+ int exponent = 0;
+
+ // Normalize mantissa to fit in 11 bits (0x7FF = 2047)
+ while (mantissa > 0x07FF) {
+ mantissa >>= 1;
+ exponent++;
+ }
+
+ if (sign) {
+ mantissa = (~mantissa + 1) & 0x07FF; // Two's complement in 11 bits
+ }
+
+ int result = 0;
+ result |= (sign ? 0x8000 : 0x0000); // Bit 15: sign
+ result |= (exponent & 0x0F) << 11; // Bits 14-11: exponent
+ result |= (mantissa & 0x07FF); // Bits 10-0: mantissa/fraction
+
+ return new byte[]{
+ (byte) ((result >> 8) & 0xFF),
+ (byte) (result & 0xFF)
+ };
+ }
+
+ @Override
+ public float decodeFloat(int numBits, byte[] bytes) {
+ if (numBits != 16) {
+ throw new IllegalArgumentException("Only 16 bit encoding supported");
+ }
+ if (bytes == null) {
+ throw new IllegalArgumentException("bytes cannot be null");
+ }
+ if (bytes.length != 2) {
+ throw new IllegalArgumentException("Invalid number of bytes");
+ }
+
+ final boolean sign = (bytes[0] & (byte) 0x80) != 0;
+ final byte exponent = (byte) ((bytes[0] & (byte) 0x78) >> 3);
+ short fraction = (short) ((short) ((bytes[0] & (byte) 0x07) << 8) | (short) (bytes[1] & 0xFF));
+ if (sign) {
+ fraction = (short) (fraction | 0xF800);
+ }
+ return (float) (0.01 * fraction * Math.pow(2, exponent));
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingManager.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingManager.java
new file mode 100644
index 00000000000..60cabc5ed24
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingManager.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ServiceLoader;
+
+public class EncodingManager {
+
+ private final Map encodingMap;
+
+ public EncodingManager() {
+ this(Thread.currentThread().getContextClassLoader());
+ }
+
+ public EncodingManager(ClassLoader classLoader) {
+ this.encodingMap = new HashMap<>();
+ ServiceLoader encodings = ServiceLoader.load(Encoding.class, classLoader);
+ for (Encoding encoding : encodings) {
+ if (encodingMap.containsKey(encoding.getName())) {
+ throw new IllegalStateException(
+ "Multiple encoding implementations available for encoding name '" +
+ encoding.getName() + "'");
+ }
+ encodingMap.put(encoding.getName(), encoding);
+ }
+ }
+
+ public Optional getEncoding(String encodingName) {
+ Encoding encoding = encodingMap.get(encodingName);
+ return Optional.ofNullable(encoding);
+ }
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingRaw.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingRaw.java
new file mode 100644
index 00000000000..ab399eb27d7
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingRaw.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.exceptions.BufferException;
+import org.apache.plc4x.java.spi.buffers.bytebased.ReadBufferRaw;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * Interface for the decoding part of raw encodings.
+ */
+public interface EncodingRaw extends Encoding {
+
+ byte decodeByte(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ short decodeShort(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ int decodeInt(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ long decodeLong(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ BigInteger decodeBigInteger(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ float decodeFloat(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ double decodeDouble(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ BigDecimal decodeBigDecimal(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+ String decodeString(int numBits, ReadBufferRaw readBuffer) throws BufferException;
+
+}
diff --git a/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingTwosComplement.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingTwosComplement.java
new file mode 100644
index 00000000000..e3ac6cc5f67
--- /dev/null
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingTwosComplement.java
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
+
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
+
+import java.math.BigInteger;
+
+public class EncodingTwosComplement extends BaseEncodingDefault {
+
+ public static final String NAME = "twos-complement";
+
+ public static WithOption optionEncodingTwosComplement() {
+ return WithOption.WithEncoding(NAME);
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public byte[] encodeByte(int numBits, byte value) {
+ if (numBits <= 0 || numBits > 8) {
+ throw new IllegalArgumentException("numBits must be between 1 and 8");
+ }
+
+ // Add value range validation
+ int minValue = -(1 << (numBits - 1));
+ int maxValue = (1 << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d bits (range: %d to %d)",
+ value, numBits, minValue, maxValue));
+ }
+
+ // Mask out the bits we want to encode
+ int masked = value & ((1 << numBits) - 1);
+
+ // Align into high bits of one byte (big-endian bit packing, left-aligned)
+ int shift = 0;//8 - numBits;
+ byte encoded = (byte) (masked << shift);
+
+ return new byte[]{encoded};
+ }
+
+ @Override
+ public byte decodeByte(int numBits, byte[] bytes) {
+ if (numBits <= 0 || numBits > 8) {
+ throw new IllegalArgumentException("numBits must be between 1 and 8");
+ }
+ if (bytes.length < 1) {
+ throw new IllegalArgumentException("At least 1 byte is required");
+ }
+
+ int shift = 0;//8 - numBits;
+ int raw = (bytes[0] & 0xFF) >>> shift;
+
+ // Sign-extend to 8 bits if highest bit of encoded value is 1
+ if ((raw & (1 << (numBits - 1))) != 0) {
+ raw |= -(1 << numBits); // fill high bits with 1s
+ }
+
+ return (byte) raw;
+ }
+
+ @Override
+ public byte[] encodeShort(int numBits, short value) {
+ if (numBits <= 0 || numBits > 16) {
+ throw new IllegalArgumentException("numBits must be between 1 and 16");
+ }
+
+ // Add value range validation
+ int minValue = -(1 << (numBits - 1));
+ int maxValue = (1 << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d bits (range: %d to %d)",
+ value, numBits, minValue, maxValue));
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ int shift = 0;//numBytes * 8 - numBits;
+
+ // Mask only the lower numBits (handle negative values via two's complement)
+ int masked = value & ((1 << numBits) - 1);
+
+ // Align to the most significant bits
+ masked <<= shift;
+
+ byte[] result = new byte[numBytes];
+ for (int i = 0; i < numBytes; i++) {
+ result[i] = (byte) ((masked >> ((numBytes - 1 - i) * 8)) & 0xFF);
+ }
+
+ return result;
+ }
+
+ @Override
+ public short decodeShort(int numBits, byte[] bytes) {
+ if (numBits <= 0 || numBits > 16) {
+ throw new IllegalArgumentException("numBits must be between 1 and 16");
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ if (bytes.length < numBytes) {
+ throw new IllegalArgumentException("Expected at least " + numBytes + " bytes");
+ }
+
+ int shift = 0;//numBytes * 8 - numBits;
+
+ // Assemble bits from bytes
+ int raw = 0;
+ for (int i = 0; i < numBytes; i++) {
+ raw = (raw << 8) | (bytes[i] & 0xFF);
+ }
+
+ // Right-align the value
+ raw >>>= shift;
+
+ // Sign-extend if necessary
+ if ((raw & (1 << (numBits - 1))) != 0) {
+ raw |= -(1 << numBits);
+ }
+
+ return (short) raw;
+ }
+
+ @Override
+ public byte[] encodeInt(int numBits, int value) {
+ if (numBits <= 0 || numBits > 32) {
+ throw new IllegalArgumentException("numBits must be between 1 and 32");
+ }
+
+ // Add value range validation
+ long minValue = -(1L << (numBits - 1));
+ long maxValue = (1L << (numBits - 1)) - 1;
+ if (value < minValue || value > maxValue) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d bits (range: %d to %d)",
+ value, numBits, minValue, maxValue));
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ int shift = 0;//numBytes * 8 - numBits;
+
+ // Mask value to numBits bits
+ byte[] result = new byte[numBytes];
+ if (numBits != 32) {
+ int masked = value & ((1 << numBits) - 1);
+ masked <<= shift;
+
+ for (int i = 0; i < numBytes; i++) {
+ result[i] = (byte) ((masked >>> ((numBytes - 1 - i) * 8)) & 0xFF);
+ }
+ } else {
+ result[0] = (byte) ((value >>> 24) & 0xFF);
+ result[1] = (byte) ((value >>> 16) & 0xFF);
+ result[2] = (byte) ((value >>> 8) & 0xFF);
+ result[3] = (byte) (value & 0xFF);
+ }
+
+ return result;
+ }
+
+ @Override
+ public int decodeInt(int numBits, byte[] bytes) {
+ if (numBits <= 0 || numBits > 32) {
+ throw new IllegalArgumentException("numBits must be between 1 and 32");
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ if (bytes.length < numBytes) {
+ throw new IllegalArgumentException("Expected at least " + numBytes + " bytes");
+ }
+
+ int shift = 0;//numBytes * 8 - numBits;
+
+ // Read bits from bytes
+ int raw = 0;
+ for (int i = 0; i < numBytes; i++) {
+ raw = (raw << 8) | (bytes[i] & 0xFF);
+ }
+
+ raw >>>= shift;
+
+ // Sign-extend if negative
+ if ((raw & (1L << (numBits - 1))) != 0) {
+ long longRaw = raw - (1L << numBits);
+ raw = (int) longRaw;
+ }
+
+ return raw;
+ }
+
+ @Override
+ public byte[] encodeLong(int numBits, long value) {
+ if (numBits <= 0 || numBits > 64) {
+ throw new IllegalArgumentException("numBits must be between 1 and 64");
+ }
+
+ // Add value range validation
+ BigInteger bigValue = BigInteger.valueOf(value);
+ BigInteger minValue = BigInteger.ONE.shiftLeft(numBits - 1).negate();
+ BigInteger maxValue = BigInteger.ONE.shiftLeft(numBits - 1).subtract(BigInteger.ONE);
+ if (bigValue.compareTo(minValue) < 0 || bigValue.compareTo(maxValue) > 0) {
+ throw new IllegalArgumentException(
+ String.format("Value %d cannot be encoded in %d bits (range: %s to %s)",
+ value, numBits, minValue.toString(), maxValue.toString()));
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ int shift = 0;//numBytes * 8 - numBits;
+
+ byte[] result = new byte[numBytes];
+
+ if (numBits != 64) {
+ // Mask to lowest numBits bits
+ long masked = value & ((1L << numBits) - 1);
+ masked <<= shift;
+
+ for (int i = 0; i < numBytes; i++) {
+ result[i] = (byte) ((masked >>> ((numBytes - 1 - i) * 8)) & 0xFF);
+ }
+ } else {
+ result[0] = (byte) ((value >>> 56) & 0xFF);
+ result[1] = (byte) ((value >>> 48) & 0xFF);
+ result[2] = (byte) ((value >>> 40) & 0xFF);
+ result[3] = (byte) ((value >>> 32) & 0xFF);
+ result[4] = (byte) ((value >>> 24) & 0xFF);
+ result[5] = (byte) ((value >>> 16) & 0xFF);
+ result[6] = (byte) ((value >>> 8) & 0xFF);
+ result[7] = (byte) (value & 0xFF);
+ }
+
+ return result;
+ }
+
+ @Override
+ public long decodeLong(int numBits, byte[] bytes) {
+ if (numBits <= 0 || numBits > 64) {
+ throw new IllegalArgumentException("numBits must be between 1 and 64");
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ if (bytes.length < numBytes) {
+ throw new IllegalArgumentException("Expected at least " + numBytes + " bytes");
+ }
+
+ int shift = 0;//numBytes * 8 - numBits;
+
+ long raw = 0;
+ for (int i = 0; i < numBytes; i++) {
+ raw = (raw << 8) | (bytes[i] & 0xFF);
+ }
+
+ raw >>>= shift;
+
+ // Sign extend if needed
+ BigInteger bigValue = BigInteger.valueOf(raw);
+ if (bigValue.and(BigInteger.ONE.shiftLeft(numBits - 1)).compareTo(BigInteger.ZERO) != 0) {
+ bigValue = bigValue.or(BigInteger.ONE.negate().shiftLeft(numBits));
+ raw = bigValue.longValue();
+ }
+
+ return raw;
+ }
+
+ @Override
+ public byte[] encodeBigInteger(int numBits, BigInteger value) {
+ if (numBits <= 0) {
+ throw new IllegalArgumentException("numBits must be > 0");
+ }
+
+ // Add value range validation
+ BigInteger minValue = BigInteger.ONE.shiftLeft(numBits - 1).negate();
+ BigInteger maxValue = BigInteger.ONE.shiftLeft(numBits - 1).subtract(BigInteger.ONE);
+ if (value.compareTo(minValue) < 0 || value.compareTo(maxValue) > 0) {
+ throw new IllegalArgumentException(
+ String.format("Value %s cannot be encoded in %d bits (range: %s to %s)",
+ value.toString(), numBits, minValue.toString(), maxValue.toString()));
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ int shift = 0;//numBytes * 8 - numBits;
+
+ // Modulo 2^numBits to get two's complement representation
+ BigInteger mod = BigInteger.ONE.shiftLeft(numBits);
+ BigInteger normalized = value.and(mod.subtract(BigInteger.ONE));
+
+ // Shift left to align with MSB
+ normalized = normalized.shiftLeft(shift);
+
+ byte[] tmp = normalized.toByteArray();
+
+ // Ensure the result is exactly numBytes long (may require trimming or padding)
+ byte[] result = new byte[numBytes];
+ int copyFrom = Math.max(0, tmp.length - numBytes);
+ int copyTo = Math.max(0, result.length - tmp.length);
+ System.arraycopy(tmp, copyFrom, result, copyTo, tmp.length - copyFrom);
+
+ return result;
+ }
+
+ @Override
+ public BigInteger decodeBigInteger(int numBits, byte[] bytes) {
+ if (numBits <= 0) {
+ throw new IllegalArgumentException("numBits must be > 0");
+ }
+
+ int numBytes = (numBits + 7) / 8;
+ if (bytes.length < numBytes) {
+ throw new IllegalArgumentException("Expected at least " + numBytes + " bytes");
+ }
+
+ int shift = 0;//numBytes * 8 - numBits;
+
+ // Extract bits from the start of the array
+ BigInteger raw = new BigInteger(1, bytes).shiftRight(shift);
+
+ // Sign extend if needed
+ if (raw.testBit(numBits - 1)) {
+ // Negative number: extend with 1s above numBits
+ BigInteger signExtension = BigInteger.valueOf(-1).shiftLeft(numBits);
+ raw = raw.or(signExtension);
+ }
+
+ return raw;
+ }
+
+}
diff --git a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/metadata/DefaultOptionMetadata.java b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingUTF16.java
similarity index 58%
rename from plc4j/spi/src/main/java/org/apache/plc4x/java/spi/metadata/DefaultOptionMetadata.java
rename to plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingUTF16.java
index 81e2902eaa3..dee591786b0 100644
--- a/plc4j/spi/src/main/java/org/apache/plc4x/java/spi/metadata/DefaultOptionMetadata.java
+++ b/plc4j/spi/buffers/byte/src/main/java/org/apache/plc4x/java/spi/buffers/bytebased/encoding/EncodingUTF16.java
@@ -16,31 +16,34 @@
* specific language governing permissions and limitations
* under the License.
*/
+package org.apache.plc4x.java.spi.buffers.bytebased.encoding;
-package org.apache.plc4x.java.spi.metadata;
+import org.apache.plc4x.java.spi.buffers.api.WithOption;
-import org.apache.plc4x.java.api.metadata.Option;
-import org.apache.plc4x.java.api.metadata.OptionMetadata;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.stream.Collectors;
+public class EncodingUTF16 extends BaseStringEncoding {
-public class DefaultOptionMetadata implements OptionMetadata {
+ public static final String NAME = "UTF16";
- final List