diff --git a/build.gradle b/build.gradle index c754a29..8330c48 100644 --- a/build.gradle +++ b/build.gradle @@ -10,8 +10,10 @@ allprojects { subprojects { apply plugin: 'java' - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + java { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } test { useJUnitPlatform() diff --git a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java index 1331bf2..6904ab0 100644 --- a/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/haus/nightmare/j3270/ui/TerminalPanel.java @@ -163,10 +163,20 @@ public class TerminalPanel extends JPanel { // IMPORTANT ARCHITECTURE NOTE: // 1. In GDDM Graphic Cursor Mode (3179G / GOCA), mouse events represent graphic light-pen touches. // Text drag-selection is explicitly suppressed so blue selection boxes do not artifact over graphics. - // 2. In Text Light Pen mode (Alt+L on 3270 formatted screens), clicks toggle selectable fields. + // 2. In Text Light Pen / Selectable Field mode (per IBM Host On-Demand MouseMgr.java / PS3270.java), + // clicks on detectable fields (faIsSelectable) trigger field selection or cursor select. // 3. In standard alphanumeric mode, click-drag selects text for copy-paste. boolean isGraphic = client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive(); - if (isGraphic || lightPenMode) { + boolean isSelectableField = false; + if (sb.isFormatted()) { + int faPos = sb.findFieldAttribute(row * displayCols + col); + if (faPos >= 0) { + int fa = sb.getCell(faPos).fa & 0xFF; + isSelectableField = haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) && + !haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa); + } + } + if (isGraphic || lightPenMode || isSelectableField) { clearSelection(); } else { selectionStartRow = row; @@ -277,6 +287,25 @@ public class TerminalPanel extends JPanel { return; } + // Automatic Light-Pen / Selectable Field detection on mouse click: + // Per IBM Host On-Demand (MouseMgr.java / PS3270.java) and 3270 specifications: + // If the clicked screen location belongs to a detectable field (faIsSelectable and not faIsZero), + // automatically process light-pen / cursor selection (e.g. immediate selection, Enter, or toggling ? -> >). + if (sb.isFormatted()) { + int faPos = sb.findFieldAttribute(clickAddr); + if (faPos >= 0) { + int fa = sb.getCell(faPos).fa & 0xFF; + if (haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) && + !haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) { + clearSelection(); + if (client.lightPenSelect(clickAddr)) { + refreshScreen(); + return; + } + } + } + } + if (lightPenMode) { clearSelection(); sb.setCursorAddress(clickAddr); @@ -947,8 +976,8 @@ public class TerminalPanel extends JPanel { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); - int fontSize = (int) Math.round(ch * 0.95); - if (fontSize < 10) fontSize = 10; + int fontSize = Math.min((int) Math.round(ch * 0.82), (int) Math.round(cw * 1.45)); + if (fontSize < 9) fontSize = 9; Font f = (boldTerminalFont != null ? boldTerminalFont : terminalFont).deriveFont((float) fontSize); g2.setFont(f); FontMetrics fm = g2.getFontMetrics(); @@ -1154,10 +1183,11 @@ public class TerminalPanel extends JPanel { Font f = bold ? boldTerminalFont : terminalFont; g2.setFont(f); g2.setColor(fgColor); + int textY = y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2); if (ch < 128) { - g2.drawString(CHAR_STRINGS[ch], x, y + fontAscent); + g2.drawString(CHAR_STRINGS[ch], x, textY); } else { - g2.drawString(String.valueOf(ch), x, y + fontAscent); + g2.drawString(String.valueOf(ch), x, textY); } } } @@ -1165,8 +1195,8 @@ public class TerminalPanel extends JPanel { // Draw underline if (underline) { g2.setColor(fgColor); - g2.drawLine(x, y + cellHeight - fontDescent, - x + cellWidth - 1, y + cellHeight - fontDescent); + int ulY = Math.min(y + cellHeight - 1, y + fontAscent + Math.max(0, (cellHeight - (fontAscent + fontDescent)) / 2) + 2); + g2.drawLine(x, ulY, x + cellWidth - 1, ulY); } // Draw selection highlight diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 7ab5b84..4a7343f 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -41,7 +41,7 @@ public class DataStreamProcessor { // Graphics & Programmed Symbols private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager(); - private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600); + private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384); private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane); private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream(); private int currentGocaSubtype = 0; @@ -853,6 +853,11 @@ public class DataStreamProcessor { sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim())); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); + int targetW = screen.getCols() * 9; + int targetH = screen.getRows() * 16; + if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) { + graphicsPlane.resize(targetW, targetH); + } if (flags == 0x80) { // SPAN_FIRST gocaAccumulator.reset(); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java index d2f2012..ffafae7 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java @@ -293,14 +293,36 @@ public class QueryReplyBuilder { out.write((Yr_3279_2 >> 16) & 0xFF); out.write((Yr_3279_2 >> 8) & 0xFF); out.write(Yr_3279_2 & 0xFF); - out.write(SW_3279_2); // AW - out.write(SH_3279_2); // AH + int charW = getCharWidth(); + int charH = getCharHeight(); + out.write(charW); // AW + out.write(charH); // AH int buf = maxCols * maxRows; out.write((buf >> 8) & 0xFF); // buffer size high out.write(buf & 0xFF); // buffer size low return out.toByteArray(); } + public int getCharWidth() { + return SW_3279_2; // 9 + } + + public int getCharHeight() { + // ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES: + // Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations: + // When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes + // total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257). + // GDDM then places the top menu bar at Row 1 (gy = 187..200). + // Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343). + // On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above. + // When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9, + // which GDDM rejected as outside its menu hit box (causing terminal alarm beeps). + // + // Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM + // and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343). + return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; + } + private byte[] buildAlphaPartitions(int maxRows) { int bufSize = screen.getMaxCols() * screen.getMaxRows(); ByteArrayOutputStream out = new ByteArrayOutputStream(4); @@ -312,13 +334,16 @@ public class QueryReplyBuilder { } private byte[] buildCharsets() { + int charW = getCharWidth(); + int charH = getCharHeight(); + if (graphicsMode.isProgrammedSymbolsEnabled()) { // Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID) ByteArrayOutputStream out = new ByteArrayOutputStream(65); out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02) out.write(0x00); // more flags - out.write(SW_3279_2); // SDW (9) - out.write(SH_3279_2); // SDH (12) + out.write(charW); // SDW (9) + out.write(charH); // SDH (16 for 3179G, 12 for 3279-2) out.write(0x0a); // Load PS format types supported: Format 1 and Format 3 out.write(0x00); // Load PS device type (high) out.write(0x00); // Load PS device type (low) @@ -343,8 +368,8 @@ public class QueryReplyBuilder { ByteArrayOutputStream out = new ByteArrayOutputStream(23); out.write(0x82); // flags: GE, CGCSGID present out.write(0x00); // more flags - out.write(SW_3279_2); // SDW - default char width (9) - out.write(SH_3279_2); // SDH - default char height (12) + out.write(charW); // SDW - default char width (9) + out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2) out.write(0x00); // LoadPS format (0x00) out.write(0x00); out.write(0x00); diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java index 31cea87..4522951 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicInputBuilder.java @@ -6,9 +6,14 @@ package haus.nightmare.lib3270j.graphics; */ public class GraphicInputBuilder { - // 56-byte template mask from IBM Host On-Demand (HODInput.java) + // 56-byte template mask from IBM Host On-Demand (HODInput.java). + // Note on Structured Field Length (bytes 0-1): + // IBM HOD sets bytes 0-1 to 0x00 0x34 (52 decimal). In 3270 GOCA architecture, + // the Data Unit Object Control payload is 52 bytes. Overwriting this with 0x00 0x38 (56) + // causes the host's 3270 inbound structured field decoder to misalign the trailing AID byte + // (0x7D) and cursor address by 4 bytes, causing host GDDM to reject the click with an alarm beep. private static final byte[] MASK = new byte[] { - 0x00, 0x38, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40, + 0x00, 0x34, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, @@ -60,9 +65,8 @@ public class GraphicInputBuilder { byte[] sf = new byte[MASK.length]; System.arraycopy(MASK, 0, sf, 0, MASK.length); - // Byte 0-1: Structured Field Length (56 bytes) - sf[0] = (byte) ((MASK.length >> 8) & 0xFF); - sf[1] = (byte) (MASK.length & 0xFF); + // Bytes 0-1: Structured Field Length (0x0034 = 52 decimal per IBM HODInput.java / GOCA architecture). + // Preserved from MASK; do not overwrite with MASK.length (56). // Bytes 16-19: Device correlation class descriptor (0x23, 0x00, 0x23, 0x00) // Note: Preserved from MASK per IBM Host On-Demand (HODInput.java); do not overwrite with row/col. diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java index 2d2d74f..1b576dd 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -36,14 +36,14 @@ public class GraphicsPlane { {-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid }; - private int canvasWidth = 800; - private int canvasHeight = 600; + // 3179G Presentation Space metrics: 9x16 cell pitch (720x384 for Model 2, 720x688 for Model 4) + private int screenCols = 80; + private int screenRows = 24; + private int canvasWidth = 720; + private int canvasHeight = 384; private int[] rgbBuffer; private boolean hasContent = false; private long updateCount = 0; - - private int screenCols = 80; - private int screenRows = 24; private ProgramSymbolManager programSymbolManager; public void setProgramSymbolManager(ProgramSymbolManager psm) { @@ -595,20 +595,19 @@ public class GraphicsPlane { int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0]; int bg = bgColorArgb; - // Note / Logic for Future Iterations: - // In IBM 3179G / GDDM architecture, solid black fills (fillColor = GOCA_COLORS[8] / 0xFF000000, - // pattern = PT_SOLID / 16) are the standard mechanism used by host applications to ERASE - // dropdown menus, dialog boxes, and dynamic regions from the screen. - // - // A prior failed attempt added an 'isTransparentBlack' guard: - // boolean isTransparentBlack = (fill == GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != MIX_OVER); - // if (... && !isTransparentBlack) - // This caused GDDM's menu erasure rectangles (which use fillColor = Black and bgMix = 5) to be - // silently skipped and discarded. As a result, closed dropdown menus were never erased, causing - // multiple menus (FILE, DRAW, TRANSFORM) to linger and stack on top of each other permanently. - // - // Solid black fills (0xFF000000) must always be rasterized to overwrite and erase previously drawn pixels. - if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { + // ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING: + // In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black). + // Background Mix (GSBMX / bgMix): + // - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent). + // Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels. + // (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0). + // - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque). + // Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs + // (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill). + boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) && + (bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0); + + if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) { // Find polygon vertical bounds across all points int minY = py[0]; int maxY = py[0]; diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java index 155d264..d46b4cf 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/input/InputProcessor.java @@ -244,7 +244,43 @@ public class InputProcessor { return; } - // Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data + if (aidCode == AID_SELECT) { + // 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E): + // Per IBM 3270 Data Stream Architecture (GA23-0059) and IBM Host On-Demand (DS3270.sendAid lines 727-1019): + // The inbound data stream consists of: + // 1. AID byte (0x7E) + // 2. Cursor address (2 bytes) + // 3. For each field with MDT=1: + // - SBA order (0x11) + // - Designator character address (faAddr + 1) + // CRITICAL: NO FIELD CHARACTER DATA IS TRANSMITTED FOR AID_SELECT! + // Sending character data in an AID_SELECT stream violates 3270 protocol and causes the host to reject the selection. + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(AID_SELECT); + + byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + out.write(caddr[0] & 0xFF); + out.write(caddr[1] & 0xFF); + + if (screen.isFormatted()) { + int size = screen.getRows() * screen.getCols(); + for (int i = 0; i < size; i++) { + ExtendedAttribute ea = screen.getCell(i); + if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) { + int designatorAddr = (i + 1) % size; + out.write(ORDER_SBA); + byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols()); + out.write(addr[0] & 0xFF); + out.write(addr[1] & 0xFF); + } + } + } + + sendAidResponse(out.toByteArray()); + return; + } + + // Enter, PF keys: send AID + optional PID + cursor address + modified field data ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write(aidCode); diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java index 14456ab..e451f0a 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilderTest.java @@ -132,11 +132,15 @@ public class QueryReplyBuilderTest { assertEquals(0x81, replies[3] & 0xFF); assertEquals(QR_DDM, replies[4] & 0xFF); - // Second SF should be Usable Area + // Second SF should be Usable Area: 23 bytes (4 bytes header + 19 bytes payload) int pos2 = 1 + len1; int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF); + assertEquals(23, len2); assertEquals(0x81, replies[pos2 + 2] & 0xFF); assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF); + // Offset 0x15 (21 in SF payload = pos2 + 21): Buffer size high + int bufSize = ((replies[pos2 + 21] & 0xFF) << 8) | (replies[pos2 + 22] & 0xFF); + assertEquals(80 * 43, bufSize); } @Test diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java index 5a7b082..6320d06 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/input/InputProcessorTest.java @@ -138,7 +138,7 @@ public class InputProcessorTest { byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false); assertEquals(56, sf.length); assertEquals(0x00, sf[0]); - assertEquals(0x38, sf[1]); // Length = 56 bytes (0x0038) + assertEquals(0x34, sf[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java assertEquals(0x0F, sf[2]); // SF ID = 0x0F assertEquals(0x0F, sf[3]); // SF ID = 0x0F @@ -160,7 +160,7 @@ public class InputProcessorTest { ); assertEquals(56, sfMouse.length); assertEquals(0x00, sfMouse[0]); - assertEquals(0x38, sfMouse[1]); + assertEquals(0x34, sfMouse[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java assertEquals(0x23, sfMouse[16]); assertEquals(0x00, sfMouse[17]); assertEquals(0x23, sfMouse[18]); @@ -394,9 +394,9 @@ public class InputProcessorTest { // 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes assertEquals(64, result.length); assertEquals((byte) AID_SF, result[0]); - // SF length = 56 (0x00 0x38) + // SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification assertEquals(0x00, result[1]); - assertEquals(0x38, result[2]); + assertEquals(0x34, result[2]); // SF ID = 0x0F0F assertEquals(0x0F, result[3]); assertEquals(0x0F, result[4]); @@ -418,4 +418,37 @@ public class InputProcessorTest { // Trailing field content 'A' at 63 assertEquals((byte) 0xC1, result[63]); } + + @Test + public void testAidSelectFramingOmitsFieldData() { + // Formatted screen with an unprotected field at 0, modified, containing 'A' and 'B' + screen.erase(false); + screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY)); + screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT)); + screen.getCell(1).ec = (byte) 0xC1; + screen.getCell(1).ucs4 = 'A'; + screen.getCell(2).ec = (byte) 0xC2; + screen.getCell(2).ucs4 = 'B'; + screen.setCursorAddress(1); + + java.util.concurrent.atomic.AtomicReference sent = new java.util.concurrent.atomic.AtomicReference<>(); + InputProcessor input = new InputProcessor(screen, translator, null) { + @Override + protected void sendAidResponse(byte[] data) { + sent.set(data); + } + }; + + input.sendAid(AID_SELECT); + + byte[] result = sent.get(); + assertNotNull(result); + // For AID_SELECT (0x7E), the stream consists ONLY of: + // 1 byte AID (0x7E) + 2 bytes cursor address + 1 byte SBA (0x11) + 2 bytes designator address = 6 bytes total. + // Field character contents ('A', 'B') MUST NOT be sent per IBM 3270 DS architecture! + assertEquals(6, result.length); + assertEquals((byte) AID_SELECT, result[0]); + // SBA order at byte 3 + assertEquals((byte) ORDER_SBA, result[3]); + } }