7 Commits

Author SHA1 Message Date
rudi e28b7dc30a Lightpen on
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 37s
2026-08-24 16:45:55 +00:00
rudi d9df1052a1 Lightpen on 2026-08-24 16:35:24 +00:00
rudi 0837de8db3 ADMOPSLA and ADMCHART are happier
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m7s
2026-08-23 01:27:45 +00:00
rudi 42eef519a6 Repo clean
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 1m4s
2026-08-22 13:17:31 +00:00
rudi b57d8f960d How had I not tested on Linux before
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 45s
2026-08-21 21:00:10 -04:00
rudi 09600260b5 Add some cleaning
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 37s
2026-08-21 18:56:37 -04:00
rudi 00c7b0770c Missed a folder!
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 46s
2026-08-21 16:35:18 -04:00
63 changed files with 1331 additions and 174 deletions
+4
View File
@@ -1,4 +1,8 @@
j3270.log.* j3270.log.*
*.txt
*.py
*.pcap
build/*
*.log *.log
.DS_Store .DS_Store
*.jar *.jar
+2 -8
View File
@@ -3,7 +3,7 @@
[![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions) [![Build & Test](https://git.hugfreevikings.wtf/rudi/j3270/actions/workflows/build.yaml/badge.svg)](https://git.hugfreevikings.wtf/rudi/j3270/actions)
[![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases) [![Releases](https://img.shields.io/badge/Releases-Gitea-blue.svg)](https://git.hugfreevikings.wtf/rudi/j3270/releases)
[![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net) [![Java](https://img.shields.io/badge/Java-11%20%7C%2017%20%7C%2021-blue.svg)](https://adoptium.net)
[![License](https://img.shields.io/badge/License-MIT%20%2F%20BSD-green.svg)](LICENSE) [![License](https://img.shields.io/badge/License-Unlicense-blue.svg)](LICENSE)
An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E. An **x3270-aligned IBM 3270 mainframe terminal emulator** written in pure Java. Designed for high compatibility with IBM z/OS, z/VM, CMS, and TSO systems over TN3270 and TN3270E.
@@ -76,7 +76,7 @@ The project includes self-contained, portable build scripts:
# Build executable JAR in 2 seconds: # Build executable JAR in 2 seconds:
sh ./build_all.sh sh ./build_all.sh
# Run all 56 automated unit tests in ~500ms: # Run all 57 automated unit tests in ~500ms:
sh ./test_all.sh sh ./test_all.sh
``` ```
@@ -90,12 +90,6 @@ The resulting standalone JAR is created at `build/j3270.jar`.
--- ---
## 🤝 Contributing
Contributions and issue reports are welcome! Please feel free to open a bug report or feature request via the Gitea issue tracker.
---
## 📜 Acknowledgements ## 📜 Acknowledgements
- [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications - [x3270](https://x3270.miraheze.org/wiki/Main_Page) — Reference C implementation and protocol specifications
+3
View File
@@ -8,3 +8,6 @@
- IND$FILE CMS and TSO: - IND$FILE CMS and TSO:
Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode. Fixed command formatting options handling (empty parenthesis removal for CMS binary/default modes and option spacing) and added Query Reply filtering for DFT/DDM mode.
- Local keyboard input and cursor updates not rendering in terminal:
Fixed in ScreenBuffer, InputProcessor, and TerminalPanel by ensuring display snapshot and cursor address are updated synchronously on user input operations (typing, backspace, delete, cursor movement, erase) so the presentation layer immediately renders user keystrokes.
-6
View File
@@ -1,6 +0,0 @@
Manifest-Version: 1.0
Main-Class: org.pubvm.j3270.J3270App
Implementation-Title: j3270
Implementation-Version: 0.1.0
Created-By: j3270 build_all.sh
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -120,7 +120,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
// Actions menu // Actions menu
JMenu actionsMenu = createMenu("Actions"); JMenu actionsMenu = createMenu("Actions");
actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_L, () -> { actionsMenu.add(createMenuItem("Clear", KeyEvent.VK_K, () -> {
if (client != null) if (client != null)
client.sendClear(); client.sendClear();
terminalPanel.repaint(); terminalPanel.repaint();
@@ -131,6 +131,10 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
terminalPanel.repaint(); terminalPanel.repaint();
})); }));
actionsMenu.addSeparator(); actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("Toggle Light Pen (Alt+L)", KeyEvent.VK_L, () -> {
terminalPanel.toggleLightPen();
}, true));
actionsMenu.addSeparator();
actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog)); actionsMenu.add(createMenuItem("File Transfer...", KeyEvent.VK_T, this::showFileTransferDialog));
menuBar.add(actionsMenu); menuBar.add(actionsMenu);
@@ -149,18 +153,22 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
return menu; return menu;
} }
private JMenuItem createMenuItem(String text, int acceleratorKey, Runnable action) { private JMenuItem createMenuItem(String name, int mnemonic, Runnable action, boolean useAlt) {
JMenuItem item = new JMenuItem(text); JMenuItem item = new JMenuItem(name);
item.setBackground(new Color(40, 40, 40)); item.setBackground(new Color(40, 40, 40));
item.setForeground(new Color(200, 200, 200)); item.setForeground(new Color(200, 200, 200));
if (acceleratorKey > 0) { if (mnemonic > 0) {
item.setAccelerator(KeyStroke.getKeyStroke(acceleratorKey, int modifier = useAlt ? KeyEvent.ALT_DOWN_MASK : Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx())); item.setAccelerator(KeyStroke.getKeyStroke(mnemonic, modifier));
} }
item.addActionListener(e -> action.run()); item.addActionListener(e -> action.run());
return item; return item;
} }
private JMenuItem createMenuItem(String name, int mnemonic, Runnable action) {
return createMenuItem(name, mnemonic, action, false);
}
private void showFileTransferDialog() { private void showFileTransferDialog() {
if (client == null) { if (client == null) {
JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.", JOptionPane.showMessageDialog(this, "Connect to a host before using File Transfer.",
@@ -278,7 +286,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.addScreenUpdateListener(this); client.addScreenUpdateListener(this);
terminalPanel.setClient(client); terminalPanel.setClient(client);
statusBar.setClient(client); statusBar.setClient(client, terminalPanel);
String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : ""; String tlsIndicator = config.isUseTls() ? (config.isTlsVerifyCert() ? " [TLS]" : " [TLS/Unverified]") : "";
setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator); setTitle("j3270 — " + config.getHost() + ":" + config.getPort() + tlsIndicator);
@@ -312,7 +320,7 @@ public class J3270App extends JFrame implements ConnectionListener, ScreenUpdate
client.disconnect(); client.disconnect();
client = null; client = null;
terminalPanel.setClient(null); terminalPanel.setClient(null);
statusBar.setClient(null); statusBar.setClient(null, terminalPanel);
terminalPanel.repaint(); terminalPanel.repaint();
setTitle("j3270 — Java TN3270 Terminal Emulator"); setTitle("j3270 — Java TN3270 Terminal Emulator");
} }
@@ -22,6 +22,7 @@ public class StatusBar extends JPanel {
private final JLabel modelInfo; private final JLabel modelInfo;
private Telnet3270Client client; private Telnet3270Client client;
private TerminalPanel terminalPanel;
// OIA colors // OIA colors
private static final Color OIA_BG = new Color(20, 20, 20); private static final Color OIA_BG = new Color(20, 20, 20);
@@ -44,6 +45,19 @@ public class StatusBar extends JPanel {
modelInfo = createLabel("", oiaFont, OIA_DIM); modelInfo = createLabel("", oiaFont, OIA_DIM);
cursorPosition = createLabel("001/001", oiaFont, OIA_FG); cursorPosition = createLabel("001/001", oiaFont, OIA_FG);
JButton lpButton = new JButton("LightPen: OFF");
lpButton.setFont(oiaFont);
lpButton.setForeground(OIA_FG);
lpButton.setBackground(OIA_BG);
lpButton.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
lpButton.setFocusable(false);
lpButton.addActionListener(e -> {
if (terminalPanel != null) {
terminalPanel.toggleLightPen();
lpButton.setText("LightPen: " + (terminalPanel.isLightPenMode() ? "ON" : "OFF"));
}
});
add(Box.createHorizontalStrut(6)); add(Box.createHorizontalStrut(6));
add(connectionStatus); add(connectionStatus);
add(Box.createHorizontalStrut(10)); add(Box.createHorizontalStrut(10));
@@ -52,6 +66,8 @@ public class StatusBar extends JPanel {
add(luName); add(luName);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
add(lockStatus); add(lockStatus);
add(Box.createHorizontalStrut(12));
add(lpButton);
add(Box.createHorizontalGlue()); add(Box.createHorizontalGlue());
add(modelInfo); add(modelInfo);
add(Box.createHorizontalStrut(12)); add(Box.createHorizontalStrut(12));
@@ -66,8 +82,9 @@ public class StatusBar extends JPanel {
return label; return label;
} }
public void setClient(Telnet3270Client client) { public void setClient(Telnet3270Client client, TerminalPanel terminalPanel) {
this.client = client; this.client = client;
this.terminalPanel = terminalPanel;
} }
public void updateStatus() { public void updateStatus() {
@@ -62,6 +62,9 @@ public class TerminalPanel extends JPanel {
private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100); private static final Color SELECTION_COLOR = new Color(75, 110, 175, 100);
// ========== Resize guard ========== // ========== Resize guard ==========
private boolean lightPenMode = false;
private long lastGraphicsUpdateCount = -1;
private java.awt.image.BufferedImage cachedGraphicsImage = null;
private boolean resizeGuard = false; private boolean resizeGuard = false;
/** /**
@@ -70,7 +73,7 @@ public class TerminalPanel extends JPanel {
*/ */
private int getRenderOffsetX() { private int getRenderOffsetX() {
int termCols = 80; int termCols = 80;
if (client != null) termCols = client.getScreenBuffer().getCols(); if (client != null) termCols = client.getScreenBuffer().getDisplayCols();
int gridW = termCols * cellWidth; int gridW = termCols * cellWidth;
int extra = getWidth() - gridW; int extra = getWidth() - gridW;
return Math.max(padding, extra / 2); return Math.max(padding, extra / 2);
@@ -78,10 +81,11 @@ public class TerminalPanel extends JPanel {
/** /**
* Compute the vertical render offset to center the grid within the panel. * Compute the vertical render offset to center the grid within the panel.
* Must use getDisplayRows() to match paintComponent's iteration.
*/ */
private int getRenderOffsetY() { private int getRenderOffsetY() {
int termRows = 24; int termRows = 24;
if (client != null) termRows = client.getScreenBuffer().getRows(); if (client != null) termRows = client.getScreenBuffer().getDisplayRows();
int gridH = termRows * cellHeight; int gridH = termRows * cellHeight;
int extra = getHeight() - gridH; int extra = getHeight() - gridH;
return Math.max(padding, extra / 2); return Math.max(padding, extra / 2);
@@ -148,10 +152,12 @@ public class TerminalPanel extends JPanel {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
int ox = getRenderOffsetX(); int ox = getRenderOffsetX();
int oy = getRenderOffsetY(); int oy = getRenderOffsetY();
int displayCols = sb.getDisplayCols();
int displayRows = sb.getDisplayRows();
int col = (e.getX() - ox) / cellWidth; int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight; int row = (e.getY() - oy) / cellHeight;
col = Math.max(0, Math.min(col, sb.getCols() - 1)); col = Math.max(0, Math.min(col, displayCols - 1));
row = Math.max(0, Math.min(row, sb.getRows() - 1)); row = Math.max(0, Math.min(row, displayRows - 1));
// Start selection // Start selection
selectionStartRow = row; selectionStartRow = row;
@@ -171,8 +177,8 @@ public class TerminalPanel extends JPanel {
int oy = getRenderOffsetY(); int oy = getRenderOffsetY();
int col = (e.getX() - ox) / cellWidth; int col = (e.getX() - ox) / cellWidth;
int row = (e.getY() - oy) / cellHeight; int row = (e.getY() - oy) / cellHeight;
col = Math.max(0, Math.min(col, sb.getCols() - 1)); col = Math.max(0, Math.min(col, sb.getDisplayCols() - 1));
row = Math.max(0, Math.min(row, sb.getRows() - 1)); row = Math.max(0, Math.min(row, sb.getDisplayRows() - 1));
selectionEndRow = row; selectionEndRow = row;
selectionEndCol = col; selectionEndCol = col;
@@ -180,6 +186,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 @Override
public void mouseReleased(MouseEvent e) { public void mouseReleased(MouseEvent e) {
if (isDragging) { if (isDragging) {
@@ -188,8 +210,44 @@ public class TerminalPanel extends JPanel {
if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) { if (selectionStartRow == selectionEndRow && selectionStartCol == selectionEndCol) {
if (client != null && client.getConnectionState().isFullSession()) { if (client != null && client.getConnectionState().isFullSession()) {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
sb.setCursorAddress(selectionStartRow * sb.getCols() + selectionStartCol); int displayCols = sb.getDisplayCols();
int newAddr = selectionStartRow * displayCols + selectionStartCol;
sb.setCursorAddress(newAddr);
clearSelection(); clearSelection();
if (lightPenMode) {
if (client.getInputProcessor().lightPenSelect(newAddr)) {
refreshScreen();
return;
}
}
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;
} }
} }
repaint(); repaint();
@@ -390,8 +448,8 @@ public class TerminalPanel extends JPanel {
int termRows = 24; int termRows = 24;
if (client != null) { if (client != null) {
ScreenBuffer sb = client.getScreenBuffer(); ScreenBuffer sb = client.getScreenBuffer();
termCols = sb.getCols(); termCols = sb.getDisplayCols();
termRows = sb.getRows(); termRows = sb.getDisplayRows();
} }
// Use minimal padding for the fit calculation // Use minimal padding for the fit calculation
@@ -513,6 +571,7 @@ public class TerminalPanel extends JPanel {
int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); int shortcutMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, shortcutMask), "j3270-COPY");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE"); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, shortcutMask), "j3270-PASTE");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_L, java.awt.event.InputEvent.ALT_DOWN_MASK), "j3270-LIGHTPEN");
// Create actions for all bound keys // Create actions for all bound keys
am.put("j3270-ENTER", createAction(this::handleEnter)); am.put("j3270-ENTER", createAction(this::handleEnter));
@@ -541,6 +600,7 @@ public class TerminalPanel extends JPanel {
// Copy/Paste actions // Copy/Paste actions
am.put("j3270-COPY", createAction(this::copySelection)); am.put("j3270-COPY", createAction(this::copySelection));
am.put("j3270-PASTE", createAction(this::pasteClipboard)); am.put("j3270-PASTE", createAction(this::pasteClipboard));
am.put("j3270-LIGHTPEN", createAction(this::toggleLightPen));
for (int i = 1; i <= 24; i++) { for (int i = 1; i <= 24; i++) {
final int pf = i; final int pf = i;
@@ -723,6 +783,9 @@ public class TerminalPanel extends JPanel {
} }
private void refreshScreen() { private void refreshScreen() {
if (client != null) {
client.getScreenBuffer().updateDisplaySnapshot();
}
repaint(); repaint();
// Notify parent to update status bar too // Notify parent to update status bar too
Container parent = getParent(); Container parent = getParent();
@@ -810,8 +873,8 @@ public class TerminalPanel extends JPanel {
// Use the CURRENT screen dimensions (not max) to eliminate extra space. // Use the CURRENT screen dimensions (not max) to eliminate extra space.
// When the host switches to alternate screen, onScreenSizeChanged fires // When the host switches to alternate screen, onScreenSizeChanged fires
// and the frame re-packs. // and the frame re-packs.
int displayCols = sb.getCols(); int displayCols = sb.getDisplayCols();
int displayRows = sb.getRows(); int displayRows = sb.getDisplayRows();
return new Dimension(displayCols * cellWidth + padding * 2, return new Dimension(displayCols * cellWidth + padding * 2,
displayRows * cellHeight + padding * 2); displayRows * cellHeight + padding * 2);
} }
@@ -844,6 +907,24 @@ public class TerminalPanel extends JPanel {
int cols = sb.getDisplayCols(); int cols = sb.getDisplayCols();
boolean isColorModel = client.getConfig().getModel().isColor(); 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) {
long currentUpdateCount = client.getGraphicsPlane().getUpdateCount();
if (cachedGraphicsImage == null || currentUpdateCount != lastGraphicsUpdateCount || cachedGraphicsImage.getWidth() != gWidth || cachedGraphicsImage.getHeight() != gHeight) {
cachedGraphicsImage = new java.awt.image.BufferedImage(gWidth, gHeight, java.awt.image.BufferedImage.TYPE_INT_ARGB);
cachedGraphicsImage.setRGB(0, 0, gWidth, gHeight, rgb, 0, gWidth);
lastGraphicsUpdateCount = currentUpdateCount;
}
g2.drawImage(cachedGraphicsImage, ox, oy, gridW, gridH, null);
}
}
// Track current field attribute for monochrome color decisions // Track current field attribute for monochrome color decisions
byte currentFA = 0; byte currentFA = 0;
ExtendedAttribute currentFieldEa = null; ExtendedAttribute currentFieldEa = null;
@@ -935,7 +1016,8 @@ public class TerminalPanel extends JPanel {
if (cs >= 0x40 && client.getProgramSymbolManager() != null) { if (cs >= 0x40 && client.getProgramSymbolManager() != null) {
org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF); org.lib3270j.graphics.ProgramSymbolSet.SymbolSlot slot = client.getProgramSymbolManager().getSymbol(cs, ea.ec & 0xFF);
if (slot != null) { 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) { if (img != null) {
g2.drawImage(img, x, y, null); g2.drawImage(img, x, y, null);
drawnAsPs = true; drawnAsPs = true;
@@ -972,20 +1054,28 @@ public class TerminalPanel extends JPanel {
} }
} }
// Draw Vector Graphics Plane overlay if present // Draw Graphic Cursor (Light-Pen / interactive graphics pointer) if active
if (client.getGraphicsPlane() != null && client.getGraphicsPlane().hasContent()) { if (client.getGocaDecoder() != null && client.getGocaDecoder().isGraphicsCursorActive() && client.getGraphicsPlane() != null) {
int gocaX = client.getGocaDecoder().getGraphicCursorX();
int gocaY = client.getGocaDecoder().getGraphicCursorY();
int canvasPx = client.getGraphicsPlane().mapX(gocaX);
int canvasPy = client.getGraphicsPlane().mapY(gocaY);
int gridW = cols * cellWidth; int gridW = cols * cellWidth;
int gridH = rows * cellHeight; int gridH = rows * cellHeight;
client.getGraphicsPlane().resize(gridW, gridH); int gWidth = client.getGraphicsPlane().getCanvasWidth();
int[] rgb = client.getGraphicsPlane().getRgbBuffer(); int gHeight = client.getGraphicsPlane().getCanvasHeight();
if (rgb != null) { int px = ox + (gWidth > 0 ? (int)Math.round((double)canvasPx * gridW / gWidth) : canvasPx);
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(gridW, gridH, java.awt.image.BufferedImage.TYPE_INT_ARGB); int py = oy + (gHeight > 0 ? (int)Math.round((double)canvasPy * gridH / gHeight) : canvasPy);
img.setRGB(0, 0, gridW, gridH, rgb, 0, gridW);
g2.drawImage(img, ox, oy, gridW, gridH, null); 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()) { if (cursorVisible && client.getConnectionState().isFullSession()) {
int curAddr = sb.getDisplayCursorAddress(); int curAddr = sb.getDisplayCursorAddress();
int curRow = curAddr / cols; int curRow = curAddr / cols;
@@ -998,6 +1088,13 @@ public class TerminalPanel extends JPanel {
g2.fillRect(cx, cy, cellWidth, cellHeight); g2.fillRect(cx, cy, cellWidth, cellHeight);
g2.setPaintMode(); g2.setPaintMode();
} }
// Draw Light Pen mode indicator
if (lightPenMode) {
g2.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
g2.setColor(new Color(50, 255, 50));
g2.drawString("LP", ox + 2, oy + rows * cellHeight + 14);
}
} }
private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) { private Color getColorForAttribute(ExtendedAttribute ea, ExtendedAttribute currentFieldEa, byte currentFA) {
@@ -1038,4 +1135,13 @@ public class TerminalPanel extends JPanel {
if (blinkTimer != null) if (blinkTimer != null)
blinkTimer.stop(); blinkTimer.stop();
} }
public void toggleLightPen() {
this.lightPenMode = !this.lightPenMode;
System.out.println("Light Pen mode: " + (this.lightPenMode ? "ON" : "OFF"));
repaint();
}
public boolean isLightPenMode() {
return this.lightPenMode;
}
} }
@@ -26,7 +26,7 @@ public class DiagnosticClient {
String host = args.length >= 1 ? args[0] : "192.168.0.30"; String host = args.length >= 1 ? args[0] : "192.168.0.30";
int port = args.length >= 2 ? Integer.parseInt(args[1]) : 3270; 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 = new Telnet3270Client(config);
client.addConnectionListener(new ConnectionListener() { client.addConnectionListener(new ConnectionListener() {
@@ -54,6 +54,7 @@ public class Telnet3270Client {
dsProcessor.setOutputSender(fsm::send3270Data); dsProcessor.setOutputSender(fsm::send3270Data);
dsProcessor.setInputProcessor(inputProcessor); dsProcessor.setInputProcessor(inputProcessor);
inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane()); inputProcessor.setGraphicsPlane(dsProcessor.getGraphicsPlane());
inputProcessor.setGocaDecoder(dsProcessor.getGocaDecoder());
} }
/** /**
@@ -89,6 +89,10 @@ public class DataStreamProcessor {
this.inputProcessor = inputProcessor; this.inputProcessor = inputProcessor;
} }
public org.lib3270j.input.InputProcessor getInputProcessor() {
return inputProcessor;
}
public void addScreenUpdateListener(ScreenUpdateListener l) { public void addScreenUpdateListener(ScreenUpdateListener l) {
screenListeners.add(l); screenListeners.add(l);
} }
@@ -175,6 +179,7 @@ public class DataStreamProcessor {
case CMD_WSF: case CMD_WSF:
case SNA_CMD_WSF: case SNA_CMD_WSF:
processWriteStructuredField(data, offset, length); processWriteStructuredField(data, offset, length);
keyboardRestore = true;
break; break;
case CMD_NOP: case CMD_NOP:
log.info(">>> NOP command"); log.info(">>> NOP command");
@@ -190,6 +195,10 @@ public class DataStreamProcessor {
screen.updateDisplaySnapshot(); screen.updateDisplaySnapshot();
} }
if (keyboardRestore && inputProcessor != null) {
inputProcessor.setKeyboardLocked(false);
}
// Debug: dump non-empty screen lines // 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)) { if (log.isLoggable(Level.FINE) && (cmd == SNA_CMD_EWA || cmd == CMD_EWA || cmd == SNA_CMD_EW || cmd == CMD_EW)) {
int r = screen.getRows(); int r = screen.getRows();
@@ -782,22 +791,27 @@ public class DataStreamProcessor {
case SF_READ_PART: case SF_READ_PART:
processSFReadPartition(data, pos, fieldLen); processSFReadPartition(data, pos, fieldLen);
break; break;
case SF_ERASE_RESET: case SF_ERASE_RESET: {
if (fieldLen >= 4) { boolean alt = (fieldLen >= 4) && ((data[pos + 3] & 0xFF) == SF_ER_ALT);
boolean alt = (data[pos + 3] & 0xFF) == SF_ER_ALT; screen.erase(alt);
screen.erase(alt); graphicsPlane.clear();
graphicsPlane.clear(); gocaDecoder.resetDefaults();
notifyScreenSizeChanged(); notifyScreenSizeChanged();
}
break; break;
}
case SF_SET_REPLY_MODE: case SF_SET_REPLY_MODE:
if (fieldLen >= 5) { if (fieldLen >= 5) {
screen.setReplyMode((byte) (data[pos + 4] & 0xFF)); screen.setReplyMode((byte) (data[pos + 4] & 0xFF));
} }
break; break;
case SF_CREATE_PART: 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(); graphicsPlane.clear();
gocaDecoder.resetDefaults();
break; break;
case SF_OUTBOUND_DS: case SF_OUTBOUND_DS:
if (fieldLen > 5) { if (fieldLen > 5) {
@@ -879,6 +893,13 @@ public class DataStreamProcessor {
} }
break; 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_OBJDATA: // 0x85: Graphics Object Data / GOCA
case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics case org.lib3270j.graphics.GocaConstants.SF_3270_G: // 0x20: 3270 Graphics
if (fieldLen > 3) { if (fieldLen > 3) {
@@ -915,6 +936,8 @@ public class DataStreamProcessor {
switch (type) { switch (type) {
case SF_RP_QUERY: case SF_RP_QUERY:
log.info("ReadPartition Query — sending all query replies"); log.info("ReadPartition Query — sending all query replies");
graphicsPlane.clear();
gocaDecoder.resetDefaults();
sendAllQueryReplies(); sendAllQueryReplies();
break; break;
case SF_RP_QLIST: case SF_RP_QLIST:
@@ -197,7 +197,7 @@ public class QueryReplyBuilder {
case QR_RPQ_NAMES: case QR_RPQ_NAMES:
case QR_RPQNAMES: case QR_RPQNAMES:
if (graphicsMode.isVectorGraphicsEnabled()) { if (graphicsMode.isVectorGraphicsEnabled()) {
appendQueryReply(out, QR_RPQ_NAMES, buildRpqNames()); appendQueryReply(out, code, buildRpqNames());
} else { } else {
appendQueryReply(out, QR_NULL, new byte[0]); appendQueryReply(out, QR_NULL, new byte[0]);
} }
@@ -54,6 +54,7 @@ public final class GocaConstants {
public static final int G_GSVW = 0x27; // Set Viewing Window 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_GSPT = 0x28; // Set Pattern Symbol
public static final int G_GSMT = 0x29; // Set Marker Symbol / Type 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_GSCH = 0x33; // Set Character Cell
public static final int G_GSCA = 0x34; // Set Character Angle public static final int G_GSCA = 0x34; // Set Character Angle
public static final int G_GSCR = 0x35; // Set Character Shear 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. * Returns Green (0xFF00FF00) if the index is out of range.
*/ */
public static int getGocaColorArgb(int colorIndex) { public static int getGocaColorArgb(int colorIndex) {
if (colorIndex == 0xFF) {
return GOCA_COLORS[7];
}
if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) { if (colorIndex >= 0 && colorIndex < GOCA_COLORS.length) {
return GOCA_COLORS[colorIndex]; return GOCA_COLORS[colorIndex];
} }
@@ -20,6 +20,8 @@ public class GocaDecoder {
private int curX = 0; private int curX = 0;
private int curY = 0; private int curY = 0;
private int curColor = GocaConstants.GOCA_COLORS[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 lineType = GocaConstants.LT_SOLID;
private int lineWidth = GocaConstants.LW_NORMAL; private int lineWidth = GocaConstants.LW_NORMAL;
private int markerType = GocaConstants.MK_PLUS; private int markerType = GocaConstants.MK_PLUS;
@@ -42,6 +44,7 @@ public class GocaDecoder {
// Area accumulation // Area accumulation
private boolean inArea = false; private boolean inArea = false;
private boolean areaDrawBoundary = true; private boolean areaDrawBoundary = true;
private boolean areaFill = true;
private final List<Integer> areaPointsX = new ArrayList<>(); private final List<Integer> areaPointsX = new ArrayList<>();
private final List<Integer> areaPointsY = new ArrayList<>(); private final List<Integer> areaPointsY = new ArrayList<>();
@@ -53,6 +56,18 @@ public class GocaDecoder {
private int imgHeight = 0; private int imgHeight = 0;
private final List<Byte> imgBuffer = new ArrayList<>(); private final List<Byte> imgBuffer = new ArrayList<>();
// Segment Store for retained graphics / segment calling (G_GCALL 0x2A)
private final java.util.Map<Integer, byte[]> segmentStore = new java.util.HashMap<>();
private final java.util.Map<Integer, Integer> segmentChainMap = new java.util.HashMap<>();
private final java.util.List<Integer> segmentOrderList = new java.util.ArrayList<>();
private final java.util.Set<Integer> 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) { public GocaDecoder(GraphicsPlane plane) {
this.plane = plane; this.plane = plane;
} }
@@ -73,14 +88,51 @@ public class GocaDecoder {
return curY; 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() { public synchronized void resetDefaults() {
curX = 0; curX = 0;
curY = 0; curY = 0;
graphicsCursorActive = false;
graphicCursorX = 0;
graphicCursorY = 0;
segmentStore.clear();
segmentChainMap.clear();
segmentOrderList.clear();
chainedTargets.clear();
resetAttributes(); resetAttributes();
} }
public synchronized void resetAttributes() { public synchronized void resetAttributes() {
curColor = getColor(0); curColor = getColor(0);
bgMix = 2; // BMX_OVERPAINT
bgColor = GocaConstants.GOCA_COLORS[8]; // Black
lineType = GocaConstants.LT_SOLID; lineType = GocaConstants.LT_SOLID;
lineWidth = GocaConstants.LW_NORMAL; lineWidth = GocaConstants.LW_NORMAL;
markerType = GocaConstants.MK_PLUS; markerType = GocaConstants.MK_PLUS;
@@ -92,6 +144,8 @@ public class GocaDecoder {
charAngle = 0.0; charAngle = 0.0;
charSet = 0; charSet = 0;
inArea = false; inArea = false;
areaDrawBoundary = true;
areaFill = true;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
inImage = false; inImage = false;
@@ -115,14 +169,14 @@ public class GocaDecoder {
order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW || order == GocaConstants.G_GSBMX || order == GocaConstants.G_GSFLW ||
order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW || order == GocaConstants.G_GSLT || order == GocaConstants.G_GSLW ||
order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT || order == GocaConstants.G_GSMS || order == GocaConstants.G_GSPT ||
order == GocaConstants.G_GSMT || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMT || order == GocaConstants.G_GSMCEL ||
order == GocaConstants.G_GSMP || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCS || order == GocaConstants.G_GSMP ||
order == GocaConstants.G_GSCC || order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GSCD || order == GocaConstants.G_GSCC ||
order == GocaConstants.G_GBAR) { order == GocaConstants.G_GSMS_SET || order == GocaConstants.G_GBAR) {
return 2; return 2;
} }
if (order == GocaConstants.G_GSAP || order == GocaConstants.G_GBIMG || order == 0x91) { if (order == GocaConstants.G_GCALL) { // Call Segment (0x2A <32-bit segment ID>)
return 10; return 5;
} }
if (idx + 1 >= end) { if (idx + 1 >= end) {
return -1; return -1;
@@ -130,6 +184,61 @@ public class GocaDecoder {
return (data[idx + 1] & 0xFF) + 2; 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. * Decodes a stream of GOCA drawing orders.
*/ */
@@ -155,6 +264,15 @@ public class GocaDecoder {
end = offset + length; 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) { while (idx < end) {
int order = inputData[idx] & 0xFF; int order = inputData[idx] & 0xFF;
int orderLen = getOrderLength(inputData, idx, end); int orderLen = getOrderLength(inputData, idx, end);
@@ -225,6 +343,22 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; 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) case GocaConstants.G_GSCA: { // Set Character Angle (0x34)
if (payloadLen >= 4 && idx + 5 < end) { if (payloadLen >= 4 && idx + 5 < end) {
int ax = readCoord(inputData, idx + 2); int ax = readCoord(inputData, idx + 2);
@@ -312,7 +446,6 @@ public class GocaDecoder {
} }
case 0x04: case 0x04:
case GocaConstants.G_GSMX: case GocaConstants.G_GSMX:
case GocaConstants.G_GSBMX:
case GocaConstants.G_GSFLW: case GocaConstants.G_GSFLW:
case GocaConstants.G_GSMP: case GocaConstants.G_GSMP:
case GocaConstants.G_GSCC: case GocaConstants.G_GSCC:
@@ -321,9 +454,17 @@ public class GocaDecoder {
idx += orderLen; idx += orderLen;
break; break;
} }
case GocaConstants.G_GSBMX: { // Set Background Mix (0x0D)
bgMix = inputData[idx + 1] & 0xFF;
idx += orderLen;
break;
}
case GocaConstants.G_GBAR: { // Begin Area (0x68) case GocaConstants.G_GBAR: { // Begin Area (0x68)
int flags = inputData[idx + 1] & 0xFF; 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; idx += orderLen;
break; break;
} }
@@ -426,7 +567,7 @@ public class GocaDecoder {
break; break;
} }
case GocaConstants.G_GBIMG: { // Begin Image (0xD1) 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 x = readCoord(inputData, idx + 2);
int y = readCoord(inputData, idx + 4); int y = readCoord(inputData, idx + 4);
int w = readCoord(inputData, idx + 6); int w = readCoord(inputData, idx + 6);
@@ -468,41 +609,57 @@ public class GocaDecoder {
int order = data[idx] & 0xFF; int order = data[idx] & 0xFF;
switch (order) { switch (order) {
case GocaConstants.P_NOP1: case GocaConstants.P_NOP1: {
case GocaConstants.P_ATTCUR: idx += (idx + 1 < end && data[idx + 1] == 0) ? 2 : 1;
case GocaConstants.P_DETCUR: 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: { case GocaConstants.P_STOPDR: {
idx++; idx += 2;
break; break;
} }
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
plane.clear(); plane.clear();
resetDefaults(); resetDefaults();
idx++; idx += 2;
break; break;
} }
case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12) case GocaConstants.P_BEGPROC: { // 0x30: Begin Procedure (length 12)
int len = 12; idx += 12;
if (idx + 1 < end && data[idx + 1] != 0) {
len = (data[idx + 1] & 0xFF) + 2;
}
idx += len;
break; 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) { if (idx + 1 < end) {
int len = data[idx + 1] & 0xFF; int len = data[idx + 1] & 0xFF;
if (idx + 2 + len <= end) {
decodeStream(data, idx + 2, len);
}
idx += 2 + len; idx += 2 + len;
} else { } else {
idx++; idx++;
} }
break; break;
} }
case GocaConstants.P_COMT: case GocaConstants.P_COMT: {
case GocaConstants.P_SETCUR: {
if (idx + 1 < end) { if (idx + 1 < end) {
int len = data[idx + 1] & 0xFF; int len = data[idx + 1] & 0xFF;
idx += 2 + len; 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.inArea = true;
this.areaDrawBoundary = drawBoundary; this.areaDrawBoundary = drawBoundary;
this.areaFill = fill;
this.fillColor = this.curColor; this.fillColor = this.curColor;
this.areaPointsX.clear(); this.areaPointsX.clear();
this.areaPointsY.clear(); this.areaPointsY.clear();
@@ -547,7 +705,8 @@ public class GocaDecoder {
py[i] = plane.mapY(areaPointsY.get(i)); 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; inArea = false;
areaPointsX.clear(); areaPointsX.clear();
areaPointsY.clear(); areaPointsY.clear();
@@ -555,8 +714,11 @@ public class GocaDecoder {
private void addAreaPoint(int x, int y) { private void addAreaPoint(int x, int y) {
if (inArea) { if (inArea) {
areaPointsX.add(x); int sz = areaPointsX.size();
areaPointsY.add(y); if (sz == 0 || areaPointsX.get(sz - 1) != x || areaPointsY.get(sz - 1) != y) {
areaPointsX.add(x);
areaPointsY.add(y);
}
} }
} }
@@ -667,39 +829,51 @@ public class GocaDecoder {
private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) { private void processArc(byte[] data, int off, int len, boolean fromCurPos, boolean isFull) {
int pos = off; int pos = off;
int startX = curX; int centerX = curX;
int startY = curY; int centerY = curY;
if (!fromCurPos && pos + 4 <= off + len) { if (!fromCurPos && pos + 4 <= off + len) {
startX = readCoord(data, pos); centerX = readCoord(data, pos);
startY = readCoord(data, pos + 2); centerY = readCoord(data, pos + 2);
pos += 4; pos += 4;
} }
double multiplier = 1.0; double sweepFraction = 1.0;
if (pos + 2 <= off + len) { if (!isFull && pos + 2 <= off + len) {
multiplier = (data[pos] & 0xFF) + ((data[pos + 1] & 0xFF) / 255.0); byte intPart = data[pos];
if (multiplier == 0.0) multiplier = 1.0; double fracPart = (data[pos + 1] & 0xFF) / 256.0;
sweepFraction = intPart + (intPart >= 0 ? fracPart : -fracPart);
} }
int dxP = Math.abs(arcParamP - arcParamR); int pxCenter = plane.mapX(centerX);
int dyQ = Math.abs(arcParamQ - arcParamS); int pyCenter = plane.mapY(centerY);
if (dxP == 0) dxP = 10; int pxCur = plane.mapX(curX);
if (dyQ == 0) dyQ = 10; int pyCur = plane.mapY(curY);
int rxVirtual = (int) (dxP * multiplier); double radius = Math.sqrt(Math.pow(pxCur - pxCenter, 2) + Math.pow(pyCur - pyCenter, 2));
int ryVirtual = (int) (dyQ * multiplier); int rx = (int) Math.round(radius);
int ry = rx;
if (rx <= 0) {
rx = 10;
ry = 10;
}
int rx = Math.abs(plane.mapX(rxVirtual) - plane.mapX(0)); double startAngleRad = Math.atan2(pyCenter - pyCur, pxCur - pxCenter);
int ry = Math.abs(plane.mapY(ryVirtual) - plane.mapY(0)); double startAngleDeg = Math.toDegrees(startAngleRad);
if (rx <= 0) rx = 10; if (startAngleDeg < 0) startAngleDeg += 360.0;
if (ry <= 0) ry = 10;
plane.drawArc(plane.mapX(startX), plane.mapY(startY), rx, ry, 0.0, 360.0, double sweepAngleDeg = isFull ? 360.0 : (sweepFraction * 360.0);
plane.drawArc(pxCenter, pyCenter, rx, ry, startAngleDeg, sweepAngleDeg,
curColor, lineType, lineWidth, isFull); curColor, lineType, lineWidth, isFull);
curX = startX; // Arc ends at the end of the sweep
curY = startY; double endAngleRad = Math.toRadians(startAngleDeg + sweepAngleDeg);
int pxEnd = (int) Math.round(pxCenter + rx * Math.cos(endAngleRad));
int pyEnd = (int) Math.round(pyCenter - ry * Math.sin(endAngleRad));
curX = plane.unmapX(pxEnd);
curY = plane.unmapY(pyEnd);
} }
private void processFillet(byte[] data, int off, int len, boolean fromCurPos) { private void processFillet(byte[] data, int off, int len, boolean fromCurPos) {
@@ -770,8 +944,9 @@ public class GocaDecoder {
int textLen = end - pos; int textLen = end - pos;
if (textLen <= 0) return; if (textLen <= 0) return;
int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 12; // IBM 3279 vector graphics base cell is 9x12
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 20; int cw = charWidth > 0 ? (int) Math.round((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10;
int ch = charHeight > 0 ? (int) Math.round((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14;
if (charSet != 0 && programSymbolManager != null) { if (charSet != 0 && programSymbolManager != null) {
for (int i = 0; i < textLen; i++) { for (int i = 0; i < textLen; i++) {
@@ -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;
}
}
@@ -40,6 +40,7 @@ public class GraphicsPlane {
private int canvasHeight = 600; private int canvasHeight = 600;
private int[] rgbBuffer; private int[] rgbBuffer;
private boolean hasContent = false; private boolean hasContent = false;
private long updateCount = 0;
private int screenCols = 80; private int screenCols = 80;
private int screenRows = 24; private int screenRows = 24;
@@ -79,6 +80,11 @@ public class GraphicsPlane {
Arrays.fill(rgbBuffer, 0); Arrays.fill(rgbBuffer, 0);
} }
hasContent = false; hasContent = false;
updateCount++;
}
public synchronized long getUpdateCount() {
return updateCount;
} }
public synchronized boolean hasContent() { public synchronized boolean hasContent() {
@@ -97,9 +103,14 @@ public class GraphicsPlane {
return canvasHeight; return canvasHeight;
} }
public void setScreenDimensions(int cols, int rows) { public synchronized void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80; this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24; 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() { public int getScreenCols() {
@@ -124,12 +135,32 @@ public class GraphicsPlane {
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down). * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
*/ */
public int mapY(int gocaY) { public int mapY(int gocaY) {
int nominalHeight = screenRows * 16; int nominalHeight = screenRows * 12;
int yMax = (nominalHeight - 1) / 2; int yMax = (nominalHeight - 1) / 2;
int ny = yMax - gocaY; int ny = yMax - gocaY;
return (int) Math.round((double) ny * canvasHeight / nominalHeight); 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). * Safely plots a pixel at (x, y).
*/ */
@@ -137,6 +168,7 @@ public class GraphicsPlane {
if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) { if (x >= 0 && x < canvasWidth && y >= 0 && y < canvasHeight) {
rgbBuffer[y * canvasWidth + x] = colorArgb; rgbBuffer[y * canvasWidth + x] = colorArgb;
hasContent = true; hasContent = true;
updateCount++;
} }
} }
@@ -178,6 +210,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
private boolean shouldPlotLinePixel(int step, int lineType) { private boolean shouldPlotLinePixel(int step, int lineType) {
@@ -241,6 +274,7 @@ public class GraphicsPlane {
prevY = nextY; prevY = nextY;
} }
hasContent = true; hasContent = true;
updateCount++;
} }
/** /**
@@ -279,6 +313,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
/** /**
@@ -286,9 +321,19 @@ public class GraphicsPlane {
*/ */
public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern, public synchronized void fillArea(int[] px, int[] py, int numPoints, int fillColorArgb, int pattern,
boolean drawBoundary, int boundaryColorArgb, int lineType, int lineWidth) { 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; if (px == null || py == null || numPoints < 3) return;
int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF; int fill = (fillColorArgb != 0) ? fillColorArgb : 0xFFFFFFFF;
int bg = bgColorArgb;
if (pattern != GocaConstants.PT_EMPTY) { if (pattern != GocaConstants.PT_EMPTY) {
// Find polygon vertical bounds // Find polygon vertical bounds
@@ -323,12 +368,14 @@ public class GraphicsPlane {
int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1)); int rightX = Math.min(canvasWidth - 1, nodeX.get(i + 1));
for (int x = leftX; x <= rightX; x++) { 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); setPixel(x, y, fill);
} else { } else {
int b = patRows[y & 7] & 0xFF; int b = patRows[y & 7] & 0xFF;
if (((b >> (7 - (x & 7))) & 1) != 0) { if (((b >> (7 - (x & 7))) & 1) != 0) {
setPixel(x, y, fill); setPixel(x, y, fill);
} else if (bgMix != 0) { // BMX_OVERPAINT (opaque background)
setPixel(x, y, bg);
} }
} }
} }
@@ -343,6 +390,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
/** /**
@@ -408,6 +456,7 @@ public class GraphicsPlane {
break; break;
} }
hasContent = true; hasContent = true;
updateCount++;
} }
private static final int[] VSS_OFFSETS = new int[256]; private static final int[] VSS_OFFSETS = new int[256];
@@ -456,6 +505,7 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
private void drawVssChar(int x, int y, char c, int color, int cw, int ch) { private void drawVssChar(int x, int y, char c, int color, int cw, int ch) {
@@ -506,13 +556,14 @@ public class GraphicsPlane {
public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) { public synchronized void drawImage(int x, int y, int width, int height, byte[] imageData, int fgColorArgb) {
if (imageData == null || width <= 0 || height <= 0) return; if (imageData == null || width <= 0 || height <= 0) return;
int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF; int fgColor = (fgColorArgb != 0) ? fgColorArgb : 0xFFFFFFFF;
int bytesPerRow = (width + 7) / 8;
for (int row = 0; row < height; row++) { for (int row = 0; row < height; row++) {
int rowOffset = row * bytesPerRow;
for (int col = 0; col < width; col++) { for (int col = 0; col < width; col++) {
int bitIndex = row * width + col; int byteIdx = rowOffset + (col / 8);
int byteIdx = bitIndex / 8;
if (byteIdx < imageData.length) { if (byteIdx < imageData.length) {
boolean bit = ((imageData[byteIdx] >> (7 - (bitIndex % 8))) & 1) != 0; boolean bit = ((imageData[byteIdx] >> (7 - (col % 8))) & 1) != 0;
if (bit) { if (bit) {
setPixel(x + col, y + row, fgColor); setPixel(x + col, y + row, fgColor);
} }
@@ -520,5 +571,6 @@ public class GraphicsPlane {
} }
} }
hasContent = true; hasContent = true;
updateCount++;
} }
} }
@@ -87,8 +87,11 @@ public class ProgramSymbolManager {
} }
ProgramSymbolSet set = lcidMap[lcid]; ProgramSymbolSet set = lcidMap[lcid];
if (set == null) { if (set == null) {
for (ProgramSymbolSet s : sets) {
if (s != null && s.getLcid() == lcid) return s;
}
for (ProgramSymbolSet s : stagingSets) { for (ProgramSymbolSet s : stagingSets) {
if (s.getLcid() == lcid) return s; if (s != null && s.getLcid() == lcid) return s;
} }
} }
return set; return set;
@@ -98,18 +101,7 @@ public class ProgramSymbolManager {
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE). * Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
*/ */
public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) { public ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
if (lcid <= 0 || lcid >= 256) { ProgramSymbolSet set = getSymbolSet(lcid);
return null;
}
ProgramSymbolSet set = lcidMap[lcid];
if (set == null) {
for (ProgramSymbolSet s : stagingSets) {
if (s.getLcid() == lcid) {
set = s;
break;
}
}
}
if (set == null) { if (set == null) {
return null; return null;
} }
@@ -153,7 +145,7 @@ public class ProgramSymbolManager {
} }
boolean isTriplePlane = (rws >= 4 && rws <= 7); boolean isTriplePlane = (rws >= 4 && rws <= 7);
ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex]; ProgramSymbolSet set = sets[setIndex];
int extHeaderLen = 0; int extHeaderLen = 0;
int cellWidth = defaultCellWidth; int cellWidth = defaultCellWidth;
@@ -176,7 +168,7 @@ public class ProgramSymbolManager {
} }
set.setLcid(lcid); set.setLcid(lcid);
if (!isTriplePlane && lcid > 0 && lcid < 256) { if (lcid > 0 && lcid < 256) {
lcidMap[lcid] = set; lcidMap[lcid] = set;
} }
@@ -32,11 +32,20 @@ public class InputProcessor {
} }
private org.lib3270j.graphics.GraphicsPlane graphicsPlane; private org.lib3270j.graphics.GraphicsPlane graphicsPlane;
private org.lib3270j.graphics.GocaDecoder gocaDecoder;
public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) { public void setGraphicsPlane(org.lib3270j.graphics.GraphicsPlane gp) {
this.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 { public enum OiaStatus {
NOT_CONNECTED("OFFLINE"), NOT_CONNECTED("OFFLINE"),
X_SYSTEM("X SYSTEM"), X_SYSTEM("X SYSTEM"),
@@ -152,13 +161,18 @@ public class InputProcessor {
} }
screen.setCursorAddress(baddr); screen.setCursorAddress(baddr);
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
/** /**
* Send an AID key (Enter, PF1-24, PA1-3, Clear). * Send an AID key (Enter, PF1-24, PA1-3, Clear).
*/ */
public void sendAid(int aidCode) { 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; lastAid = aidCode;
setKeyboardLocked(true); setKeyboardLocked(true);
@@ -206,20 +220,36 @@ public class InputProcessor {
int nextRowAddr = ((row + 1) % screen.getRows()) * cols; int nextRowAddr = ((row + 1) % screen.getRows()) * cols;
screen.setCursorAddress(nextRowAddr); screen.setCursorAddress(nextRowAddr);
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
setKeyboardLocked(false); setKeyboardLocked(false);
return; return;
} }
if (aidCode == AID_PA1 || aidCode == AID_PA2 || aidCode == AID_PA3) { 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[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
byte[] data = new byte[] { (byte) aidCode, caddr[0], caddr[1] }; out.write(caddr[0] & 0xFF);
sendAidResponse(data); out.write(caddr[1] & 0xFF);
sendAidResponse(out.toByteArray());
return; 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(); 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); out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols()); byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
@@ -265,28 +295,16 @@ public class InputProcessor {
} }
} }
} else { } else {
// Unformatted screen in 3270 mode (e.g. line-mode console): // Unformatted screen in 3270 mode:
// Send AID + cursor address + only the active input line (row containing cursor) // Send AID + cursor address + all non-null characters on the screen, suppressing trailing nulls per line
int cols = screen.getCols(); // or we can just send everything up to the last non-null on the screen.
int curAddr = screen.getCursorAddress(); // IBM spec: "all alphanumeric characters... Nulls are suppressed."
int row = curAddr / cols; // Actually, the simplest is to send everything, but suppress nulls.
int rowStart = row * cols; int size = screen.getRows() * screen.getCols();
int rowEnd = rowStart + cols; for (int i = 0; i < size; i++) {
int b = screen.getCell(i).ec & 0xFF;
// Find last non-null, non-blank character on the current line if (b != 0x00) {
int lastChar = rowStart - 1; out.write(b);
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);
} }
} }
} }
@@ -302,13 +320,97 @@ 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 ========== // ========== Cursor movement ==========
/**
* Simulate a Text Light Pen selection.
*/
public boolean lightPenSelect(int address) {
if (screen == null || !screen.isFormatted()) {
return false;
}
int size = screen.getRows() * screen.getCols();
int faPos = screen.findFieldAttribute(address);
if (faPos < 0) {
return false;
}
ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF;
if (!org.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
return false;
}
int designatorPos = (faPos + 1) % size;
ExtendedAttribute desCell = screen.getCell(designatorPos);
int ebcdic = desCell.ec & 0xFF;
char ascii = (char) desCell.ucs4;
screen.setCursorAddress(designatorPos);
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(org.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
} else if (ebcdic == 0x50 || ascii == '&') {
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(org.lib3270j.protocol.DS3270Constants.AID_ENTER);
return true;
} else if (ebcdic == 0x6F || ascii == '?') {
desCell.ec = (byte) 0x6E;
desCell.ucs4 = '>';
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else if (ebcdic == 0x6E || ascii == '>') {
desCell.ec = (byte) 0x6F;
desCell.ucs4 = '?';
faCell.fa &= ~org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else {
faCell.fa |= org.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(org.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
}
}
public void cursorUp() { public void cursorUp() {
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
addr -= screen.getCols(); addr -= screen.getCols();
if (addr < 0) addr += screen.getRows() * screen.getCols(); if (addr < 0) addr += screen.getRows() * screen.getCols();
screen.setCursorAddress(addr); screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
} }
public void cursorDown() { public void cursorDown() {
@@ -316,18 +418,21 @@ public class InputProcessor {
addr += screen.getCols(); addr += screen.getCols();
if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols(); if (addr >= screen.getRows() * screen.getCols()) addr -= screen.getRows() * screen.getCols();
screen.setCursorAddress(addr); screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
} }
public void cursorLeft() { public void cursorLeft() {
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
addr = screen.decrementAddress(addr); addr = screen.decrementAddress(addr);
screen.setCursorAddress(addr); screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
} }
public void cursorRight() { public void cursorRight() {
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
addr = screen.incrementAddress(addr); addr = screen.incrementAddress(addr);
screen.setCursorAddress(addr); screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
} }
public void cursorHome() { public void cursorHome() {
@@ -336,11 +441,13 @@ public class InputProcessor {
} else { } else {
screen.setCursorAddress(0); screen.setCursorAddress(0);
} }
screen.updateDisplaySnapshot();
} }
public void tab() { public void tab() {
int addr = screen.findNextUnprotected(screen.getCursorAddress()); int addr = screen.findNextUnprotected(screen.getCursorAddress());
screen.setCursorAddress(addr); screen.setCursorAddress(addr);
screen.updateDisplaySnapshot();
} }
public int getLastAid() { return lastAid; } public int getLastAid() { return lastAid; }
@@ -348,6 +455,7 @@ public class InputProcessor {
public void setCursorAddress(int baddr) { public void setCursorAddress(int baddr) {
screen.setCursorAddress(baddr); screen.setCursorAddress(baddr);
screen.updateDisplaySnapshot();
} }
public void backTab() { public void backTab() {
@@ -361,6 +469,7 @@ public class InputProcessor {
if (screen.getCell(addr).isFieldAttribute()) { if (screen.getCell(addr).isFieldAttribute()) {
if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) { if (!faIsProtected(screen.getCell(addr).fa & 0xFF)) {
screen.setCursorAddress(screen.incrementAddress(addr)); screen.setCursorAddress(screen.incrementAddress(addr));
screen.updateDisplaySnapshot();
return; return;
} }
} }
@@ -377,6 +486,7 @@ public class InputProcessor {
ea.ucs4 = 0; ea.ucs4 = 0;
} }
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
return; return;
} }
int addr = screen.getCursorAddress(); int addr = screen.getCursorAddress();
@@ -398,6 +508,7 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
} }
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
public void deleteChar() { public void deleteChar() {
@@ -426,6 +537,7 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
} }
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
public void backspace() { public void backspace() {
@@ -439,6 +551,7 @@ public class InputProcessor {
if (keyboardLocked) return; if (keyboardLocked) return;
screen.eraseAllUnprotected(); screen.eraseAllUnprotected();
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
/** Move cursor to first unprotected field on next line (Newline key in 3270). */ /** Move cursor to first unprotected field on next line (Newline key in 3270). */
@@ -455,6 +568,7 @@ public class InputProcessor {
} }
screen.setCursorAddress(target); screen.setCursorAddress(target);
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
/** Insert Duplicate (DUP) code and advance to next field. */ /** Insert Duplicate (DUP) code and advance to next field. */
@@ -472,6 +586,8 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
} }
tab(); tab();
screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
/** Insert Field Mark (FM) code. */ /** Insert Field Mark (FM) code. */
@@ -495,6 +611,7 @@ public class InputProcessor {
} }
screen.setCursorAddress(baddr); screen.setCursorAddress(baddr);
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
} }
/** Attention key (sends Telnet IP). */ /** Attention key (sends Telnet IP). */
@@ -571,6 +688,7 @@ public class InputProcessor {
screen.getCell(i).ucs4 = 0; screen.getCell(i).ucs4 = 0;
} }
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
return size; return size;
} }
@@ -608,6 +726,7 @@ public class InputProcessor {
screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY); screen.getCell(faAddr).fa = (byte) (screen.getCell(faAddr).fa | FA_MODIFY);
} }
screen.markAllChanged(); screen.markAllChanged();
screen.updateDisplaySnapshot();
return fieldLen; return fieldLen;
} }
@@ -76,6 +76,7 @@ public final class TelnetConstants {
case TELOPT_BINARY: return "BINARY"; case TELOPT_BINARY: return "BINARY";
case TELOPT_ECHO: return "ECHO"; case TELOPT_ECHO: return "ECHO";
case TELOPT_SGA: return "SGA"; case TELOPT_SGA: return "SGA";
case TELOPT_TM: return "TIMING-MARK";
case TELOPT_TTYPE: return "TTYPE"; case TELOPT_TTYPE: return "TTYPE";
case TELOPT_EOR: return "EOR"; case TELOPT_EOR: return "EOR";
case TELOPT_NAWS: return "NAWS"; case TELOPT_NAWS: return "NAWS";
@@ -25,6 +25,9 @@ public class ScreenBuffer {
private boolean formatted; // Screen has at least one field attribute? private boolean formatted; // Screen has at least one field attribute?
private byte replyMode = SF_SRM_FIELD; private byte replyMode = SF_SRM_FIELD;
private int activePartition = 0; // 0 = implicit partition
private boolean explicitPartitionActive = false;
// Change tracking // Change tracking
private boolean screenChanged; private boolean screenChanged;
private int firstChanged = -1; private int firstChanged = -1;
@@ -60,6 +63,7 @@ public class ScreenBuffer {
defaultFA.ic = 1; defaultFA.ic = 1;
allocateBuffers(); allocateBuffers();
updateDisplaySnapshot();
} }
private void allocateBuffers() { private void allocateBuffers() {
@@ -139,7 +143,7 @@ public class ScreenBuffer {
public boolean isScreenAlt() { return screenAlt; } public boolean isScreenAlt() { return screenAlt; }
/** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */ /** Update alternate dimensions from BIND image. Re-allocates buffers if needed. */
public void setAlternateDimensions(int newAltRows, int newAltCols) { public synchronized void setAlternateDimensions(int newAltRows, int newAltCols) {
if (newAltRows == altRows && newAltCols == altCols) return; if (newAltRows == altRows && newAltCols == altCols) return;
this.altRows = newAltRows; this.altRows = newAltRows;
this.altCols = newAltCols; this.altCols = newAltCols;
@@ -149,11 +153,15 @@ public class ScreenBuffer {
this.maxCols = Math.max(maxCols, newAltCols); this.maxCols = Math.max(maxCols, newAltCols);
allocateBuffers(); allocateBuffers();
} }
updateDisplaySnapshot();
} }
// ========== Cursor ========== // ========== Cursor ==========
public int getCursorAddress() { return cursorAddress; } public int getCursorAddress() { return cursorAddress; }
public void setCursorAddress(int addr) { this.cursorAddress = addr; } public synchronized void setCursorAddress(int addr) {
this.cursorAddress = addr;
this.displayCursorAddress = addr;
}
public int getCursorRow() { return cursorAddress / cols; } public int getCursorRow() { return cursorAddress / cols; }
public int getCursorCol() { return cursorAddress % cols; } public int getCursorCol() { return cursorAddress % cols; }
@@ -163,31 +171,53 @@ public class ScreenBuffer {
public byte getReplyMode() { return replyMode; } public byte getReplyMode() { return replyMode; }
public void setReplyMode(byte mode) { this.replyMode = mode; } 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 ========== // ========== Screen erase ==========
/** /**
* Perform an erase, optionally using the alternate screen size. * Perform an erase, optionally using the alternate screen size.
*/ */
public void erase(boolean alt) { public synchronized void erase(boolean alt) {
clear(); clear();
int newRows = alt ? altRows : defRows; int newRows = alt ? altRows : defRows;
int newCols = alt ? altCols : defCols; int newCols = alt ? altCols : defCols;
if (alt == screenAlt && rows == newRows && cols == newCols) { if (alt == screenAlt && rows == newRows && cols == newCols) {
updateDisplaySnapshot();
return; return;
} }
rows = newRows; rows = newRows;
cols = newCols; cols = newCols;
screenAlt = alt; screenAlt = alt;
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. */ /** Clear the entire buffer. */
public void clear() { public synchronized void clear() {
for (ExtendedAttribute ea : buffer) { for (ExtendedAttribute ea : buffer) {
ea.clear(); ea.clear();
} }
cursorAddress = 0; cursorAddress = 0;
bufferAddress = 0; bufferAddress = 0;
formatted = false; formatted = false;
replyMode = SF_SRM_FIELD;
activePartition = 0;
explicitPartitionActive = false;
screenChanged = true; screenChanged = true;
defaultFg = 0x00; defaultFg = 0x00;
@@ -196,12 +226,13 @@ public class ScreenBuffer {
defaultCs = 0x00; defaultCs = 0x00;
defaultIc = 0x00; defaultIc = 0x00;
replyMode = SF_SRM_FIELD; replyMode = SF_SRM_FIELD;
updateDisplaySnapshot();
} }
/** /**
* Erase all unprotected fields. * Erase all unprotected fields.
*/ */
public void eraseAllUnprotected() { public synchronized void eraseAllUnprotected() {
int size = rows * cols; int size = rows * cols;
boolean inUnprotected = false; boolean inUnprotected = false;
@@ -229,6 +260,7 @@ public class ScreenBuffer {
// Move cursor to first unprotected field // Move cursor to first unprotected field
cursorAddress = findNextUnprotected(0); cursorAddress = findNextUnprotected(0);
screenChanged = true; screenChanged = true;
updateDisplaySnapshot();
} }
// ========== Field attribute navigation ========== // ========== Field attribute navigation ==========
@@ -322,7 +354,7 @@ public class ScreenBuffer {
// ========== Setters for model reconfiguration ========== // ========== Setters for model reconfiguration ==========
public void setDimensions(int maxRows, int maxCols, int defRows, int defCols, public synchronized void setDimensions(int maxRows, int maxCols, int defRows, int defCols,
int altRows, int altCols) { int altRows, int altCols) {
this.maxRows = maxRows; this.maxRows = maxRows;
this.maxCols = maxCols; this.maxCols = maxCols;
@@ -333,6 +365,7 @@ public class ScreenBuffer {
this.rows = defRows; this.rows = defRows;
this.cols = defCols; this.cols = defCols;
allocateBuffers(); allocateBuffers();
updateDisplaySnapshot();
} }
public void translateToUnicode() { public void translateToUnicode() {
@@ -8,6 +8,7 @@ import org.lib3270j.listener.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger; import java.util.logging.Logger;
@@ -59,6 +60,32 @@ public class TelnetFSM {
private int responseRequired = RSF_NO_RESPONSE; private int responseRequired = RSF_NO_RESPONSE;
private boolean deferredWillTtype; private boolean deferredWillTtype;
private boolean tn3270eDeviceTypeSent; private boolean tn3270eDeviceTypeSent;
private int ttypeIndex = 0;
private List<String> getCandidateTerminalTypes() {
List<String> 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 // Connection references
private TelnetConnection connection; private TelnetConnection connection;
@@ -109,6 +136,7 @@ public class TelnetFSM {
tn3270eBound = false; tn3270eBound = false;
eXmitSeq = 0; eXmitSeq = 0;
deferredWillTtype = false; deferredWillTtype = false;
ttypeIndex = 0;
ibuf.reset(); ibuf.reset();
sbbuf.reset(); sbbuf.reset();
@@ -116,6 +144,9 @@ public class TelnetFSM {
eFuncs[FUNC_BIND_IMAGE] = true; eFuncs[FUNC_BIND_IMAGE] = true;
eFuncs[FUNC_RESPONSES] = true; eFuncs[FUNC_RESPONSES] = true;
eFuncs[FUNC_SYSREQ] = 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); changeState(ConnectionState.TELNET_PENDING);
} }
@@ -240,6 +271,11 @@ public class TelnetFSM {
log.fine("RCVD GA"); log.fine("RCVD GA");
state = TNS_DATA; state = TNS_DATA;
break; break;
case AYT:
log.info("RCVD AYT - sending acknowledgment");
sendBytes(new byte[] { (byte) IAC, (byte) NOP });
state = TNS_DATA;
break;
case NOP: case NOP:
log.fine("RCVD NOP"); log.fine("RCVD NOP");
state = TNS_DATA; state = TNS_DATA;
@@ -267,6 +303,11 @@ public class TelnetFSM {
} }
break; break;
case TELOPT_TM:
// RFC 860: Timing Mark - reply with DO TM
sendCommand(DO, opt);
break;
case TELOPT_TN3270E: case TELOPT_TN3270E:
if (!config.isTn3270eEnabled()) { if (!config.isTn3270eEnabled()) {
sendCommand(DONT, opt); sendCommand(DONT, opt);
@@ -310,6 +351,11 @@ public class TelnetFSM {
} }
break; break;
case TELOPT_TM:
// RFC 860: Timing Mark - reply with WILL TM
sendCommand(WILL, opt);
break;
case TELOPT_TTYPE: case TELOPT_TTYPE:
if (!myOpts[opt]) { if (!myOpts[opt]) {
myOpts[opt] = true; myOpts[opt] = true;
@@ -426,8 +472,12 @@ public class TelnetFSM {
private void handleTTypeSB(byte[] data) { private void handleTTypeSB(byte[] data) {
if (data.length >= 2 && data[1] == TELQUAL_SEND) { if (data.length >= 2 && data[1] == TELQUAL_SEND) {
// Host asks for terminal type // Host asks for terminal type — cycle per RFC 1091
String termType = config.getEffectiveTerminalType(); List<String> 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); log.info("RCVD SB TTYPE SEND - Responding with: " + termType);
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
@@ -538,7 +588,7 @@ public class TelnetFSM {
if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) { if (reason == REASON_INV_DEVICE_TYPE || reason == REASON_UNSUPPORTED_REQ) {
// Try fallback model 2 if we were requesting something else // Try fallback model 2 if we were requesting something else
if (config.getModel() != TerminalModel.IBM_3278_2 && 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 (" + log.warning("TN3270E device-type rejected (" +
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2"); TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
config.setModel(TerminalModel.IBM_3278_2); config.setModel(TerminalModel.IBM_3278_2);
@@ -880,6 +930,23 @@ public class TelnetFSM {
tn3270eSubmode = TN3270ESubmode.E_NVT; tn3270eSubmode = TN3270ESubmode.E_NVT;
break; 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: case DT_RESPONSE:
log.fine("Received response, seq=" + seqNumber); log.fine("Received response, seq=" + seqNumber);
break; 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]; byte[] resp = new byte[EH_SIZE + 1];
resp[0] = (byte) DT_RESPONSE; resp[0] = (byte) DT_RESPONSE;
resp[1] = 0; resp[1] = 0;
@@ -915,6 +982,31 @@ public class TelnetFSM {
sendRecord(resp); 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 ========== // ========== Check if we should transition to 3270 mode ==========
private void checkIn3270() { private void checkIn3270() {
@@ -232,4 +232,30 @@ public class QueryReplyBuilderTest {
assertEquals(0x07, replies[descOffset + 49] & 0xFF); assertEquals(0x07, replies[descOffset + 49] & 0xFF);
assertEquals(0xC0, replies[descOffset + 49 + 1] & 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
}
} }
@@ -158,6 +158,7 @@ public class GocaDecoderTest {
byte[] stream = out.toByteArray(); byte[] stream = out.toByteArray();
decoder.decodeStream(stream, 0, stream.length); decoder.decodeStream(stream, 0, stream.length);
assertTrue(plane.hasContent()); assertTrue(plane.hasContent());
} }
@@ -203,7 +204,7 @@ public class GocaDecoderTest {
// 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10) // 0xA1: Relative Line from Current Position: (+10, +20), (-5, -10)
out.write(GocaConstants.G_GCRLIN); 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(0x0A); out.write(0x14); // dx = +10, dy = +20
out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10 out.write(0xFB); out.write(0xF6); // dx = -5, dy = -10
@@ -217,21 +218,74 @@ public class GocaDecoderTest {
@Test @Test
public void test3179GCoordinateMapping() { public void test3179GCoordinateMapping() {
GraphicsPlane plane = new GraphicsPlane(720, 688); GraphicsPlane plane = new GraphicsPlane(720, 516);
plane.setScreenDimensions(80, 43); 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(360, plane.mapX(0));
assertEquals(343, plane.mapY(0)); assertEquals(257, plane.mapY(0));
// Left edge (-360) should map to 0 // Left edge (-360) should map to 0
assertEquals(0, plane.mapX(-360)); assertEquals(0, plane.mapX(-360));
// Right edge (+359) should map to 719 // Right edge (+359) should map to 719
assertEquals(719, plane.mapX(359)); assertEquals(719, plane.mapX(359));
// Top edge (+343) should map to 0 // Top edge (+257) should map to 0
assertEquals(0, plane.mapY(343)); assertEquals(0, plane.mapY(257));
// Bottom edge (-344) should map to 687 // Bottom edge (-258) should map to 515
assertEquals(687, plane.mapY(-344)); 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");
} }
} }
@@ -44,10 +44,113 @@ public class InputProcessorTest {
assertEquals('A', screen.getCell(1).ucs4); assertEquals('A', screen.getCell(1).ucs4);
assertEquals('B', screen.getCell(2).ucs4); assertEquals('B', screen.getCell(2).ucs4);
assertEquals('A', screen.getDisplayCell(1).ucs4);
assertEquals('B', screen.getDisplayCell(2).ucs4);
input.eraseInput(); input.eraseInput();
assertEquals(0, screen.getCell(1).ec); assertEquals(0, screen.getCell(1).ec);
assertEquals(0, screen.getCell(2).ec); assertEquals(0, screen.getCell(2).ec);
assertEquals(0, screen.getDisplayCell(1).ec);
assertEquals(0, screen.getDisplayCell(2).ec);
}
@Test
public void testTypeCharacterUpdatesDisplaySnapshot() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE); // Unprotected field starting at 1
screen.setCursorAddress(1);
screen.updateDisplaySnapshot(); // Initial snapshot
// Type "logon"
input.typeCharacter('l');
input.typeCharacter('o');
input.typeCharacter('g');
input.typeCharacter('o');
input.typeCharacter('n');
// Verify underlying buffer
assertEquals('l', screen.getCell(1).ucs4);
assertEquals('o', screen.getCell(2).ucs4);
assertEquals('g', screen.getCell(3).ucs4);
assertEquals('o', screen.getCell(4).ucs4);
assertEquals('n', screen.getCell(5).ucs4);
// Verify display snapshot (what TerminalPanel renders)
assertEquals('l', screen.getDisplayCell(1).ucs4);
assertEquals('o', screen.getDisplayCell(2).ucs4);
assertEquals('g', screen.getDisplayCell(3).ucs4);
assertEquals('o', screen.getDisplayCell(4).ucs4);
assertEquals('n', screen.getDisplayCell(5).ucs4);
assertEquals(6, screen.getDisplayCursorAddress());
assertEquals(6, screen.getCursorAddress());
}
@Test
public void testCursorMovementUpdatesDisplayCursorAddress() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCursorAddress(10);
assertEquals(10, screen.getDisplayCursorAddress());
input.cursorRight();
assertEquals(11, screen.getDisplayCursorAddress());
input.cursorLeft();
assertEquals(10, screen.getDisplayCursorAddress());
screen.setCursorAddress(45);
assertEquals(45, screen.getDisplayCursorAddress());
}
@Test
public void testDeleteAndEraseEofUpdateDisplaySnapshot() {
InputProcessor input = new InputProcessor(screen, translator, null);
screen.erase(false);
screen.setCellFA(0, (byte) FA_PRINTABLE);
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.setCursorAddress(1);
input.typeCharacter('A');
input.typeCharacter('B');
input.typeCharacter('C');
assertEquals('A', screen.getDisplayCell(1).ucs4);
assertEquals('B', screen.getDisplayCell(2).ucs4);
assertEquals('C', screen.getDisplayCell(3).ucs4);
// Backspace over 'C'
input.backspace();
assertEquals(3, screen.getCursorAddress());
assertEquals(3, screen.getDisplayCursorAddress());
assertEquals(0, screen.getDisplayCell(3).ucs4);
// Erase EOF from position 2 ('B')
screen.setCursorAddress(2);
input.eraseEof();
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]);
} }
} }
@@ -0,0 +1,297 @@
package org.lib3270j.telnet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.lib3270j.ConnectionConfig;
import org.lib3270j.ConnectionState;
import org.lib3270j.TerminalModel;
import org.lib3270j.charset.EbcdicTranslator;
import org.lib3270j.datastream.DataStreamProcessor;
import org.lib3270j.protocol.TelnetConstants;
import org.lib3270j.screen.ScreenBuffer;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class TelnetFSMTest {
private ConnectionConfig config;
private ScreenBuffer screenBuffer;
private DataStreamProcessor dsProcessor;
private TelnetFSM fsm;
private MockConnection connection;
private static class MockConnection extends TelnetConnection {
final List<byte[]> sentData = new ArrayList<>();
MockConnection(ConnectionConfig config, TelnetFSM fsm) {
super(config, fsm);
}
@Override
public synchronized void sendRaw(byte[] data) {
sentData.add(data.clone());
}
@Override
public synchronized void sendRaw(byte[] data, int offset, int length) {
byte[] b = new byte[length];
System.arraycopy(data, offset, b, 0, length);
sentData.add(b);
}
}
@BeforeEach
public void setup() {
config = new ConnectionConfig("localhost", 23, TerminalModel.IBM_3279_4);
screenBuffer = new ScreenBuffer(TerminalModel.IBM_3279_4, new EbcdicTranslator());
dsProcessor = new DataStreamProcessor(screenBuffer, new EbcdicTranslator());
fsm = new TelnetFSM(config, screenBuffer, dsProcessor);
connection = new MockConnection(config, fsm);
fsm.setConnection(connection);
}
private void feedBytes(int... bytes) {
for (int b : bytes) {
fsm.feedByte(b & 0xFF);
}
}
@Test
public void testHostPrefixParsingPlainTn3270() {
ConnectionConfig c1 = ConnectionConfig.parseHostString("P:mainframe.example.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c1.isTn3270eEnabled(), "P: prefix should disable TN3270E");
assertFalse(c1.isUseTls());
assertEquals("mainframe.example.com", c1.getHost());
ConnectionConfig c2 = ConnectionConfig.parseHostString("plain:zos.net:2323", 23, TerminalModel.IBM_3279_4);
assertFalse(c2.isTn3270eEnabled());
assertEquals("zos.net", c2.getHost());
assertEquals(2323, c2.getPort());
ConnectionConfig c3 = ConnectionConfig.parseHostString("L:P:secure.mainframe.com:992", 0, TerminalModel.IBM_3279_4);
assertTrue(c3.isUseTls(), "L: should enable TLS");
assertFalse(c3.isTn3270eEnabled(), "P: should disable TN3270E");
assertEquals("secure.mainframe.com", c3.getHost());
assertEquals(992, c3.getPort());
ConnectionConfig c4 = ConnectionConfig.parseHostString("non-e:vm.ibm.com", 23, TerminalModel.IBM_3279_4);
assertFalse(c4.isTn3270eEnabled());
assertEquals("vm.ibm.com", c4.getHost());
}
@Test
public void testPlainTn3270NegotiationRejectsTn3270E() {
config.setTn3270eEnabled(false);
fsm.onConnected();
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
// Host offers DO TN3270E -> client must reply WONT TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
assertFalse(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host offers WILL TN3270E -> client must reply DONT TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_TN3270E);
assertFalse(fsm.getHisOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host negotiates standard 3270 options: DO BINARY, WILL BINARY, DO EOR, WILL EOR, DO TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_BINARY);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.WILL, TelnetConstants.TELOPT_EOR);
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TTYPE);
// Client should have transitioned to CONNECTED_3270
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
// Host asks for TTYPE
feedBytes(TelnetConstants.IAC, TelnetConstants.SB, TelnetConstants.TELOPT_TTYPE,
TelnetConstants.TELQUAL_SEND, TelnetConstants.IAC, TelnetConstants.SE);
// Verify sent packets contain WONT TN3270E, DONT TN3270E, and TTYPE IS
boolean foundWontTn3270e = false;
boolean foundTtypeIs = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length == 3 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.WONT && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E) {
foundWontTn3270e = true;
}
if (pkt.length >= 4 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TTYPE) {
foundTtypeIs = true;
}
}
assertTrue(foundWontTn3270e, "Expected WONT TN3270E response");
assertTrue(foundTtypeIs, "Expected TTYPE IS response");
}
@Test
public void testTn3270eFunctionsNegotiationWithoutBindImageTransitionsToConnected() {
config.setTn3270eEnabled(true);
fsm.onConnected();
assertEquals(ConnectionState.TELNET_PENDING, fsm.getConnectionState());
// Host offers DO TN3270E -> client accepts with WILL TN3270E
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
assertTrue(fsm.getMyOpts()[TelnetConstants.TELOPT_TN3270E]);
// Host responds with DEVICE-TYPE IS IBM-3279-4-E CONNECT LU01
byte[] devTypeIs = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, // OP_DEVICE_TYPE, OP_IS
'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
0x01, // OP_CONNECT
'L', 'U', '0', '1',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
// Host responds with FUNCTIONS REQUEST <empty> (e.g. pytn3270 / RFC 2355 server)
byte[] funcsReq = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x03, 0x07, // OP_FUNCTIONS, OP_REQUEST
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
// Per RFC 2355: Client must reply with FUNCTIONS IS, and without BIND-IMAGE, session is bound immediately
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState(),
"Expected CONNECTED_TN3270E when BIND-IMAGE is not negotiated");
// Verify that client sent SB TN3270E FUNCTIONS IS SE
boolean foundFunctionsIs = false;
for (byte[] pkt : connection.sentData) {
if (pkt.length >= 5 && (pkt[0] & 0xFF) == TelnetConstants.IAC &&
(pkt[1] & 0xFF) == TelnetConstants.SB && (pkt[2] & 0xFF) == TelnetConstants.TELOPT_TN3270E &&
(pkt[3] & 0xFF) == 0x03 && (pkt[4] & 0xFF) == 0x04) {
foundFunctionsIs = true;
}
}
assertTrue(foundFunctionsIs, "Expected FUNCTIONS IS reply from client upon receiving FUNCTIONS REQUEST");
}
@Test
public void testPlainDataStreamFallbackInTn3270eMode() {
config.setTn3270eEnabled(true);
fsm.onConnected();
// Negotiate TN3270E with empty functions
feedBytes(TelnetConstants.IAC, TelnetConstants.DO, TelnetConstants.TELOPT_TN3270E);
byte[] devTypeIs = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x02, 0x04, 'I', 'B', 'M', '-', '3', '2', '7', '9', '-', '4', '-', 'E',
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : devTypeIs) fsm.feedByte(b & 0xFF);
byte[] funcsReq = new byte[]{
(byte) TelnetConstants.IAC, (byte) TelnetConstants.SB, (byte) TelnetConstants.TELOPT_TN3270E,
0x03, 0x07, (byte) TelnetConstants.IAC, (byte) TelnetConstants.SE
};
for (byte b : funcsReq) fsm.feedByte(b & 0xFF);
assertEquals(ConnectionState.CONNECTED_TN3270E, fsm.getConnectionState());
// Now host sends raw 3270 data stream without 5-byte TN3270E header (e.g. EraseWrite 0xF5)
// 0xF5 = EraseWrite, 0xC3 = WCC, 0x11 = SBA, 0x40 0x40 = Pos 0, "HELLO"
EbcdicTranslator trans = new EbcdicTranslator();
String msg = "HELLO";
byte[] rawStream = new byte[3 + 2 + msg.length() + 2];
rawStream[0] = (byte) 0xF5; // EraseWrite
rawStream[1] = (byte) 0xC3; // WCC
rawStream[2] = 0x11; // SBA
rawStream[3] = 0x40; rawStream[4] = 0x40; // addr 0
for (int i = 0; i < msg.length(); i++) {
rawStream[5 + i] = (byte) trans.unicodeToEbcdic(msg.charAt(i));
}
rawStream[rawStream.length - 2] = (byte) TelnetConstants.IAC;
rawStream[rawStream.length - 1] = (byte) TelnetConstants.EOR;
for (byte b : rawStream) fsm.feedByte(b & 0xFF);
// State must have automatically switched to CONNECTED_3270 and screen updated
assertEquals(ConnectionState.CONNECTED_3270, fsm.getConnectionState());
assertEquals('H', trans.ebcdicToUnicode(screenBuffer.getCellEC(0)));
assertEquals('E', trans.ebcdicToUnicode(screenBuffer.getCellEC(1)));
assertEquals('L', trans.ebcdicToUnicode(screenBuffer.getCellEC(2)));
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);
}
}
@@ -31,7 +31,7 @@ public class TlsConfigTest {
assertEquals(992, c1.getPort()); assertEquals(992, c1.getPort());
// ssl: prefix with explicit port // 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()); assertTrue(c2.isUseTls());
assertEquals("zos.local", c2.getHost()); assertEquals("zos.local", c2.getHost());
assertEquals(2323, c2.getPort()); assertEquals(2323, c2.getPort());