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