Add tn3270(no e) and fix animations
This commit is contained in:
@@ -17,7 +17,8 @@ public class ConnectionConfig {
|
||||
private int connectTimeoutMs = 15000;
|
||||
private int nopIntervalSeconds = 0;
|
||||
private String terminalName = null; // override terminal type string
|
||||
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.NONE;
|
||||
private boolean tn3270eEnabled = true;
|
||||
private org.lib3270j.graphics.GraphicsMode graphicsMode = org.lib3270j.graphics.GraphicsMode.BOTH;
|
||||
|
||||
public ConnectionConfig() {}
|
||||
|
||||
@@ -66,6 +67,9 @@ public class ConnectionConfig {
|
||||
public boolean isTlsVerifyCert() { return tlsVerifyCert; }
|
||||
public void setTlsVerifyCert(boolean verify) { this.tlsVerifyCert = verify; }
|
||||
|
||||
public boolean isTn3270eEnabled() { return tn3270eEnabled; }
|
||||
public void setTn3270eEnabled(boolean enabled) { this.tn3270eEnabled = enabled; }
|
||||
|
||||
public org.lib3270j.tls.TlsCertificateVerifier getCertificateVerifier() { return certificateVerifier; }
|
||||
public void setCertificateVerifier(org.lib3270j.tls.TlsCertificateVerifier verifier) { this.certificateVerifier = verifier; }
|
||||
|
||||
@@ -87,8 +91,8 @@ public class ConnectionConfig {
|
||||
public void setTerminalName(String name) { this.terminalName = name; }
|
||||
|
||||
/**
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port")
|
||||
* or standard "host:port" formats.
|
||||
* Parse a host connection string which may include prefixes for TLS (e.g. "L:host:port", "ssl:host:port", "y:host:port"),
|
||||
* plain TN3270 (e.g. "P:host:port", "plain:host:port", "non-e:host:port"), or standard "host:port" formats.
|
||||
*/
|
||||
public static ConnectionConfig parseHostString(String hostStr, int defaultPort, TerminalModel defaultModel) {
|
||||
if (hostStr == null || hostStr.trim().isEmpty()) {
|
||||
@@ -96,17 +100,35 @@ public class ConnectionConfig {
|
||||
}
|
||||
String s = hostStr.trim();
|
||||
boolean tls = false;
|
||||
boolean tn3270e = true;
|
||||
|
||||
// Check TLS prefixes
|
||||
if (s.startsWith("L:") || s.startsWith("l:")) {
|
||||
tls = true;
|
||||
s = s.substring(2);
|
||||
} else if (s.startsWith("Y:") || s.startsWith("y:")) {
|
||||
tls = true;
|
||||
s = s.substring(2);
|
||||
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||
tls = true;
|
||||
s = s.substring(4);
|
||||
// Parse chained x3270-style prefixes (e.g. "L:P:host:port" or "P:host:port")
|
||||
boolean prefixFound = true;
|
||||
while (prefixFound) {
|
||||
prefixFound = false;
|
||||
if (s.startsWith("L:") || s.startsWith("l:") || s.startsWith("Y:") || s.startsWith("y:")) {
|
||||
tls = true;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("ssl:") || s.toLowerCase().startsWith("tls:")) {
|
||||
tls = true;
|
||||
s = s.substring(4);
|
||||
prefixFound = true;
|
||||
} else if (s.startsWith("N:") || s.startsWith("n:") || s.toLowerCase().startsWith("notls:") || s.toLowerCase().startsWith("nossl:")) {
|
||||
tls = false;
|
||||
int colon = s.indexOf(':');
|
||||
s = s.substring(colon + 1);
|
||||
prefixFound = true;
|
||||
} else if (s.startsWith("P:") || s.startsWith("p:")) {
|
||||
tn3270e = false;
|
||||
s = s.substring(2);
|
||||
prefixFound = true;
|
||||
} else if (s.toLowerCase().startsWith("plain:") || s.toLowerCase().startsWith("non-e:")) {
|
||||
tn3270e = false;
|
||||
int colon = s.indexOf(':');
|
||||
s = s.substring(colon + 1);
|
||||
prefixFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
String host = s;
|
||||
@@ -133,6 +155,7 @@ public class ConnectionConfig {
|
||||
|
||||
ConnectionConfig config = new ConnectionConfig(host, port, defaultModel != null ? defaultModel : TerminalModel.IBM_3279_4);
|
||||
config.setUseTls(tls);
|
||||
config.setTn3270eEnabled(tn3270e);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import static org.lib3270j.protocol.DS3270Constants.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
@@ -105,74 +106,92 @@ public class DataStreamProcessor {
|
||||
return;
|
||||
|
||||
int cmd = data[offset] & 0xFF;
|
||||
log.info(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
|
||||
synchronized (screen.getRenderLock()) {
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> CMD: " + commandName(cmd) + " (0x" + String.format("%02x", cmd) + ") " + length + " bytes");
|
||||
}
|
||||
|
||||
switch (cmd) {
|
||||
case CMD_W:
|
||||
case SNA_CMD_W:
|
||||
processWrite(data, offset, length, false);
|
||||
break;
|
||||
case CMD_EW:
|
||||
case SNA_CMD_EW:
|
||||
log.info(">>> ERASE/WRITE: clearing screen (default size)"); {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
graphicsPlane.clear();
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
switch (cmd) {
|
||||
case CMD_W:
|
||||
case SNA_CMD_W:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processWrite(data, offset, length, false);
|
||||
break;
|
||||
case CMD_EW:
|
||||
case SNA_CMD_EW:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> ERASE/WRITE: clearing screen (default size)");
|
||||
}
|
||||
{
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(false);
|
||||
graphicsPlane.clear();
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_EWA:
|
||||
case SNA_CMD_EWA:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
if (log.isLoggable(Level.FINE)) {
|
||||
log.fine(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)");
|
||||
}
|
||||
{
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
graphicsPlane.clear();
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
processReadModified(true);
|
||||
break;
|
||||
case CMD_EAU:
|
||||
case SNA_CMD_EAU:
|
||||
programSymbolManager.commitStagedSymbols();
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
processWriteStructuredField(data, offset, length);
|
||||
break;
|
||||
case CMD_NOP:
|
||||
log.info(">>> NOP command");
|
||||
break;
|
||||
default:
|
||||
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case CMD_EWA:
|
||||
case SNA_CMD_EWA:
|
||||
log.info(">>> ERASE/WRITE ALTERNATE: clearing screen (alt size)"); {
|
||||
int oldRows = screen.getRows();
|
||||
int oldCols = screen.getCols();
|
||||
screen.erase(true);
|
||||
graphicsPlane.clear();
|
||||
processWrite(data, offset, length, true);
|
||||
if (oldRows != screen.getRows() || oldCols != screen.getCols()) {
|
||||
notifyScreenSizeChanged();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_RB:
|
||||
case SNA_CMD_RB:
|
||||
processReadBuffer();
|
||||
break;
|
||||
case CMD_RM:
|
||||
case SNA_CMD_RM:
|
||||
processReadModified(false);
|
||||
break;
|
||||
case CMD_RMA:
|
||||
case SNA_CMD_RMA:
|
||||
processReadModified(true);
|
||||
break;
|
||||
case CMD_EAU:
|
||||
case SNA_CMD_EAU:
|
||||
log.info(">>> EAU: erasing all unprotected fields");
|
||||
screen.eraseAllUnprotected();
|
||||
break;
|
||||
case CMD_WSF:
|
||||
case SNA_CMD_WSF:
|
||||
processWriteStructuredField(data, offset, length);
|
||||
break;
|
||||
case CMD_NOP:
|
||||
log.info(">>> NOP command");
|
||||
break;
|
||||
default:
|
||||
log.warning(">>> UNKNOWN 3270 command: " + String.format("0x%02x", cmd));
|
||||
break;
|
||||
|
||||
// Translate EBCDIC to Unicode for display
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
screen.updateDisplaySnapshot();
|
||||
}
|
||||
|
||||
// Translate EBCDIC to Unicode for display
|
||||
screen.translateToUnicode();
|
||||
screen.markAllChanged();
|
||||
|
||||
// Debug: dump non-empty screen lines
|
||||
if (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 c = screen.getCols();
|
||||
StringBuilder dump = new StringBuilder();
|
||||
@@ -205,7 +224,7 @@ public class DataStreamProcessor {
|
||||
}
|
||||
}
|
||||
if (dump.length() > 0) {
|
||||
log.info("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
||||
log.fine("Screen content after " + commandName(cmd) + ":\n" + dump.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,7 +817,6 @@ public class DataStreamProcessor {
|
||||
byte[] psData = new byte[fieldLen - 3];
|
||||
System.arraycopy(data, pos + 3, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
case org.lib3270j.graphics.GocaConstants.SF_LOADPS: { // 0x0F: 2-byte Structured Field prefix
|
||||
@@ -810,7 +828,6 @@ public class DataStreamProcessor {
|
||||
byte[] psData = new byte[fieldLen - 4];
|
||||
System.arraycopy(data, pos + 4, psData, 0, psData.length);
|
||||
programSymbolManager.loadps(psData);
|
||||
notifyScreenUpdated();
|
||||
}
|
||||
break;
|
||||
case org.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
|
||||
|
||||
@@ -22,7 +22,7 @@ public class QueryReplyBuilder {
|
||||
private static final int Yr_3279_2 = 0x0002006f;
|
||||
|
||||
private final ScreenBuffer screen;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.NONE;
|
||||
private GraphicsMode graphicsMode = GraphicsMode.BOTH;
|
||||
|
||||
// Base query reply codes (text mode)
|
||||
private static final int[] SUPPORTED_QR_BASE = {
|
||||
@@ -313,9 +313,9 @@ public class QueryReplyBuilder {
|
||||
|
||||
private byte[] buildCharsets() {
|
||||
if (graphicsMode.isProgrammedSymbolsEnabled()) {
|
||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A)
|
||||
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
|
||||
out.write(0x82); // flags: GE, CGCSGID present
|
||||
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
|
||||
out.write(0x00); // more flags
|
||||
out.write(SW_3279_2); // SDW (9)
|
||||
out.write(SH_3279_2); // SDH (12)
|
||||
@@ -328,14 +328,14 @@ public class QueryReplyBuilder {
|
||||
out.write(0x00); out.write(0x10); out.write(0x00); out.write(0x02); out.write(0xb9); out.write(0x00); out.write(0x25);
|
||||
// Descriptor 2 (SET 1): APL/GE character set
|
||||
out.write(0x01); out.write(0x00); out.write(0xf1); out.write(0x03); out.write(0xc3); out.write(0x01); out.write(0x36);
|
||||
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3)
|
||||
// Loadable Single-Plane PS Sets (PSA, PSB: Slots 2, 3) - Flags = 0x80 (Loadable, single plane)
|
||||
out.write(0x02); out.write(0x80); out.write(0x40); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x03); out.write(0x80); out.write(0x41); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7)
|
||||
out.write(0x04); out.write(0x80); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x05); out.write(0x80); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x06); out.write(0x80); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x07); out.write(0x80); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
// Loadable Triple-Plane PS Sets (PSC, PSD, PSE, PSF: Slots 4, 5, 6, 7) - Flags = 0xC0 (0x80 Loadable | 0x40 Triple-plane)
|
||||
out.write(0x04); out.write(0xc0); out.write(0x42); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x05); out.write(0xc0); out.write(0x43); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x06); out.write(0xc0); out.write(0x44); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
out.write(0x07); out.write(0xc0); out.write(0x45); out.write(0x00); out.write(0x00); out.write(0x00); out.write(0x00);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,31 @@ public class ProgramSymbolManager {
|
||||
public static final int NUMBER_TRIPLE_PLANE_PS_SETS = 4; // RWS 4..7 (Sets 2..5)
|
||||
public static final int NUMBER_GRAPHICS_SYMBOL_SETS = 4; // RWS 8..11 (Sets 6..9)
|
||||
|
||||
private int defaultCellWidth = 9;
|
||||
private int defaultCellHeight = 12; // Standard IBM 3279 PS Slot Default Height (SDH = 0x0C = 12)
|
||||
|
||||
public void setDefaultCellDimensions(int width, int height) {
|
||||
this.defaultCellWidth = (width > 0) ? width : 9;
|
||||
this.defaultCellHeight = (height > 0) ? height : 12;
|
||||
}
|
||||
|
||||
public int getDefaultCellWidth() {
|
||||
return defaultCellWidth;
|
||||
}
|
||||
|
||||
public int getDefaultCellHeight() {
|
||||
return defaultCellHeight;
|
||||
}
|
||||
|
||||
private final ProgramSymbolSet[] sets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||
private final ProgramSymbolSet[] stagingSets = new ProgramSymbolSet[NUMBER_SYMBOL_SETS];
|
||||
private final ProgramSymbolSet[] lcidMap = new ProgramSymbolSet[256];
|
||||
|
||||
public ProgramSymbolManager() {
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
boolean isTriple = (i >= NUMBER_SINGLE_PLANE_PS_SETS && i < NUMBER_SINGLE_PLANE_PS_SETS + NUMBER_TRIPLE_PLANE_PS_SETS);
|
||||
sets[i] = new ProgramSymbolSet(isTriple);
|
||||
stagingSets[i] = new ProgramSymbolSet(isTriple);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,32 +49,67 @@ public class ProgramSymbolManager {
|
||||
* Resets all symbol sets.
|
||||
*/
|
||||
public synchronized void clearAll() {
|
||||
for (ProgramSymbolSet set : sets) {
|
||||
set.clear();
|
||||
set.setLcid(0);
|
||||
Arrays.fill(lcidMap, null);
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
sets[i].clear();
|
||||
sets[i].setLcid(0);
|
||||
stagingSets[i].clear();
|
||||
stagingSets[i].setLcid(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits all staged multi-plane symbol sets to the active presentation sets atomically.
|
||||
*/
|
||||
public synchronized void commitStagedSymbols() {
|
||||
for (int i = 0; i < NUMBER_SYMBOL_SETS; i++) {
|
||||
int lcid = stagingSets[i].getLcid();
|
||||
if (lcid > 0) {
|
||||
ProgramSymbolSet staged = stagingSets[i];
|
||||
ProgramSymbolSet active = sets[i];
|
||||
active.setLcid(lcid);
|
||||
for (int slot = 0; slot < ProgramSymbolSet.NUM_SLOTS; slot++) {
|
||||
active.setSlot(slot, staged.getSlot(slot));
|
||||
}
|
||||
if (lcid < 256) {
|
||||
lcidMap[lcid] = active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the symbol set corresponding to a given LCID (0x40 - 0xFE).
|
||||
*/
|
||||
public synchronized ProgramSymbolSet getSymbolSet(int lcid) {
|
||||
if (lcid <= 0) {
|
||||
public ProgramSymbolSet getSymbolSet(int lcid) {
|
||||
if (lcid <= 0 || lcid >= 256) {
|
||||
return null;
|
||||
}
|
||||
for (ProgramSymbolSet set : sets) {
|
||||
if (set.getLcid() == lcid) {
|
||||
return set;
|
||||
ProgramSymbolSet set = lcidMap[lcid];
|
||||
if (set == null) {
|
||||
for (ProgramSymbolSet s : stagingSets) {
|
||||
if (s.getLcid() == lcid) return s;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a specific symbol glyph by LCID and character code point (0x40 - 0xFE).
|
||||
*/
|
||||
public synchronized ProgramSymbolSet.SymbolSlot getSymbol(int lcid, int codePoint) {
|
||||
ProgramSymbolSet set = getSymbolSet(lcid);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (set == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -98,12 +152,12 @@ public class ProgramSymbolManager {
|
||||
return;
|
||||
}
|
||||
|
||||
ProgramSymbolSet set = sets[setIndex];
|
||||
boolean isTriplePlane = (rws >= 4 && rws <= 7);
|
||||
ProgramSymbolSet set = isTriplePlane ? stagingSets[setIndex] : sets[setIndex];
|
||||
|
||||
int extHeaderLen = 0;
|
||||
int cellWidth = 9;
|
||||
int cellHeight = 16;
|
||||
int cellWidth = defaultCellWidth;
|
||||
int cellHeight = defaultCellHeight;
|
||||
int colorPlane = 0; // 0 = all planes, 1 = Red, 2 = Green, 4 = Blue
|
||||
|
||||
if (hasExtHeader && data.length > 4) {
|
||||
@@ -121,10 +175,10 @@ public class ProgramSymbolManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (clearAll) {
|
||||
set.clear();
|
||||
}
|
||||
set.setLcid(lcid);
|
||||
if (!isTriplePlane && lcid > 0 && lcid < 256) {
|
||||
lcidMap[lcid] = set;
|
||||
}
|
||||
|
||||
int offset = 4 + (hasExtHeader ? extHeaderLen : 0);
|
||||
int remaining = data.length - offset;
|
||||
@@ -142,12 +196,10 @@ public class ProgramSymbolManager {
|
||||
}
|
||||
|
||||
while (remaining >= bytesPerSymbol && codeIndex < ProgramSymbolSet.NUM_SLOTS) {
|
||||
byte[] pixelData;
|
||||
byte[] pixelData = new byte[cellWidth * cellHeight];
|
||||
ProgramSymbolSet.SymbolSlot existing = set.getSlot(codeIndex);
|
||||
if (existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||
pixelData = existing.getPixelData();
|
||||
} else {
|
||||
pixelData = new byte[cellWidth * cellHeight];
|
||||
if (!clearAll && existing != null && existing.getWidth() == cellWidth && existing.getHeight() == cellHeight) {
|
||||
System.arraycopy(existing.getPixelData(), 0, pixelData, 0, Math.min(existing.getPixelData().length, pixelData.length));
|
||||
}
|
||||
|
||||
if (loadFormat == 1) {
|
||||
@@ -163,9 +215,17 @@ public class ProgramSymbolManager {
|
||||
remaining -= bytesPerSymbol;
|
||||
}
|
||||
|
||||
logger.info(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
||||
if (clearAll) {
|
||||
for (int i = codeIndex; i < ProgramSymbolSet.NUM_SLOTS; i++) {
|
||||
set.clearSlot(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isLoggable(Level.FINE)) {
|
||||
logger.fine(String.format("LOADPS: Loaded PS Set LCID=0x%02X (RWS=%d, %s, %dx%d, %d glyphs)",
|
||||
lcid, rws, isTriplePlane ? "Triple-Plane" : "Single-Plane",
|
||||
cellWidth, cellHeight, codeIndex - ((startCodePoint >= 0x40) ? (startCodePoint - 0x40) : startCodePoint)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,12 @@ public class ProgramSymbolSet {
|
||||
private int[] cachedRgbArray;
|
||||
private int cachedFgRgb = -1;
|
||||
private int cachedBgRgb = -1;
|
||||
private java.awt.image.BufferedImage cachedImage;
|
||||
private java.awt.image.BufferedImage cachedScaledImage;
|
||||
private int cachedTargetW = 0;
|
||||
private int cachedTargetH = 0;
|
||||
private int cachedScaledFgRgb = -1;
|
||||
private int cachedScaledBgRgb = -1;
|
||||
|
||||
public SymbolSlot(int width, int height, byte[] pixelData, boolean isTriplePlane) {
|
||||
this.width = width > 0 ? width : 9;
|
||||
@@ -88,6 +94,61 @@ public class ProgramSymbolSet {
|
||||
return isTriplePlane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an image scaled directly to target dimensions (cellWidth x cellHeight).
|
||||
* Enables unscaled 1:1 hardware blitting in Java2D.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getScaledImage(int targetW, int targetH, int fgArgb, int bgArgb) {
|
||||
if (targetW <= 0 || targetH <= 0) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
}
|
||||
if (targetW == width && targetH == height) {
|
||||
return getImage(fgArgb, bgArgb);
|
||||
}
|
||||
if (cachedScaledImage != null && cachedTargetW == targetW && cachedTargetH == targetH
|
||||
&& cachedScaledFgRgb == fgArgb && cachedScaledBgRgb == bgArgb) {
|
||||
return cachedScaledImage;
|
||||
}
|
||||
int[] srcRgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage scaled = new java.awt.image.BufferedImage(targetW, targetH, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] dstRgb = ((java.awt.image.DataBufferInt) scaled.getRaster().getDataBuffer()).getData();
|
||||
|
||||
for (int dy = 0; dy < targetH; dy++) {
|
||||
int sy = dy * height / targetH;
|
||||
int srcRowOffset = sy * width;
|
||||
int dstRowOffset = dy * targetW;
|
||||
for (int dx = 0; dx < targetW; dx++) {
|
||||
int sx = dx * width / targetW;
|
||||
dstRgb[dstRowOffset + dx] = srcRgb[srcRowOffset + sx];
|
||||
}
|
||||
}
|
||||
|
||||
this.cachedScaledImage = scaled;
|
||||
this.cachedTargetW = targetW;
|
||||
this.cachedTargetH = targetH;
|
||||
this.cachedScaledFgRgb = fgArgb;
|
||||
this.cachedScaledBgRgb = bgArgb;
|
||||
return scaled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the cached BufferedImage for this symbol glyph.
|
||||
* Eliminates per-cell heap allocations during high frame rate rendering.
|
||||
*/
|
||||
public synchronized java.awt.image.BufferedImage getImage(int fgArgb, int bgArgb) {
|
||||
if (cachedImage != null && cachedFgRgb == fgArgb && cachedBgRgb == bgArgb) {
|
||||
return cachedImage;
|
||||
}
|
||||
int[] rgb = getRgbPixels(fgArgb, bgArgb);
|
||||
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
|
||||
int[] imgData = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData();
|
||||
System.arraycopy(rgb, 0, imgData, 0, rgb.length);
|
||||
this.cachedImage = img;
|
||||
this.cachedFgRgb = fgArgb;
|
||||
this.cachedBgRgb = bgArgb;
|
||||
return img;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and returns the 32-bit ARGB pixel array for this symbol.
|
||||
* The returned array has length (width * height).
|
||||
|
||||
@@ -36,8 +36,12 @@ public class ScreenBuffer {
|
||||
private byte defaultGr = 0x00;
|
||||
private byte defaultCs = 0x00;
|
||||
private byte defaultIc = 0x00;
|
||||
|
||||
private final EbcdicTranslator translator;
|
||||
private final Object renderLock = new Object();
|
||||
|
||||
public Object getRenderLock() {
|
||||
return renderLock;
|
||||
}
|
||||
|
||||
public ScreenBuffer(TerminalModel model, EbcdicTranslator translator) {
|
||||
this.translator = translator;
|
||||
@@ -79,6 +83,50 @@ public class ScreenBuffer {
|
||||
return buffer[addr];
|
||||
}
|
||||
|
||||
private ExtendedAttribute[] displaySnapshot;
|
||||
private int displayRows;
|
||||
private int displayCols;
|
||||
private int displayCursorAddress;
|
||||
|
||||
/**
|
||||
* Atomically creates a snapshot of the current presentation buffer for tear-free rendering.
|
||||
* Takes ~2 microseconds and eliminates mutual thread contention with the UI thread.
|
||||
*/
|
||||
public synchronized void updateDisplaySnapshot() {
|
||||
int size = rows * cols;
|
||||
if (displaySnapshot == null || displaySnapshot.length < size) {
|
||||
displaySnapshot = new ExtendedAttribute[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
displaySnapshot[i] = new ExtendedAttribute();
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < size; i++) {
|
||||
displaySnapshot[i].copyFrom(buffer[i]);
|
||||
}
|
||||
this.displayRows = rows;
|
||||
this.displayCols = cols;
|
||||
this.displayCursorAddress = cursorAddress;
|
||||
}
|
||||
|
||||
public synchronized ExtendedAttribute getDisplayCell(int addr) {
|
||||
if (displaySnapshot == null || addr < 0 || addr >= displayRows * displayCols) {
|
||||
return getCell(addr);
|
||||
}
|
||||
return displaySnapshot[addr];
|
||||
}
|
||||
|
||||
public synchronized int getDisplayRows() {
|
||||
return displayRows > 0 ? displayRows : rows;
|
||||
}
|
||||
|
||||
public synchronized int getDisplayCols() {
|
||||
return displayCols > 0 ? displayCols : cols;
|
||||
}
|
||||
|
||||
public synchronized int getDisplayCursorAddress() {
|
||||
return displayRows > 0 ? displayCursorAddress : cursorAddress;
|
||||
}
|
||||
|
||||
// ========== Dimension accessors ==========
|
||||
public int getRows() { return rows; }
|
||||
public int getCols() { return cols; }
|
||||
|
||||
@@ -149,9 +149,7 @@ public class TelnetConnection {
|
||||
log.fine("RCVD " + n + " bytes: " + formatHex(buf, 0, n));
|
||||
}
|
||||
try {
|
||||
for (int i = 0; i < n; i++) {
|
||||
fsm.feedByte(buf[i] & 0xFF);
|
||||
}
|
||||
fsm.feedBytes(buf, 0, n);
|
||||
fsm.endOfNetworkData();
|
||||
} catch (Throwable t) {
|
||||
log.log(Level.SEVERE, "Exception processing incoming data stream", t);
|
||||
|
||||
@@ -58,6 +58,7 @@ public class TelnetFSM {
|
||||
private int eXmitSeq;
|
||||
private int responseRequired = RSF_NO_RESPONSE;
|
||||
private boolean deferredWillTtype;
|
||||
private boolean tn3270eDeviceTypeSent;
|
||||
|
||||
// Connection references
|
||||
private TelnetConnection connection;
|
||||
@@ -103,6 +104,7 @@ public class TelnetFSM {
|
||||
java.util.Arrays.fill(hisOpts, false);
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
tn3270eNegotiated = false;
|
||||
tn3270eDeviceTypeSent = false;
|
||||
tn3270eSubmode = TN3270ESubmode.UNBOUND;
|
||||
tn3270eBound = false;
|
||||
eXmitSeq = 0;
|
||||
@@ -118,6 +120,37 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.TELNET_PENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed a bulk buffer of bytes from the network into the FSM.
|
||||
*/
|
||||
public void feedBytes(byte[] buf, int offset, int len) {
|
||||
int end = offset + len;
|
||||
int i = offset;
|
||||
while (i < end) {
|
||||
if (state == TNS_DATA) {
|
||||
int start = i;
|
||||
while (i < end && (buf[i] & 0xFF) != IAC) {
|
||||
i++;
|
||||
}
|
||||
if (i > start) {
|
||||
if (connectionState == ConnectionState.TELNET_PENDING) {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
ibuf.write(buf, start, i - start);
|
||||
}
|
||||
}
|
||||
if (i < end) {
|
||||
state = TNS_IAC;
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
feedByte(buf[i] & 0xFF);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed a single byte from the network into the FSM.
|
||||
*/
|
||||
@@ -172,8 +205,8 @@ public class TelnetFSM {
|
||||
changeState(ConnectionState.CONNECTED_NVT);
|
||||
}
|
||||
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
||||
// Accumulate data for 3270, TN3270E (including SSCP-LU and unbound states, and pending)
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
ibuf.write(c);
|
||||
}
|
||||
// NVT data would go to NVT processor (not implemented in initial version)
|
||||
@@ -189,7 +222,7 @@ public class TelnetFSM {
|
||||
break;
|
||||
case EOR: // End of record — process accumulated 3270 data
|
||||
log.fine("RCVD EOR");
|
||||
if (connectionState.is3270() || connectionState.isTn3270e()) {
|
||||
if (connectionState.is3270() || connectionState.isTn3270e() || connectionState == ConnectionState.TELNET_PENDING) {
|
||||
processEndOfRecord();
|
||||
}
|
||||
ibuf.reset();
|
||||
@@ -235,7 +268,9 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!hisOpts[opt]) {
|
||||
if (!config.isTn3270eEnabled()) {
|
||||
sendCommand(DONT, opt);
|
||||
} else if (!hisOpts[opt]) {
|
||||
hisOpts[opt] = true;
|
||||
sendCommand(DO, opt);
|
||||
}
|
||||
@@ -278,7 +313,7 @@ public class TelnetFSM {
|
||||
case TELOPT_TTYPE:
|
||||
if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
if (hisOpts[TELOPT_TN3270E]) {
|
||||
if (config.isTn3270eEnabled() && hisOpts[TELOPT_TN3270E]) {
|
||||
// Defer TTYPE response until TN3270E negotiation completes
|
||||
deferredWillTtype = true;
|
||||
} else {
|
||||
@@ -288,11 +323,16 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
case TELOPT_TN3270E:
|
||||
if (!myOpts[opt]) {
|
||||
if (!config.isTn3270eEnabled()) {
|
||||
sendCommand(WONT, opt);
|
||||
} else if (!myOpts[opt]) {
|
||||
myOpts[opt] = true;
|
||||
sendCommand(WILL, opt);
|
||||
// Start TN3270E sub-negotiation: send device type request
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
if (!tn3270eDeviceTypeSent) {
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
tn3270eDeviceTypeSent = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -428,7 +468,10 @@ public class TelnetFSM {
|
||||
switch (op) {
|
||||
case OP_SEND:
|
||||
// Host asks us to send device-type request
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
if (!tn3270eDeviceTypeSent) {
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
tn3270eDeviceTypeSent = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case OP_DEVICE_TYPE:
|
||||
@@ -488,14 +531,19 @@ public class TelnetFSM {
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Check if REJECT
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REJECT) {
|
||||
// Rejection
|
||||
pos++;
|
||||
int reason = -1;
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_REASON) {
|
||||
pos++;
|
||||
if (pos < data.length) {
|
||||
reason = data[pos] & 0xFF;
|
||||
int reason = (pos < data.length) ? (data[pos] & 0xFF) : REASON_UNSUPPORTED_REQ;
|
||||
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) {
|
||||
log.warning("TN3270E device-type rejected (" +
|
||||
TN3270EConstants.reasonName(reason) + "), retrying with IBM-3278-2");
|
||||
config.setModel(TerminalModel.IBM_3278_2);
|
||||
sendTN3270EDeviceTypeRequest();
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.warning("TN3270E device-type rejected: " + TN3270EConstants.reasonName(reason));
|
||||
@@ -563,33 +611,86 @@ public class TelnetFSM {
|
||||
log.info("SENT SB TN3270E FUNCTIONS REQUEST " + funcNames + " SE");
|
||||
}
|
||||
|
||||
private void handleTN3270EFunctions(byte[] data) {
|
||||
// Parse: TN3270E FUNCTIONS IS [func...]
|
||||
int pos = 2; // Skip TN3270E, FUNCTIONS
|
||||
private void sendTN3270EFunctionsIs() {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(IAC);
|
||||
out.write(SB);
|
||||
out.write(TELOPT_TN3270E);
|
||||
out.write(OP_FUNCTIONS);
|
||||
out.write(OP_IS);
|
||||
|
||||
if (pos < data.length && (data[pos] & 0xFF) == OP_IS) {
|
||||
pos++; // Skip IS
|
||||
StringBuilder funcNames = new StringBuilder();
|
||||
for (int i = 0; i < eFuncs.length; i++) {
|
||||
if (eFuncs[i]) {
|
||||
out.write(i);
|
||||
if (funcNames.length() > 0) funcNames.append(" ");
|
||||
funcNames.append(TN3270EConstants.functionName(i));
|
||||
}
|
||||
}
|
||||
|
||||
// The remaining bytes are the agreed-upon functions
|
||||
java.util.Arrays.fill(eFuncs, false);
|
||||
StringBuilder funcNames = new StringBuilder();
|
||||
out.write(IAC);
|
||||
out.write(SE);
|
||||
sendBytes(out.toByteArray());
|
||||
log.info("SENT SB TN3270E FUNCTIONS IS " + funcNames + " SE");
|
||||
}
|
||||
|
||||
private void handleTN3270EFunctions(byte[] data) {
|
||||
// Parse: TN3270E FUNCTIONS REQUEST [func...] or TN3270E FUNCTIONS IS [func...]
|
||||
int pos = 1;
|
||||
boolean isRequest = false;
|
||||
|
||||
while (pos < data.length) {
|
||||
int b = data[pos] & 0xFF;
|
||||
if (b == OP_FUNCTIONS) {
|
||||
pos++;
|
||||
} else if (b == OP_REQUEST) {
|
||||
isRequest = true;
|
||||
pos++;
|
||||
break;
|
||||
} else if (b == OP_IS) {
|
||||
isRequest = false;
|
||||
pos++;
|
||||
break;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
// The remaining bytes are the proposed/agreed functions
|
||||
boolean[] hostFuncs = new boolean[8];
|
||||
while (pos < data.length) {
|
||||
int func = data[pos] & 0xFF;
|
||||
if (func <= FUNC_SNA_SENSE) {
|
||||
eFuncs[func] = true;
|
||||
if (funcNames.length() > 0) funcNames.append(" ");
|
||||
funcNames.append(TN3270EConstants.functionName(func));
|
||||
hostFuncs[func] = true;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (isRequest) {
|
||||
// Host sent FUNCTIONS REQUEST -> We reply with FUNCTIONS IS (intersection of functions)
|
||||
for (int i = 0; i < eFuncs.length; i++) {
|
||||
eFuncs[i] = eFuncs[i] && hostFuncs[i];
|
||||
}
|
||||
sendTN3270EFunctionsIs();
|
||||
} else {
|
||||
// Host sent FUNCTIONS IS -> Accept host's agreed function list
|
||||
for (int i = 0; i < eFuncs.length; i++) {
|
||||
eFuncs[i] = hostFuncs[i];
|
||||
}
|
||||
}
|
||||
|
||||
tn3270eNegotiated = true;
|
||||
log.info("TN3270E functions IS: " + funcNames);
|
||||
log.info("TN3270E functions negotiated: " + getNegotiatedFunctionNames());
|
||||
log.info("TN3270E negotiation complete");
|
||||
|
||||
// Move to CONNECTED_UNBOUND or CONNECTED_SSCP
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
// RFC 2355: If BIND-IMAGE function is not negotiated, the emulation session is considered
|
||||
// to be bound immediately upon completion of the FUNCTIONS negotiation.
|
||||
if (eFuncs[FUNC_BIND_IMAGE]) {
|
||||
changeState(ConnectionState.CONNECTED_UNBOUND);
|
||||
} else {
|
||||
changeState(ConnectionState.CONNECTED_TN3270E);
|
||||
tn3270eSubmode = TN3270ESubmode.E_3270;
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
for (ConnectionListener l : connectionListeners) {
|
||||
@@ -597,12 +698,29 @@ public class TelnetFSM {
|
||||
}
|
||||
}
|
||||
|
||||
private String getNegotiatedFunctionNames() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < eFuncs.length; i++) {
|
||||
if (eFuncs[i]) {
|
||||
if (sb.length() > 0) sb.append(" ");
|
||||
sb.append(TN3270EConstants.functionName(i));
|
||||
}
|
||||
}
|
||||
return sb.length() > 0 ? sb.toString() : "<none>";
|
||||
}
|
||||
|
||||
// ========== End of Record processing ==========
|
||||
|
||||
private void processEndOfRecord() {
|
||||
byte[] data = ibuf.toByteArray();
|
||||
ibuf.reset();
|
||||
if (data.length == 0) return;
|
||||
|
||||
if (connectionState == ConnectionState.TELNET_PENDING && !tn3270eNegotiated) {
|
||||
log.info("Received EOR during TELNET_PENDING - transitioning to plain TN3270 mode");
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
}
|
||||
|
||||
if (tn3270eNegotiated) {
|
||||
// TN3270E mode: data starts with 5-byte header
|
||||
processTN3270ERecord(data);
|
||||
@@ -701,11 +819,11 @@ public class TelnetFSM {
|
||||
bindRa = screenBuffer.getMaxRows();
|
||||
bindCa = screenBuffer.getMaxCols();
|
||||
break;
|
||||
case 0x7e:
|
||||
case 0x7E:
|
||||
// Both default and alternate = specified values
|
||||
bindRa = bindRd; bindCa = bindCd;
|
||||
break;
|
||||
case 0x7f:
|
||||
case 0x7F:
|
||||
// Default and alternate are both specified separately
|
||||
bindRa = data[EH_SIZE + BIND_OFF_RA] & 0xFF;
|
||||
bindCa = data[EH_SIZE + BIND_OFF_CA] & 0xFF;
|
||||
@@ -767,7 +885,20 @@ public class TelnetFSM {
|
||||
break;
|
||||
|
||||
default:
|
||||
log.info("Unhandled TN3270E data type: " + dataType);
|
||||
// Check if the host sent a raw 3270 data stream (e.g. 0xF5 EraseWrite, 0x7E EW Alternate, 0xF1 Write, etc.)
|
||||
// This happens when the server ignores or drops TN3270E framing and speaks plain 3270 data stream.
|
||||
if (dataType == 0xF5 || dataType == 0x7E || dataType == 0xF1 || dataType == 0x6F ||
|
||||
dataType == 0x6E || dataType == 0xF2 || dataType == 0xF6 || dataType == 0x05 ||
|
||||
dataType == 0x0D || dataType == 0x01 || (data.length >= 2 && (data[0] & 0xFF) == 0x11)) {
|
||||
log.warning("Received plain 3270 command (0x" + Integer.toHexString(dataType) +
|
||||
") in TN3270E mode — automatically switching to plain TN3270 mode");
|
||||
tn3270eNegotiated = false;
|
||||
changeState(ConnectionState.CONNECTED_3270);
|
||||
dsProcessor.processRecord(data, 0, data.length, true);
|
||||
notifyScreenUpdate();
|
||||
} else {
|
||||
log.info("Unhandled TN3270E data type: " + dataType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -790,7 +921,7 @@ public class TelnetFSM {
|
||||
if (connectionState != ConnectionState.TELNET_PENDING) return;
|
||||
|
||||
// For TN3270E, we wait for TN3270E negotiation to complete
|
||||
if (myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
|
||||
if (config.isTn3270eEnabled() && myOpts[TELOPT_TN3270E] && hisOpts[TELOPT_TN3270E]) {
|
||||
return; // TN3270E in progress
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,27 @@ public class QueryReplyBuilderTest {
|
||||
private final QueryReplyBuilder qrBuilder = new QueryReplyBuilder(screen);
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesDefaultNone() {
|
||||
assertEquals(GraphicsMode.NONE, qrBuilder.getGraphicsMode());
|
||||
public void testBuildAllQueryRepliesDefaultBoth() {
|
||||
assertEquals(GraphicsMode.BOTH, qrBuilder.getGraphicsMode());
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
assertTrue(replies.length > 0);
|
||||
assertEquals((byte) AID_SF, replies[0]);
|
||||
|
||||
// In GraphicsMode.BOTH, Vector Graphics QR 0xB0 must be present
|
||||
boolean hasB0 = false;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_GRAPHICS) {
|
||||
hasB0 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(hasB0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildAllQueryRepliesExplicitNone() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.NONE);
|
||||
byte[] replies = qrBuilder.buildAllQueryReplies(80, 43, 80 * 43);
|
||||
assertNotNull(replies);
|
||||
assertTrue(replies.length > 0);
|
||||
@@ -156,4 +175,61 @@ public class QueryReplyBuilderTest {
|
||||
assertEquals(80, altCols);
|
||||
assertEquals(43, altRows);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgrammedSymbolsDescriptorsSingleAndTriplePlane() {
|
||||
qrBuilder.setGraphicsMode(GraphicsMode.PROGRAMMED_SYMBOLS);
|
||||
byte[] requested = new byte[] { (byte) QR_CHARSETS };
|
||||
byte[] replies = qrBuilder.buildQueryReplies(requested, 80, 43, 80 * 43);
|
||||
|
||||
assertNotNull(replies);
|
||||
int offset = -1;
|
||||
for (int i = 0; i < replies.length - 3; i++) {
|
||||
if ((replies[i] & 0xFF) == 0x81 && (replies[i + 1] & 0xFF) == QR_CHARSETS) {
|
||||
offset = i + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertTrue(offset >= 0, "QR_CHARSETS must be present");
|
||||
|
||||
// QR_CHARSETS payload structure:
|
||||
// Flags (2 bytes), SDW (1 byte), SDH (1 byte), Form (1 byte), DevType (2 bytes), Res (1 byte), DL (1 byte)
|
||||
// DL is at offset + 8, and is 7 bytes per descriptor.
|
||||
int dl = replies[offset + 8] & 0xFF;
|
||||
assertEquals(7, dl);
|
||||
|
||||
int descOffset = offset + 9;
|
||||
|
||||
// Descriptor 1: SET 0 (Base) -> flags 0x10
|
||||
assertEquals(0x00, replies[descOffset] & 0xFF);
|
||||
assertEquals(0x10, replies[descOffset + 1] & 0xFF);
|
||||
|
||||
// Descriptor 2: SET 1 (APL) -> flags 0x00
|
||||
assertEquals(0x01, replies[descOffset + 7] & 0xFF);
|
||||
assertEquals(0x00, replies[descOffset + 7 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 3: PSA (Single plane) -> flags 0x80 (Loadable, single-plane)
|
||||
assertEquals(0x02, replies[descOffset + 14] & 0xFF);
|
||||
assertEquals(0x80, replies[descOffset + 14 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 4: PSB (Single plane) -> flags 0x80 (Loadable, single-plane)
|
||||
assertEquals(0x03, replies[descOffset + 21] & 0xFF);
|
||||
assertEquals(0x80, replies[descOffset + 21 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 5: PSC (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x04, replies[descOffset + 28] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 28 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 6: PSD (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x05, replies[descOffset + 35] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 35 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 7: PSE (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x06, replies[descOffset + 42] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 42 + 1] & 0xFF);
|
||||
|
||||
// Descriptor 8: PSF (Triple plane) -> flags 0xC0 (Loadable | Triple-plane)
|
||||
assertEquals(0x07, replies[descOffset + 49] & 0xFF);
|
||||
assertEquals(0xC0, replies[descOffset + 49 + 1] & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ public class GraphicsModeTest {
|
||||
@Test
|
||||
public void testConnectionConfigDefault() {
|
||||
ConnectionConfig config = new ConnectionConfig();
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(GraphicsMode.BOTH);
|
||||
assertEquals(GraphicsMode.BOTH, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(GraphicsMode.NONE);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
|
||||
config.setGraphicsMode(null);
|
||||
assertEquals(GraphicsMode.NONE, config.getGraphicsMode());
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class ProgramSymbolManagerTest {
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(16, slot.getHeight());
|
||||
assertEquals(12, slot.getHeight());
|
||||
assertFalse(slot.isTriplePlane());
|
||||
|
||||
// Verify pixel data (Row 0 Col 0 is 1)
|
||||
@@ -64,10 +64,33 @@ public class ProgramSymbolManagerTest {
|
||||
int bg = 0xFF000000; // Black
|
||||
int[] rgb = slot.getRgbPixels(fg, bg);
|
||||
assertNotNull(rgb);
|
||||
assertEquals(9 * 16, rgb.length);
|
||||
assertEquals(9 * 12, rgb.length);
|
||||
assertEquals(fg, rgb[0]); // Row 0 Col 0 pixel should be foreground Green
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExplicitCellDimensions() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
manager.setDefaultCellDimensions(9, 16);
|
||||
|
||||
byte[] payload = new byte[4 + 18];
|
||||
payload[0] = 0x01; // Format 1
|
||||
payload[1] = 0x40; // LCID 0x40
|
||||
payload[2] = 0x41; // Code point 0x41
|
||||
payload[3] = 0x02; // RWS 2
|
||||
payload[4] = (byte) 0x80;
|
||||
payload[5] = (byte) 0x00;
|
||||
for (int r = 0; r < 16; r++) {
|
||||
payload[6 + r] = (byte) 0xFF;
|
||||
}
|
||||
|
||||
manager.loadps(payload);
|
||||
ProgramSymbolSet.SymbolSlot slot = manager.getSymbol(0x40, 0x41);
|
||||
assertNotNull(slot);
|
||||
assertEquals(9, slot.getWidth());
|
||||
assertEquals(16, slot.getHeight());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTriplePlaneMultiColorComposite() {
|
||||
ProgramSymbolManager manager = new ProgramSymbolManager();
|
||||
|
||||
Reference in New Issue
Block a user