From 0837de8db3da0b62b0f3cd686d43896cdd454890 Mon Sep 17 00:00:00 2001 From: Rudi Date: Sun, 23 Aug 2026 01:27:45 +0000 Subject: [PATCH] ADMOPSLA and ADMCHART are happier --- .../org/pubvm/j3270/ui/TerminalPanel.java | 88 +++++-- .../java/org/lib3270j/DiagnosticClient.java | 2 +- .../java/org/lib3270j/Telnet3270Client.java | 1 + .../datastream/DataStreamProcessor.java | 39 +++- .../datastream/QueryReplyBuilder.java | 2 +- .../org/lib3270j/graphics/GocaConstants.java | 4 + .../org/lib3270j/graphics/GocaDecoder.java | 220 +++++++++++++++--- .../graphics/GraphicInputBuilder.java | 59 +++++ .../org/lib3270j/graphics/GraphicsPlane.java | 50 +++- .../graphics/ProgramSymbolManager.java | 22 +- .../org/lib3270j/input/InputProcessor.java | 100 +++++--- .../lib3270j/protocol/TelnetConstants.java | 1 + .../org/lib3270j/screen/ScreenBuffer.java | 23 ++ .../java/org/lib3270j/telnet/TelnetFSM.java | 100 +++++++- .../datastream/QueryReplyBuilderTest.java | 26 +++ .../lib3270j/graphics/GocaDecoderTest.java | 70 +++++- .../lib3270j/input/InputProcessorTest.java | 22 ++ .../org/lib3270j/telnet/TelnetFSMTest.java | 75 ++++++ .../java/org/lib3270j/tls/TlsConfigTest.java | 2 +- 19 files changed, 792 insertions(+), 114 deletions(-) create mode 100644 lib3270j/src/main/java/org/lib3270j/graphics/GraphicInputBuilder.java diff --git a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java index 95f861b..ccce71c 100644 --- a/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java +++ b/j3270/src/main/java/org/pubvm/j3270/ui/TerminalPanel.java @@ -180,6 +180,22 @@ public class TerminalPanel extends JPanel { } } + @Override + public void mouseMoved(MouseEvent e) { + if (client != null && client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) { + int ox = getRenderOffsetX(); + int oy = getRenderOffsetY(); + ScreenBuffer sb = client.getScreenBuffer(); + int gridW = sb.getDisplayCols() * cellWidth; + int gridH = sb.getDisplayRows() * cellHeight; + int gWidth = client.getGraphicsPlane().getCanvasWidth(); + int gHeight = client.getGraphicsPlane().getCanvasHeight(); + int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox); + int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy); + client.getGocaDecoder().setGraphicCursorFromPixel(px, py); + } + } + @Override public void mouseReleased(MouseEvent e) { if (isDragging) { @@ -188,8 +204,35 @@ public class TerminalPanel extends JPanel { if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) { if (client != null && client.getConnectionState().isFullSession()) { ScreenBuffer sb = client.getScreenBuffer(); - sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol); + int newAddr = selectionStartRow * sb.getCols() + selectionStartCol; + sb.setCursorAddress(newAddr); clearSelection(); + + if (client.getGocaDecoder() != null && client.getGraphicsPlane() != null) { + int ox = getRenderOffsetX(); + int oy = getRenderOffsetY(); + int gridW = sb.getDisplayCols() * cellWidth; + int gridH = sb.getDisplayRows() * cellHeight; + int gWidth = client.getGraphicsPlane().getCanvasWidth(); + int gHeight = client.getGraphicsPlane().getCanvasHeight(); + int px = (gridW > 0 && gWidth > 0) ? (int) Math.round((double) (e.getX() - ox) * gWidth / gridW) : (e.getX() - ox); + int py = (gridH > 0 && gHeight > 0) ? (int) Math.round((double) (e.getY() - oy) * gHeight / gridH) : (e.getY() - oy); + client.getGocaDecoder().setGraphicCursorFromPixel(px, py); + + if (client.getGocaDecoder().isGraphicsCursorActive()) { + // Light-pen / Graphic cursor touch event (matches IBM Host On-Demand PS3179G) + int button = javax.swing.SwingUtilities.isRightMouseButton(e) ? 2 : 1; + client.getInputProcessor().sendGraphicMouseAid( + org.lib3270j.protocol.DS3270Constants.AID_ENTER, + button, + e.isShiftDown(), + e.isControlDown() + ); + refreshScreen(); + return; + } + } + refreshScreen(); return; } @@ -849,6 +892,20 @@ public class TerminalPanel extends JPanel { int cols = sb.getDisplayCols(); boolean isColorModel = client.getConfig().getModel().isColor(); + // Draw Vector Graphics Plane under text if present + if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) { + int gridW = cols * cellWidth; + int gridH = rows * cellHeight; + int gWidth = client.getGraphicsPlane().getCanvasWidth(); + int gHeight = client.getGraphicsPlane().getCanvasHeight(); + int[] rgb = client.getGraphicsPlane().getRgbBuffer(); + if (rgb != null && gWidth > 0 && gHeight > 0) { + java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB); + img.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth); + g2.drawImage(img, ox, oy, gridW, gridH, null); + } + } + // Track current field attribute for monochrome color decisions byte currentFA = 0; ExtendedAttribute currentFieldEa = null; @@ -940,7 +997,8 @@ public class TerminalPanel extends JPanel { if (cs >= 0x40 && client.getProgramSymbolManager() != null) { org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); if (slot != null) { - java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), bgColor.getRGB()); + int symBg = (!bgColor.equals(this.bgColor) || reverse) ? bgColor.getRGB() : 0; + java.awt.image.BufferedImage img = slot.getScaledImage(cellWidth, cellHeight, fgColor.getRGB(), symBg); if (img != null) { g2.drawImage(img, x, y, null); drawnAsPs = true; @@ -977,20 +1035,22 @@ public class TerminalPanel extends JPanel { } } - // Draw Vector Graphics Plane overlay if present - if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) { - int gridW = cols * cellWidth; - int gridH = rows * cellHeight; - client.getGraphicsPlane().resize(gridW, gridH); - int[] rgb = client.getGraphicsPlane().getRgbBuffer(); - if (rgb != null) { - java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB); - img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW); - g2.drawImage(img, ox, oy, gridW, gridH, null); - } + // Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active + if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) { + int gocaX = client.getGocaDecoder().getGraphicCursorX(); + int gocaY = client.getGocaDecoder().getGraphicCursorY(); + int px = ox + client.getGraphicsPlane().mapX(gocaX); + int py = oy + client.getGraphicsPlane().mapY(gocaY); + + g2.setColor(Color.WHITE); + g2.setXORMode(Color.BLACK); + // Draw a crosshair cursor for the light-pen / graphic cursor + g2.drawLine(px - 6, py, px + 6, py); + g2.drawLine(px, py - 6, px, py + 6); + g2.setPaintMode(); } - // Draw cursor + // Draw 3270 text cursor if (cursorVisible && client.getConnectionState().isFullSession()) { int curAddr = sb.getDisplayCursorAddress(); int curRow = curAddr / cols; diff --git a/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java b/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java index 618b9d9..4e4f550 100644 --- a/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java +++ b/lib3270j/src/main/java/org/lib3270j/DiagnosticClient.java @@ -26,7 +26,7 @@ public class DiagnosticClient { String host = args.length >= 1 ? args[0] : "192.168.0.30"; int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270; - ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_2); + ConnectionConfig config = new ConnectionConfig(host, port, TerminalModel.IBM_3279_4); client = new Telnet3270Client(config); client.addConnectionListener(new ConnectionListener() { diff --git a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java index def3f0c..7fa0579 100644 --- a/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java +++ b/lib3270j/src/main/java/org/lib3270j/Telnet3270Client.java @@ -54,6 +54,7 @@ public class Telnet3270Client { dsProcessor.setOutputSender(fsm::send3270Data); dsProcessor.setInputProcessor(inputProcessor); inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane()); + inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder()); } /** diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java index 2596c4d..f29547c 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/DataStreamProcessor.java @@ -89,6 +89,10 @@ public class DataStreamProcessor { this.inputProcessor = inputProcessor; } + public org.lib3270j.input.InputProcessor getInputProcessor() { + return inputProcessor; + } + public void addScreenUpdateListener(ScreenUpdateListener l) { screenListeners.add(l); } @@ -175,6 +179,7 @@ public class DataStreamProcessor { case CMD_WSF: case SNA_CMD_WSF: processWriteStructuredField(data, offset, length); + keyboardRestore = true; break; case CMD_NOP: log.info(">>> NOP command"); @@ -190,6 +195,10 @@ public class DataStreamProcessor { screen.updateDisplaySnapshot(); } + if (keyboardRestore && inputProcessor != null) { + inputProcessor.setKeyboardLocked(false); + } + // Debug: dump non-empty screen lines if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) { int r = screen.getRows(); @@ -782,22 +791,27 @@ public class DataStreamProcessor { case SF_READ_PART: processSFReadPartition(data, pos, fieldLen); break; - case SF_ERASE_RESET: - if (fieldLen >= 4) { - boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT; - screen.erase(alt); - graphicsPlane.clear(); - notifyScreenSizeChanged(); - } + case SF_ERASE_RESET: { + boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT); + screen.erase(alt); + graphicsPlane.clear(); + gocaDecoder.resetDefaults(); + notifyScreenSizeChanged(); break; + } case SF_SET_REPLY_MODE: if (fieldLen >= 5) { screen.setReplyMode((byte) (data[pos + 4] & 0xFF)); } break; case SF_CREATE_PART: - // Acknowledged — we use implicit partition + if (fieldLen >= 4) { + int pid = data[pos + 3] & 0xFF; + screen.setActivePartition(pid); + log.fine("Created active partition ID=" + pid); + } graphicsPlane.clear(); + gocaDecoder.resetDefaults(); break; case SF_OUTBOUND_DS: if (fieldLen > 5) { @@ -879,6 +893,13 @@ public class DataStreamProcessor { } break; } + case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL: // 0x24: Object Control (Procedure orders) + if (fieldLen > 3) { + graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); + gocaDecoder.processProcedureOrders(data, pos + 3, fieldLen - 3); + notifyScreenUpdated(); + } + break; case org.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics if (fieldLen > 3) { @@ -915,6 +936,8 @@ public class DataStreamProcessor { switch (type) { case SF_RP_QUERY: log.info("ReadPartition Query — sending all query replies"); + graphicsPlane.clear(); + gocaDecoder.resetDefaults(); sendAllQueryReplies(); break; case SF_RP_QLIST: diff --git a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java index 0172521..8c0ab03 100644 --- a/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/org/lib3270j/datastream/QueryReplyBuilder.java @@ -197,7 +197,7 @@ public class QueryReplyBuilder { case QR_RPQ_NAMES: case QR_RPQNAMES: if (graphicsMode.isVectorGraphicsEnabled()) { - appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); + appendQueryReply(out, code, buildRpqNames()); } else { appendQueryReply(out, QR_NULL, new byte[0]); } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java index 2141736..97c9ec9 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaConstants.java @@ -54,6 +54,7 @@ public final class GocaConstants { public static final int G_GSVW = 0x27; // Set Viewing Window public static final int G_GSPT = 0x28; // Set Pattern Symbol public static final int G_GSMT = 0x29; // Set Marker Symbol / Type + public static final int G_GCALL = 0x2A; // Call Segment public static final int G_GSCH = 0x33; // Set Character Cell public static final int G_GSCA = 0x34; // Set Character Angle public static final int G_GSCR = 0x35; // Set Character Shear @@ -176,6 +177,9 @@ public final class GocaConstants { * Returns Green (0xFF00FF00) if the index is out of range. */ public static int getGocaColorArgb(int colorIndex) { + if (colorIndex == 0xFF) { + return GOCA_COLORS[7]; + } if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) { return GOCA_COLORS[colorIndex]; } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java index 72571be..fda679e 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GocaDecoder.java @@ -20,6 +20,8 @@ public class GocaDecoder { private int curX = 0; private int curY = 0; private int curColor = GocaConstants.GOCA_COLORS[0]; + private int bgMix = 2; // BMX_OVERPAINT default + private int bgColor = GocaConstants.GOCA_COLORS[8]; // Black private int lineType = GocaConstants.LT_SOLID; private int lineWidth = GocaConstants.LW_NORMAL; private int markerType = GocaConstants.MK_PLUS; @@ -42,6 +44,7 @@ public class GocaDecoder { // Area accumulation private boolean inArea = false; private boolean areaDrawBoundary = true; + private boolean areaFill = true; private final List areaPointsX = new ArrayList<>(); private final List areaPointsY = new ArrayList<>(); @@ -53,6 +56,18 @@ public class GocaDecoder { private int imgHeight = 0; private final List imgBuffer = new ArrayList<>(); + // Segment Store for retained graphics / segment calling (G_GCALL 0x2A) + private final java.util.Map segmentStore = new java.util.HashMap<>(); + private final java.util.Map segmentChainMap = new java.util.HashMap<>(); + private final java.util.List segmentOrderList = new java.util.ArrayList<>(); + private final java.util.Set chainedTargets = new java.util.HashSet<>(); + private int callDepth = 0; + + // Graphic Cursor (Light-Pen) state + private boolean graphicsCursorActive = false; + private int graphicCursorX = 0; + private int graphicCursorY = 0; + public GocaDecoder(GraphicsPlane plane) { this.plane = plane; } @@ -73,14 +88,51 @@ public class GocaDecoder { return curY; } + public synchronized boolean isGraphicsCursorActive() { + return graphicsCursorActive; + } + + public synchronized void setGraphicsCursorActive(boolean active) { + this.graphicsCursorActive = active; + } + + public synchronized int getGraphicCursorX() { + return graphicCursorX; + } + + public synchronized int getGraphicCursorY() { + return graphicCursorY; + } + + public synchronized void setGraphicCursorPosition(int x, int y) { + this.graphicCursorX = x; + this.graphicCursorY = y; + } + + public synchronized void setGraphicCursorFromPixel(int px, int py) { + if (plane != null) { + this.graphicCursorX = plane.unmapX(px); + this.graphicCursorY = plane.unmapY(py); + } + } + public synchronized void resetDefaults() { curX = 0; curY = 0; + graphicsCursorActive = false; + graphicCursorX = 0; + graphicCursorY = 0; + segmentStore.clear(); + segmentChainMap.clear(); + segmentOrderList.clear(); + chainedTargets.clear(); resetAttributes(); } public synchronized void resetAttributes() { curColor = getColor(0); + bgMix = 2; // BMX_OVERPAINT + bgColor = GocaConstants.GOCA_COLORS[8]; // Black lineType = GocaConstants.LT_SOLID; lineWidth = GocaConstants.LW_NORMAL; markerType = GocaConstants.MK_PLUS; @@ -92,6 +144,8 @@ public class GocaDecoder { charAngle = 0.0; charSet = 0; inArea = false; + areaDrawBoundary = true; + areaFill = true; areaPointsX.clear(); areaPointsY.clear(); inImage = false; @@ -115,14 +169,14 @@ public class GocaDecoder { order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW || order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW || order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT || - order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS || - order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD || - order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET || - order == GocaConstants.G_GBAR) { + order == GocaConstants.G_GSMT || order == GocaConstants.G_GSMCEL || + order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMP || + order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCC || + order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) { return 2; } - if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) { - return 10; + if (order == GocaConstants.G_GCALL) { // Call Segment (0x2A <32-bit segment ID>) + return 5; } if (idx + 1 >= end) { return -1; @@ -130,6 +184,61 @@ public class GocaDecoder { return (data[idx + 1] & 0xFF) + 2; } + private void indexSegments(byte[] data, int offset, int length) { + int idx = offset; + int end = offset + length; + while (idx < end) { + int order = data[idx] & 0xFF; + if (order == GocaConstants.G_BEGSEGM) { + int segStart = idx; + int segLen = getOrderLength(data, idx, end); + if (segLen <= 0 || idx + 5 >= end) { + break; + } + int segId = ((data[idx + 2] & 0xFF) << 24) | + ((data[idx + 3] & 0xFF) << 16) | + ((data[idx + 4] & 0xFF) << 8) | + (data[idx + 5] & 0xFF); + + int nextId = 0; + if (segLen >= 14 && (data[idx + 1] & 0xFF) >= 12) { + nextId = ((data[idx + 10] & 0xFF) << 24) | + ((data[idx + 11] & 0xFF) << 16) | + ((data[idx + 12] & 0xFF) << 8) | + (data[idx + 13] & 0xFF); + } + + int searchIdx = idx + segLen; + while (searchIdx < end) { + int o = data[searchIdx] & 0xFF; + int oLen = getOrderLength(data, searchIdx, end); + if (oLen <= 0) break; + if (o == GocaConstants.G_ENDSEGM) { + searchIdx += oLen; + break; + } + searchIdx += oLen; + } + int fullSegLen = searchIdx - segStart; + if (fullSegLen > 0 && segStart + fullSegLen <= end) { + byte[] segBytes = new byte[fullSegLen]; + System.arraycopy(data, segStart, segBytes, 0, fullSegLen); + segmentStore.put(segId, segBytes); + segmentOrderList.add(segId); + if (nextId != 0) { + segmentChainMap.put(segId, nextId); + chainedTargets.add(nextId); + } + } + idx = searchIdx; + } else { + int oLen = getOrderLength(data, idx, end); + if (oLen <= 0) break; + idx += oLen; + } + } + } + /** * Decodes a stream of GOCA drawing orders. */ @@ -155,6 +264,15 @@ public class GocaDecoder { end = offset + length; } + if (callDepth == 0) { + indexSegments(inputData, idx, end - idx); + } + + decodeStreamDirect(inputData, idx, end - idx); + } + + private void decodeStreamDirect(byte[] inputData, int idx, int length) { + int end = idx + length; while (idx < end) { int order = inputData[idx] & 0xFF; int orderLen = getOrderLength(inputData, idx, end); @@ -225,6 +343,22 @@ public class GocaDecoder { idx += orderLen; break; } + case GocaConstants.G_GCALL: { // Call Segment (0x2A) + if (callDepth < 16 && idx + 4 < end) { + int targetSegId = ((inputData[idx + 1] & 0xFF) << 24) | + ((inputData[idx + 2] & 0xFF) << 16) | + ((inputData[idx + 3] & 0xFF) << 8) | + (inputData[idx + 4] & 0xFF); + byte[] targetSeg = segmentStore.get(targetSegId); + if (targetSeg != null) { + callDepth++; + decodeStream(targetSeg, 0, targetSeg.length); + callDepth--; + } + } + idx += orderLen; + break; + } case GocaConstants.G_GSCA: { // Set Character Angle (0x34) if (payloadLen >= 4 && idx + 5 < end) { int ax = readCoord(inputData, idx + 2); @@ -312,7 +446,6 @@ public class GocaDecoder { } case 0x04: case GocaConstants.G_GSMX: - case GocaConstants.G_GSBMX: case GocaConstants.G_GSFLW: case GocaConstants.G_GSMP: case GocaConstants.G_GSCC: @@ -321,9 +454,17 @@ public class GocaDecoder { idx += orderLen; break; } + case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D) + bgMix = inputData[idx + 1] & 0xFF; + idx += orderLen; + break; + } case GocaConstants.G_GBAR: { // Begin Area (0x68) int flags = inputData[idx + 1] & 0xFF; - beginArea((flags & 0x40) != 0); + boolean drawBoundary = (flags & 0x80) != 0 || (flags == 0); + boolean fill = (flags == 0) || (flags & 0x40) != 0 || + (pattern >= 1 && pattern <= 14); + beginArea(drawBoundary, fill); idx += orderLen; break; } @@ -426,7 +567,7 @@ public class GocaDecoder { break; } case GocaConstants.G_GBIMG: { // Begin Image (0xD1) - if (idx + 9 < end) { + if (payloadLen >= 8 && idx + 2 + payloadLen <= end) { int x = readCoord(inputData, idx + 2); int y = readCoord(inputData, idx + 4); int w = readCoord(inputData, idx + 6); @@ -468,41 +609,57 @@ public class GocaDecoder { int order = data[idx] & 0xFF; switch (order) { - case GocaConstants.P_NOP1: - case GocaConstants.P_ATTCUR: - case GocaConstants.P_DETCUR: + case GocaConstants.P_NOP1: { + idx += (idx + 1 < end && data[idx + 1] == 0) ? 2 : 1; + break; + } + case GocaConstants.P_ATTCUR: { // 0x08: Attach Graphic Cursor + this.graphicsCursorActive = true; + idx += 2; + break; + } + case GocaConstants.P_DETCUR: { // 0x09: Detach Graphic Cursor + this.graphicsCursorActive = false; + idx += 2; + break; + } case GocaConstants.P_STOPDR: { - idx++; + idx += 2; break; } case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space plane.clear(); resetDefaults(); - idx++; + idx += 2; break; } case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12) - int len = 12; - if (idx + 1 < end && data[idx + 1] != 0) { - len = (data[idx + 1] & 0xFF) + 2; - } - idx += len; + idx += 12; break; } - case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults + case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute + if (idx + 1 < end) { + int len = (data[idx + 1] & 0xFF) + 2; + idx += len; + } else { + idx++; + } + break; + } + case GocaConstants.P_SETCUR: { // 0x31: Set Graphic Cursor Position + if (idx + 5 <= end) { + this.graphicCursorX = readCoord(data, idx + 2); + this.graphicCursorY = readCoord(data, idx + 4); + } if (idx + 1 < end) { int len = data[idx + 1] & 0xFF; - if (idx + 2 + len <= end) { - decodeStream(data, idx + 2, len); - } idx += 2 + len; } else { idx++; } break; } - case GocaConstants.P_COMT: - case GocaConstants.P_SETCUR: { + case GocaConstants.P_COMT: { if (idx + 1 < end) { int len = data[idx + 1] & 0xFF; idx += 2 + len; @@ -524,9 +681,10 @@ public class GocaDecoder { } } - private void beginArea(boolean drawBoundary) { + private void beginArea(boolean drawBoundary, boolean fill) { this.inArea = true; this.areaDrawBoundary = drawBoundary; + this.areaFill = fill; this.fillColor = this.curColor; this.areaPointsX.clear(); this.areaPointsY.clear(); @@ -547,7 +705,8 @@ public class GocaDecoder { py[i] = plane.mapY(areaPointsY.get(i)); } - plane.fillArea(px, py, n, fillColor, pattern, areaDrawBoundary, curColor, lineType, lineWidth); + plane.fillArea(px, py, n, fillColor, areaFill ? pattern : GocaConstants.PT_EMPTY, + areaDrawBoundary, curColor, lineType, lineWidth, bgMix, bgColor); inArea = false; areaPointsX.clear(); areaPointsY.clear(); @@ -555,8 +714,11 @@ public class GocaDecoder { private void addAreaPoint(int x, int y) { if (inArea) { - areaPointsX.add(x); - areaPointsY.add(y); + int sz = areaPointsX.size(); + if (sz == 0 || areaPointsX.get(sz - 1) != x || areaPointsY.get(sz - 1) != y) { + areaPointsX.add(x); + areaPointsY.add(y); + } } } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicInputBuilder.java b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicInputBuilder.java new file mode 100644 index 0000000..dab76f6 --- /dev/null +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicInputBuilder.java @@ -0,0 +1,59 @@ +package org.lib3270j.graphics; + +/** + * Builds the 56-byte IBM 3179G / 3270G Graphic Input Structured Field. + * Used for light-pen and graphics cursor interactive input per IBM HOD / GDDM specifications. + */ +public class GraphicInputBuilder { + + // 56-byte template mask from IBM Host On-Demand (HODInput.java) + private static final byte[] MASK = new byte[] { + 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, + 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00 + }; + + /** + * Builds the 56-byte Graphic Input Structured Field. + * + * @param gocaX GOCA signed X coordinate (-xMax..+xMax) + * @param gocaY GOCA signed Y coordinate (-yMax..+yMax) + * @param aidCode The 3270 AID code (e.g. 0x7D for ENTER, 0xF3 for PF3) + * @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID + * @param isShift true if shift key was down + * @param isCtrl true if ctrl key was down + * @return 56-byte payload + */ + public static byte[] buildGraphicInput(int gocaX, int gocaY, int aidCode, + boolean isMouseAction, boolean isShift, boolean isCtrl) { + byte[] sf = new byte[MASK.length]; + System.arraycopy(MASK, 0, sf, 0, MASK.length); + + // Byte 24-25: GOCA X coordinate (signed 16-bit big-endian) + sf[24] = (byte) ((gocaX >> 8) & 0xFF); + sf[25] = (byte) (gocaX & 0xFF); + + // Byte 26-27: GOCA Y coordinate (signed 16-bit big-endian) + sf[26] = (byte) ((gocaY >> 8) & 0xFF); + sf[27] = (byte) (gocaY & 0xFF); + + if (isMouseAction) { + sf[31] = 0x04; + sf[33] = 0x04; + sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00); + sf[35] = (byte) (aidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick + } else { + // Keyboard AID (Enter, PF keys) + sf[31] = 0x07; + sf[33] = 0x07; + sf[34] = (byte) 0xFF; + sf[35] = (byte) (aidCode & 0xFF); + } + + return sf; + } +} diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java index 2cb144f..414ccb1 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/GraphicsPlane.java @@ -97,9 +97,14 @@ public class GraphicsPlane { return canvasHeight; } - public void setScreenDimensions(int cols, int rows) { + public synchronized void setScreenDimensions(int cols, int rows) { this.screenCols = cols > 0 ? cols : 80; this.screenRows = rows > 0 ? rows : 24; + int targetW = this.screenCols * 9; + int targetH = this.screenRows * 12; + if (this.canvasWidth != targetW || this.canvasHeight != targetH) { + resize(targetW, targetH); + } } public int getScreenCols() { @@ -124,12 +129,32 @@ public class GraphicsPlane { * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down). */ public int mapY(int gocaY) { - int nominalHeight = screenRows * 16; + int nominalHeight = screenRows * 12; int yMax = (nominalHeight - 1) / 2; int ny = yMax - gocaY; return (int) Math.round((double) ny * canvasHeight / nominalHeight); } + /** + * Maps a canvas pixel X coordinate back to GOCA signed coordinate (-xMax..+xMax). + */ + public int unmapX(int px) { + int nominalWidth = screenCols * 9; + int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0); + int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1)); + return nx - xMax; + } + + /** + * Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up). + */ + public int unmapY(int py) { + int nominalHeight = screenRows * 12; + int yMax = (nominalHeight - 1) / 2; + int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1)); + return yMax - ny; + } + /** * Safely plots a pixel at (x, y). */ @@ -286,9 +311,19 @@ public class GraphicsPlane { */ public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern, boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) { + fillArea(px, py, numPoints, fillColorArgb, pattern, drawBoundary, boundaryColorArgb, lineType, lineWidth, 2, 0xFF000000); + } + + /** + * Fills a closed polygon area with a solid color or hatching pattern and background mix. + */ + public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern, + boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth, + int bgMix, int bgColorArgb) { if (px == null || py == null || numPoints < 3) return; int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF; + int bg = bgColorArgb; if (pattern != GocaConstants.PT_EMPTY) { // Find polygon vertical bounds @@ -323,12 +358,14 @@ public class GraphicsPlane { int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); for (int x = leftX; x <= rightX; x++) { - if (pattern == GocaConstants.PT_SOLID || pattern == GocaConstants.PT_DEFAULT || pattern > 16) { + if (pattern == GocaConstants.PT_SOLID || pattern >= 16) { setPixel(x, y, fill); } else { int b = patRows[y & 7] & 0xFF; if (((b >> (7 - (x & 7))) & 1) != 0) { setPixel(x, y, fill); + } else if (bgMix != 0) { // BMX_OVERPAINT (opaque background) + setPixel(x, y, bg); } } } @@ -506,13 +543,14 @@ public class GraphicsPlane { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { if (imageData == null || width <= 0 || height <= 0) return; int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF; + int bytesPerRow = (width + 7) / 8; for (int row = 0; row < height; row++) { + int rowOffset = row * bytesPerRow; for (int col = 0; col < width; col++) { - int bitIndex = row * width + col; - int byteIdx = bitIndex / 8; + int byteIdx = rowOffset + (col / 8); if (byteIdx < imageData.length) { - boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0; + boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0; if (bit) { setPixel(x + col, y + row, fgColor); } diff --git a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java index 49933f3..3da3601 100644 --- a/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java +++ b/lib3270j/src/main/java/org/lib3270j/graphics/ProgramSymbolManager.java @@ -87,8 +87,11 @@ public class ProgramSymbolManager { } ProgramSymbolSet set = lcidMap[lcid]; if (set == null) { + for (ProgramSymbolSet s : sets) { + if (s != null && s.getLcid() == lcid) return s; + } for (ProgramSymbolSet s : stagingSets) { - if (s.getLcid() == lcid) return s; + if (s != null && s.getLcid() == lcid) return s; } } return set; @@ -98,18 +101,7 @@ public class ProgramSymbolManager { * Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE). */ public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) { - if (lcid <= 0 || lcid >= 256) { - return null; - } - ProgramSymbolSet set = lcidMap[lcid]; - if (set == null) { - for (ProgramSymbolSet s : stagingSets) { - if (s.getLcid() == lcid) { - set = s; - break; - } - } - } + ProgramSymbolSet set = getSymbolSet(lcid); if (set == null) { return null; } @@ -153,7 +145,7 @@ public class ProgramSymbolManager { } boolean isTriplePlane = (rws >= 4 && rws <= 7); - ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex]; + ProgramSymbolSet set = sets[setIndex]; int extHeaderLen = 0; int cellWidth = defaultCellWidth; @@ -176,7 +168,7 @@ public class ProgramSymbolManager { } set.setLcid(lcid); - if (!isTriplePlane && lcid > 0 && lcid < 256) { + if (lcid > 0 && lcid < 256) { lcidMap[lcid] = set; } diff --git a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java index 63362be..801e0e2 100644 --- a/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java +++ b/lib3270j/src/main/java/org/lib3270j/input/InputProcessor.java @@ -32,11 +32,20 @@ public class InputProcessor { } private org.lib3270j.graphics.GraphicsPlane graphicsPlane; + private org.lib3270j.graphics.GocaDecoder gocaDecoder; public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) { this.graphicsPlane = gp; } + public void setGocaDecoder(org.lib3270j.graphics.GocaDecoder gd) { + this.gocaDecoder = gd; + } + + public org.lib3270j.graphics.GocaDecoder getGocaDecoder() { + return gocaDecoder; + } + public enum OiaStatus { NOT_CONNECTED("OFFLINE"), X_SYSTEM("X SYSTEM"), @@ -159,7 +168,11 @@ public class InputProcessor { * Send an AID key (Enter, PF1-24, PA1-3, Clear). */ public void sendAid(int aidCode) { - if (keyboardLocked && aidCode != AID_CLEAR) return; + System.err.println("sendAid called: 0x" + Integer.toHexString(aidCode) + " locked=" + keyboardLocked); + if (keyboardLocked && aidCode != AID_CLEAR) { + System.err.println("Keyboard locked, dropping AID"); + return; + } lastAid = aidCode; setKeyboardLocked(true); @@ -213,15 +226,30 @@ public class InputProcessor { } if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) { - // PA keys: send AID + cursor address only (no modified data) + // PA keys: send AID + optional PID + cursor address only (no modified data) + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(aidCode); byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); - byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] }; - sendAidResponse(data); + out.write(caddr[0] & 0xFF); + out.write(caddr[1] & 0xFF); + sendAidResponse(out.toByteArray()); return; } - // Enter, PF keys: send AID + cursor address + modified field data + // Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data ByteArrayOutputStream out = new ByteArrayOutputStream(); + + // If GDDM attached graphic cursor (interactive graphics / light-pen mode): + if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive() && aidCode != AID_CLEAR) { + int gx = gocaDecoder.getGraphicCursorX(); + int gy = gocaDecoder.getGraphicCursorY(); + byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(gx, gy, aidCode, false, false, false); + out.write(AID_SF); // 0x88 + try { + out.write(sf); + } catch (java.io.IOException ignored) {} + } + out.write(aidCode); byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); @@ -267,28 +295,16 @@ public class InputProcessor { } } } else { - // Unformatted screen in 3270 mode (e.g. line-mode console): - // Send AID + cursor address + only the active input line (row containing cursor) - int cols = screen.getCols(); - int curAddr = screen.getCursorAddress(); - int row = curAddr / cols; - int rowStart = row * cols; - int rowEnd = rowStart + cols; - - // Find last non-null, non-blank character on the current line - int lastChar = rowStart - 1; - for (int i = rowEnd - 1; i >= rowStart; i--) { - int ec = screen.getCell(i).ec & 0xFF; - if (ec != 0x00 && ec != 0x40) { - lastChar = i; - break; - } - } - - if (lastChar >= rowStart) { - for (int i = rowStart; i <= lastChar; i++) { - int b = screen.getCell(i).ec & 0xFF; - out.write(b != 0 ? b : 0x40); + // Unformatted screen in 3270 mode: + // Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line + // or we can just send everything up to the last non-null on the screen. + // IBM spec: "all alphanumeric characters... Nulls are suppressed." + // Actually, the simplest is to send everything, but suppress nulls. + int size = screen.getRows() * screen.getCols(); + for (int i = 0; i < size; i++) { + int b = screen.getCell(i).ec & 0xFF; + if (b != 0x00) { + out.write(b); } } } @@ -304,6 +320,36 @@ public class InputProcessor { } } + /** + * Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G. + */ + public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) { + if (fsm == null || !fsm.getConnectionState().isFullSession()) { + return; + } + if (isKeyboardLocked()) { + return; + } + setKeyboardLocked(true); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) { + int gx = gocaDecoder.getGraphicCursorX(); + int gy = gocaDecoder.getGraphicCursorY(); + byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(gx, gy, aidCode, true, isShift, isCtrl); + out.write(AID_SF); + try { + out.write(sf); + } catch (java.io.IOException ignored) {} + } + out.write(aidCode); + byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); + out.write(caddr[0] & 0xFF); + out.write(caddr[1] & 0xFF); + + sendAidResponse(out.toByteArray()); + } + // ========== Cursor movement ========== public void cursorUp() { diff --git a/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java b/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java index 476f7e6..607ee38 100644 --- a/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java +++ b/lib3270j/src/main/java/org/lib3270j/protocol/TelnetConstants.java @@ -76,6 +76,7 @@ public final class TelnetConstants { case TELOPT_BINARY: return "BINARY"; case TELOPT_ECHO: return "ECHO"; case TELOPT_SGA: return "SGA"; + case TELOPT_TM: return "TIMING-MARK"; case TELOPT_TTYPE: return "TTYPE"; case TELOPT_EOR: return "EOR"; case TELOPT_NAWS: return "NAWS"; diff --git a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java index c7150c4..7032585 100644 --- a/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java +++ b/lib3270j/src/main/java/org/lib3270j/screen/ScreenBuffer.java @@ -24,6 +24,9 @@ public class ScreenBuffer { private boolean screenAlt; // Using alternate screen? private boolean formatted; // Screen has at least one field attribute? private byte replyMode = SF_SRM_FIELD; + + private int activePartition = 0; // 0 = implicit partition + private boolean explicitPartitionActive = false; // Change tracking private boolean screenChanged; @@ -167,6 +170,10 @@ public class ScreenBuffer { public byte getReplyMode() { return replyMode; } public void setReplyMode(byte mode) { this.replyMode = mode; } + + public int getActivePartition() { return activePartition; } + public void setActivePartition(int pid) { this.activePartition = pid; this.explicitPartitionActive = true; } + public boolean isExplicitPartitionActive() { return explicitPartitionActive; } // ========== Screen erase ========== @@ -187,6 +194,19 @@ public class ScreenBuffer { updateDisplaySnapshot(); } + public void setFieldAttribute(int pos, byte fa) { + ExtendedAttribute ea = buffer[pos]; + ea.clear(); + ea.fa = fa; + if (!formatted) { + System.err.println("SCREEN BECAME FORMATTED at pos " + pos); + } + formatted = true; + screenChanged = true; + // The display logic needs to render this attribute character + ea.ec = 0x00; // Typically null character for the attribute space itself + } + /** Clear the entire buffer. */ public synchronized void clear() { for (ExtendedAttribute ea : buffer) { @@ -195,6 +215,9 @@ public class ScreenBuffer { cursorAddress = 0; bufferAddress = 0; formatted = false; + replyMode = SF_SRM_FIELD; + activePartition = 0; + explicitPartitionActive = false; screenChanged = true; defaultFg = 0x00; diff --git a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java index d6436ba..d6caa42 100644 --- a/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java +++ b/lib3270j/src/main/java/org/lib3270j/telnet/TelnetFSM.java @@ -8,6 +8,7 @@ import org.lib3270j.listener.*; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; @@ -59,6 +60,32 @@ public class TelnetFSM { private int responseRequired = RSF_NO_RESPONSE; private boolean deferredWillTtype; private boolean tn3270eDeviceTypeSent; + private int ttypeIndex = 0; + + private List getCandidateTerminalTypes() { + List list = new ArrayList<>(); + if (config.getTerminalName() != null && !config.getTerminalName().trim().isEmpty()) { + list.add(config.getTerminalName().trim()); + return list; + } + TerminalModel model = config.getModel(); + list.add(model.getTerminalType()); + list.add(model.getBaseTerminalType()); + if (model.isColor()) { + try { + TerminalModel mono = TerminalModel.forModel(model.getModelNumber(), false); + list.add(mono.getTerminalType()); + list.add(mono.getBaseTerminalType()); + } catch (Exception ignored) {} + } + if (model.getModelNumber() != 2) { + list.add("IBM-3279-2-E"); + list.add("IBM-3279-2"); + list.add("IBM-3278-2"); + } + list.add("UNKNOWN"); + return list; + } // Connection references private TelnetConnection connection; @@ -109,6 +136,7 @@ public class TelnetFSM { tn3270eBound = false; eXmitSeq = 0; deferredWillTtype = false; + ttypeIndex = 0; ibuf.reset(); sbbuf.reset(); @@ -116,6 +144,9 @@ public class TelnetFSM { eFuncs[FUNC_BIND_IMAGE] = true; eFuncs[FUNC_RESPONSES] = true; eFuncs[FUNC_SYSREQ] = true; + eFuncs[FUNC_SNA_SENSE] = true; + eFuncs[FUNC_DATA_STREAM_CTL] = true; + eFuncs[FUNC_CONTENTION_RESOLUTION] = true; changeState(ConnectionState.TELNET_PENDING); } @@ -240,6 +271,11 @@ public class TelnetFSM { log.fine("RCVD GA"); state = TNS_DATA; break; + case AYT: + log.info("RCVD AYT - sending acknowledgment"); + sendBytes(new byte[] { (byte) IAC, (byte) NOP }); + state = TNS_DATA; + break; case NOP: log.fine("RCVD NOP"); state = TNS_DATA; @@ -267,6 +303,11 @@ public class TelnetFSM { } break; + case TELOPT_TM: + // RFC 860: Timing Mark - reply with DO TM + sendCommand(DO, opt); + break; + case TELOPT_TN3270E: if (!config.isTn3270eEnabled()) { sendCommand(DONT, opt); @@ -310,6 +351,11 @@ public class TelnetFSM { } break; + case TELOPT_TM: + // RFC 860: Timing Mark - reply with WILL TM + sendCommand(WILL, opt); + break; + case TELOPT_TTYPE: if (!myOpts[opt]) { myOpts[opt] = true; @@ -426,8 +472,12 @@ public class TelnetFSM { private void handleTTypeSB(byte[] data) { if (data.length >= 2 && data[1] == TELQUAL_SEND) { - // Host asks for terminal type - String termType = config.getEffectiveTerminalType(); + // Host asks for terminal type — cycle per RFC 1091 + List candidates = getCandidateTerminalTypes(); + String termType = candidates.get(Math.min(ttypeIndex, candidates.size() - 1)); + if (ttypeIndex < candidates.size() - 1) { + ttypeIndex++; + } log.info("RCVD SB TTYPE SEND - Responding with: " + termType); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -538,7 +588,7 @@ public class TelnetFSM { if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) { // Try fallback model 2 if we were requesting something else if (config.getModel() != TerminalModel.IBM_3278_2 && - config.getModel() != TerminalModel.IBM_3279_2) { + config.getModel() != TerminalModel.IBM_3279_4) { log.warning("TN3270E device-type rejected (" + TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2"); config.setModel(TerminalModel.IBM_3278_2); @@ -880,6 +930,23 @@ public class TelnetFSM { tn3270eSubmode = TN3270ESubmode.E_NVT; break; + case DT_REQUEST: + log.info("Received DT_REQUEST requestFlag=" + requestFlag); + if ((requestFlag & RQF_KEYBOARD_RESTORE) != 0) { + if (dsProcessor != null && dsProcessor.getInputProcessor() != null) { + dsProcessor.getInputProcessor().setKeyboardLocked(false); + } + } + if ((requestFlag & RQF_SIGNAL) != 0) { + for (ScreenUpdateListener l : screenListeners) { + l.onSoundAlarm(); + } + } + if (eFuncs[FUNC_RESPONSES] && responseFlag == RSF_ALWAYS_RESPONSE) { + sendTN3270EPositiveResponse(seqNumber); + } + break; + case DT_RESPONSE: log.fine("Received response, seq=" + seqNumber); break; @@ -903,7 +970,7 @@ public class TelnetFSM { } } - private void sendTN3270EPositiveResponse(int seqNumber) { + public void sendTN3270EPositiveResponse(int seqNumber) { byte[] resp = new byte[EH_SIZE + 1]; resp[0] = (byte) DT_RESPONSE; resp[1] = 0; @@ -915,6 +982,31 @@ public class TelnetFSM { sendRecord(resp); } + public void sendTN3270ENegativeResponse(int seqNumber, int negCode) { + byte[] resp = new byte[EH_SIZE + 1]; + resp[0] = (byte) DT_RESPONSE; + resp[1] = 0; + resp[2] = (byte) RSF_NEGATIVE_RESPONSE; + resp[3] = (byte) ((seqNumber >> 8) & 0xFF); + resp[4] = (byte) (seqNumber & 0xFF); + resp[5] = (byte) (negCode & 0xFF); + + sendRecord(resp); + } + + public void sendTN3270ESnaSenseResponse(int seqNumber, int sense1, int sense2) { + byte[] resp = new byte[EH_SIZE + 2]; + resp[0] = (byte) DT_RESPONSE; + resp[1] = 0; + resp[2] = (byte) RSF_SNA_SENSE; + resp[3] = (byte) ((seqNumber >> 8) & 0xFF); + resp[4] = (byte) (seqNumber & 0xFF); + resp[5] = (byte) (sense1 & 0xFF); + resp[6] = (byte) (sense2 & 0xFF); + + sendRecord(resp); + } + // ========== Check if we should transition to 3270 mode ========== private void checkIn3270() { diff --git a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java index 447929f..71a36f6 100644 --- a/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java +++ b/lib3270j/src/test/java/org/lib3270j/datastream/QueryReplyBuilderTest.java @@ -232,4 +232,30 @@ public class QueryReplyBuilderTest { assertEquals(0x07, replies[descOffset + 49] & 0xFF); assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF); } + + @Test + public void testQueryReplyImageAndRpqNames() { + qrBuilder.setGraphicsMode(GraphicsMode.BOTH); + byte[] requested = new byte[] { (byte) QR_IMAGE, (byte) QR_RPQNAMES, (byte) QR_GIMAGE }; + byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43); + + assertNotNull(replies); + assertEquals((byte) AID_SF, replies[0]); + + // First SF should be QR_NULL (0xFF) because 0x82 is 3270 Image SF (not supported on 3179G) + int len1 = ((replies[1] & 0xFF) << 8) | (replies[2] & 0xFF); + assertEquals(0x81, replies[3] & 0xFF); // SFID_QREPLY + assertEquals(QR_NULL, replies[4] & 0xFF); // 0xFF + + // Second SF should be QR_RPQNAMES (0xA1) matching requested code + int pos2 = 1 + len1; + int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF); + assertEquals(0x81, replies[pos2 + 2] & 0xFF); + assertEquals(QR_RPQNAMES, replies[pos2 + 3] & 0xFF); // 0xA1 + + // Third SF should be QR_GIMAGE (0xB1) + int pos3 = pos2 + len2; + assertEquals(0x81, replies[pos3 + 2] & 0xFF); + assertEquals(QR_GIMAGE, replies[pos3 + 3] & 0xFF); // 0xB1 + } } diff --git a/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java b/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java index 90a9a1c..3848b96 100644 --- a/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java +++ b/lib3270j/src/test/java/org/lib3270j/graphics/GocaDecoderTest.java @@ -158,6 +158,7 @@ public class GocaDecoderTest { byte[] stream = out.toByteArray(); decoder.decodeStream(stream, 0, stream.length); + assertTrue(plane.hasContent()); } @@ -203,7 +204,7 @@ public class GocaDecoderTest { // 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10) out.write(GocaConstants.G_GCRLIN); - out.write(0x04); // len = 4 (2 steps) + out.write(0x04); // len = 4 (2 deltas * 2 bytes) out.write(0x0A); out.write(0x14); // dx = +10, dy = +20 out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10 @@ -217,21 +218,74 @@ public class GocaDecoderTest { @Test public void test3179GCoordinateMapping() { - GraphicsPlane plane = new GraphicsPlane(720, 688); + GraphicsPlane plane = new GraphicsPlane(720, 516); plane.setScreenDimensions(80, 43); - // Screen center (0, 0) should map to canvas center (360, 343) + // Screen center (0, 0) should map to canvas center (360, 257) assertEquals(360, plane.mapX(0)); - assertEquals(343, plane.mapY(0)); + assertEquals(257, plane.mapY(0)); // Left edge (-360) should map to 0 assertEquals(0, plane.mapX(-360)); // Right edge (+359) should map to 719 assertEquals(719, plane.mapX(359)); - // Top edge (+343) should map to 0 - assertEquals(0, plane.mapY(343)); - // Bottom edge (-344) should map to 687 - assertEquals(687, plane.mapY(-344)); + // Top edge (+257) should map to 0 + assertEquals(0, plane.mapY(257)); + // Bottom edge (-258) should map to 515 + assertEquals(515, plane.mapY(-258)); + } + + @Test + public void testImageOrdersGbimgGimdGeimg() { + GraphicsPlane plane = new GraphicsPlane(400, 300); + GocaDecoder decoder = new GocaDecoder(plane); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // G_GBIMG (0xD1): Begin Image: order, len=8, x=100, y=100, w=16, h=2 + out.write(GocaConstants.G_GBIMG); + out.write(0x08); + out.write(0x00); out.write(0x64); // x = 100 + out.write(0x00); out.write(0x64); // y = 100 + out.write(0x00); out.write(0x10); // w = 16 + out.write(0x00); out.write(0x02); // h = 2 + + // G_GIMD (0x92): Image Data: order, len=4, 4 bytes of bitmap (16x2 pixels = 32 bits = 4 bytes) + out.write(GocaConstants.G_GIMD); + out.write(0x04); + out.write(0xFF); out.write(0x00); + out.write(0xAA); out.write(0x55); + + // G_GEIMG (0x91): End Image + out.write(GocaConstants.G_GEIMG); + out.write(0x00); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent(), "Expected plane to have content after image decoding"); + } + + @Test + public void testDirectObjectControlSf24() { + org.lib3270j.screen.ScreenBuffer sb = new org.lib3270j.screen.ScreenBuffer( + org.lib3270j.TerminalModel.IBM_3279_4, new org.lib3270j.charset.EbcdicTranslator()); + org.lib3270j.datastream.DataStreamProcessor dsp = new org.lib3270j.datastream.DataStreamProcessor( + sb, new org.lib3270j.charset.EbcdicTranslator()); + + // First draw something on graphics plane + dsp.getGraphicsPlane().setPixel(10, 10, 0xFFFFFFFF); + assertTrue(dsp.getGraphicsPlane().hasContent()); + + // Send WSF with SF_OBJCNTL (0x24) containing P_ERASE (0x0A) + byte[] wsf = new byte[] { + (byte) org.lib3270j.protocol.DS3270Constants.CMD_WSF, + 0x00, 0x04, // Field length = 4 + (byte) org.lib3270j.graphics.GocaConstants.SF_OBJCNTL, // 0x24 + (byte) org.lib3270j.graphics.GocaConstants.P_ERASE // 0x0A + }; + dsp.processRecord(wsf, 0, wsf.length, false); + + assertFalse(dsp.getGraphicsPlane().hasContent(), "Expected graphics plane to be cleared after SF_OBJCNTL P_ERASE"); } } diff --git a/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java b/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java index dd15919..829db33 100644 --- a/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java +++ b/lib3270j/src/test/java/org/lib3270j/input/InputProcessorTest.java @@ -131,4 +131,26 @@ public class InputProcessorTest { assertEquals(0, screen.getDisplayCell(2).ucs4); assertEquals('A', screen.getDisplayCell(1).ucs4); } + + @Test + public void testGraphicInputBuilderStructure() { + byte[] sf = org.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false); + assertEquals(56, sf.length); + assertEquals(0x00, sf[0]); + assertEquals(0x34, sf[1]); // Length = 52 + assertEquals(0x0F, sf[2]); // SF ID = 0x0F + assertEquals(0x0F, sf[3]); // SF ID = 0x0F + + // Coordinates + int x = (sf[24] << 8) | (sf[25] & 0xFF); + int y = (sf[26] << 8) | (sf[27] & 0xFF); + assertEquals(100, (short) x); + assertEquals(-50, (short) y); + + // Keyboard AID + assertEquals(0x07, sf[31]); + assertEquals(0x07, sf[33]); + assertEquals((byte) 0xFF, sf[34]); + assertEquals((byte) AID_ENTER, sf[35]); + } } diff --git a/lib3270j/src/test/java/org/lib3270j/telnet/TelnetFSMTest.java b/lib3270j/src/test/java/org/lib3270j/telnet/TelnetFSMTest.java index 4d25ece..39c96a6 100644 --- a/lib3270j/src/test/java/org/lib3270j/telnet/TelnetFSMTest.java +++ b/lib3270j/src/test/java/org/lib3270j/telnet/TelnetFSMTest.java @@ -219,4 +219,79 @@ public class TelnetFSMTest { assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(3))); assertEquals('O', trans.ebcdicToUnicode(screenBuffer.getCellEC(4))); } + + @Test + public void testTelnetTimingMarkOption6() { + fsm.onConnected(); + connection.sentData.clear(); + + // Host sends DO TELOPT_TM (DO 6) -> Client must reply WILL TELOPT_TM (WILL 6) per RFC 860 + feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TM); + boolean foundWillTm = false; + for (byte[] pkt : connection.sentData) { + if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC && + (pkt[1] & 0xFF) == TelnetConstants.WILL && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) { + foundWillTm = true; + } + } + assertTrue(foundWillTm, "Expected IAC WILL TELOPT_TM reply upon receiving IAC DO TELOPT_TM"); + + // Host sends WILL TELOPT_TM (WILL 6) -> Client must reply DO TELOPT_TM (DO 6) + connection.sentData.clear(); + feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TM); + boolean foundDoTm = false; + for (byte[] pkt : connection.sentData) { + if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC && + (pkt[1] & 0xFF) == TelnetConstants.DO && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TM) { + foundDoTm = true; + } + } + assertTrue(foundDoTm, "Expected IAC DO TELOPT_TM reply upon receiving IAC WILL TELOPT_TM"); + } + + @Test + public void testIACAYT() { + fsm.onConnected(); + connection.sentData.clear(); + + // Host sends IAC AYT + feedBytes(TelnetConstants.IAC, TelnetConstants.AYT); + boolean foundNop = false; + for (byte[] pkt : connection.sentData) { + if (pkt.length == 2 && (pkt[0] & 0xFF) == TelnetConstants.IAC && (pkt[1] & 0xFF) == TelnetConstants.NOP) { + foundNop = true; + } + } + assertTrue(foundNop, "Expected IAC NOP acknowledgment for IAC AYT"); + } + + @Test + public void testTTypeCycling() { + fsm.onConnected(); + connection.sentData.clear(); + + // Enable TTYPE + feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE); + + // First SEND request -> Expect configured type IBM-3279-4-E + feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE, + TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE); + byte[] lastPkt = connection.sentData.get(connection.sentData.size() - 1); + String resp1 = new String(lastPkt, 4, lastPkt.length - 6); + assertEquals("IBM-3279-4-E", resp1); + + // Second SEND request -> Expect fallback IBM-3279-4 + feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE, + TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE); + lastPkt = connection.sentData.get(connection.sentData.size() - 1); + String resp2 = new String(lastPkt, 4, lastPkt.length - 6); + assertEquals("IBM-3279-4", resp2); + + // Third SEND request -> Expect monochrome fallback IBM-3278-4-E + feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE, + TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE); + lastPkt = connection.sentData.get(connection.sentData.size() - 1); + String resp3 = new String(lastPkt, 4, lastPkt.length - 6); + assertEquals("IBM-3278-4-E", resp3); + } } diff --git a/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java b/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java index 4a11bcf..006cdc7 100644 --- a/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java +++ b/lib3270j/src/test/java/org/lib3270j/tls/TlsConfigTest.java @@ -31,7 +31,7 @@ public class TlsConfigTest { assertEquals(992, c1.getPort()); // ssl: prefix with explicit port - ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_2); + ConnectionConfig c2 = ConnectionConfig.parseHostString("ssl:zos.local:2323", 0, TerminalModel.IBM_3279_4); assertTrue(c2.isUseTls()); assertEquals("zos.local", c2.getHost()); assertEquals(2323, c2.getPort());