ibm5250 0.1.0.dev7__tar.gz → 0.1.0.dev8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/PKG-INFO +1 -1
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/pyproject.toml +1 -1
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/__init__.py +2 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/connection.py +29 -25
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/constants.py +79 -44
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/data-stream-logic.md +100 -38
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/datastream.py +153 -210
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/screen.py +22 -17
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/terminal.py +60 -68
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/uv.lock +1 -1
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/.github/copilot-instructions.md +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/.github/workflows/README.md +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/.github/workflows/publish.yml +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/.gitignore +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/LICENSE +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/README.md +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/scripts/compute_version.py +0 -0
- {ibm5250-0.1.0.dev7 → ibm5250-0.1.0.dev8}/src/ibm5250/exceptions.py +0 -0
|
@@ -19,6 +19,7 @@ from .constants import (
|
|
|
19
19
|
FFW,
|
|
20
20
|
SCREEN_COLS,
|
|
21
21
|
SCREEN_ROWS,
|
|
22
|
+
Attr,
|
|
22
23
|
Command,
|
|
23
24
|
Order,
|
|
24
25
|
TelnetCmd,
|
|
@@ -65,6 +66,7 @@ __all__ = [
|
|
|
65
66
|
"TelnetCmd",
|
|
66
67
|
"TelnetOpt",
|
|
67
68
|
"FFW",
|
|
69
|
+
"Attr",
|
|
68
70
|
"SCREEN_ROWS",
|
|
69
71
|
"SCREEN_COLS",
|
|
70
72
|
# Address helpers
|
|
@@ -26,7 +26,6 @@ from dataclasses import dataclass
|
|
|
26
26
|
|
|
27
27
|
from .constants import (
|
|
28
28
|
AID,
|
|
29
|
-
CC2,
|
|
30
29
|
Command,
|
|
31
30
|
TelnetCmd,
|
|
32
31
|
TelnetOpt,
|
|
@@ -49,7 +48,6 @@ _TELNET_OPT_CMD_LEN = 3 # bytes: IAC + command + option (WILL/WONT/DO/DONT)
|
|
|
49
48
|
# SNA / TN5250E wire-format framing constants
|
|
50
49
|
_GDS_RECORD_ID_HI = 0x12 # SNA GDS record-type identifier, high byte
|
|
51
50
|
_GDS_RECORD_ID_LO = 0xA0 # SNA GDS record-type identifier, low byte
|
|
52
|
-
_SAVE_SCREEN_RESP_CC1 = 0x01 # CC1 value in the Save Screen response variable header
|
|
53
51
|
|
|
54
52
|
# TN5250E header byte 7 (request/response flags). Real emulators set bit 0x40
|
|
55
53
|
# here to signal the Attention (Attn) key -- a header-only INPUT record with no
|
|
@@ -73,6 +71,20 @@ _MAX_NEG_PASSES = 40 # Maximum Telnet option exchange iterations during negotia
|
|
|
73
71
|
|
|
74
72
|
_TN5250E_HEADER_LEN = 10
|
|
75
73
|
|
|
74
|
+
# The header splits into a 6-byte fixed part (length, GDS id, flow type) and a
|
|
75
|
+
# variable-length part whose length is given by byte 6 -- so the 5250 payload
|
|
76
|
+
# begins at ``6 + header[6]`` (normally 6 + 4 = 10).
|
|
77
|
+
_TN5250E_FIXED_HEADER_LEN = 6
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _payload_offset(frame: bytes) -> int:
|
|
81
|
+
"""Return the offset of the 5250 payload within a TN5250E *frame*."""
|
|
82
|
+
offset = _TN5250E_FIXED_HEADER_LEN + frame[_TN5250E_FIXED_HEADER_LEN]
|
|
83
|
+
if not _TN5250E_HEADER_LEN <= offset <= len(frame):
|
|
84
|
+
# Malformed variable-header length; fall back to the standard 10 bytes.
|
|
85
|
+
return _TN5250E_HEADER_LEN
|
|
86
|
+
return offset
|
|
87
|
+
|
|
76
88
|
|
|
77
89
|
@dataclass
|
|
78
90
|
class TN5250EHeader:
|
|
@@ -325,23 +337,20 @@ class TN5250Connection:
|
|
|
325
337
|
frame = _build_tn5250e_frame(req_resp=0x00, opcode=_TN5250E_OP_CANCEL_INVITE)
|
|
326
338
|
self._send_frame(frame, description="Sending Cancel-Invite response")
|
|
327
339
|
|
|
328
|
-
def send_save_screen_response(self) -> None:
|
|
340
|
+
def send_save_screen_response(self, save_id: bytes) -> None:
|
|
329
341
|
"""Send the TN5250E-level Save Screen acknowledgment.
|
|
330
342
|
|
|
331
343
|
When the server issues an ESC+SAVE_SCREEN (0x04 0x02) command the
|
|
332
344
|
terminal must reply with a matching TN5250E frame whose variable header
|
|
333
|
-
encodes ESC+RESTORE_SCREEN+
|
|
334
|
-
|
|
335
|
-
|
|
345
|
+
encodes ESC + RESTORE_SCREEN + a two-byte identifier for the screen it
|
|
346
|
+
just saved. The host stores that identifier and echoes it back in the
|
|
347
|
+
Restore Screen command when it wants that particular screen displayed
|
|
348
|
+
again, so every save must use a distinct *save_id*.
|
|
336
349
|
"""
|
|
337
350
|
if not self._tn5250e_active:
|
|
338
351
|
return # save/restore handshake only occurs in TN5250E mode
|
|
339
352
|
|
|
340
|
-
|
|
341
|
-
# byte 9 of the fixed header carries the variable-header length.
|
|
342
|
-
var_hdr = bytes(
|
|
343
|
-
[Command.ESC, Command.RESTORE_SCREEN, _SAVE_SCREEN_RESP_CC1, CC2.RESET_MDT]
|
|
344
|
-
)
|
|
353
|
+
var_hdr = bytes([Command.ESC, Command.RESTORE_SCREEN]) + bytes(save_id)
|
|
345
354
|
frame = _build_tn5250e_frame(req_resp=0x00, opcode=len(var_hdr), var_hdr=var_hdr)
|
|
346
355
|
self._send_frame(frame, description="Sending Save Screen response")
|
|
347
356
|
|
|
@@ -354,8 +363,8 @@ class TN5250Connection:
|
|
|
354
363
|
log.debug("Short TN5250E frame (%d bytes), skipping", len(frame))
|
|
355
364
|
continue
|
|
356
365
|
hdr = TN5250EHeader.from_bytes(frame)
|
|
357
|
-
self._last_recv_opcode = frame[9] # byte 9: opcode
|
|
358
|
-
payload = bytes(frame[
|
|
366
|
+
self._last_recv_opcode = frame[9] # byte 9: opcode
|
|
367
|
+
payload = bytes(frame[_payload_offset(frame) :])
|
|
359
368
|
log.debug(
|
|
360
369
|
"← TN5250E type=%02X data_type=%02X payload=%d bytes",
|
|
361
370
|
hdr.record_type,
|
|
@@ -365,20 +374,16 @@ class TN5250Connection:
|
|
|
365
374
|
else:
|
|
366
375
|
frame_bytes = bytes(frame)
|
|
367
376
|
# IBM i sends TN5250E-formatted records even when TN5250E was
|
|
368
|
-
# not formally negotiated. The
|
|
369
|
-
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
372
|
-
# 5250 payload.
|
|
377
|
+
# not formally negotiated. The header begins with a big-endian
|
|
378
|
+
# 16-bit record length that equals the total frame size, and
|
|
379
|
+
# byte 6 holds the length of the variable header that follows
|
|
380
|
+
# it — the real 5250 payload starts after that.
|
|
373
381
|
if len(frame_bytes) >= _TN5250E_HEADER_LEN and int.from_bytes(
|
|
374
382
|
frame_bytes[:2], "big"
|
|
375
383
|
) == len(frame_bytes):
|
|
376
384
|
hdr = TN5250EHeader.from_bytes(frame_bytes)
|
|
377
385
|
self._last_recv_opcode = frame_bytes[9] # byte 9: opcode
|
|
378
|
-
|
|
379
|
-
9
|
|
380
|
-
] # RFC 2877 variable-header length field
|
|
381
|
-
skip = _TN5250E_HEADER_LEN + var_hdr_len
|
|
386
|
+
skip = _payload_offset(frame_bytes)
|
|
382
387
|
payload = frame_bytes[skip:]
|
|
383
388
|
# Server is speaking TN5250E — our responses must include the header too
|
|
384
389
|
if not self._tn5250e_active:
|
|
@@ -387,9 +392,8 @@ class TN5250Connection:
|
|
|
387
392
|
)
|
|
388
393
|
self._tn5250e_active = True
|
|
389
394
|
log.debug(
|
|
390
|
-
"← implicit TN5250E header stripped (
|
|
391
|
-
|
|
392
|
-
var_hdr_len,
|
|
395
|
+
"← implicit TN5250E header stripped (%d bytes) data_type=%02X payload=%d bytes",
|
|
396
|
+
skip,
|
|
393
397
|
hdr.data_type,
|
|
394
398
|
len(payload),
|
|
395
399
|
)
|
|
@@ -83,11 +83,12 @@ class TN5250ESubNeg:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
class Command:
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
ESC = 0x04 # precedes every 5250 command in a record
|
|
87
|
+
CLEAR_UNIT = 0x40 # erase the display, reset it to the primary size (24×80)
|
|
88
|
+
CLEAR_UNIT_ALTERNATE = (
|
|
89
|
+
0x20 # erase the display, switch to the alternate size (27×132);
|
|
90
|
+
# followed by a one-byte parameter (0x00 or 0x80)
|
|
88
91
|
)
|
|
89
|
-
ESC = 0x04 # precedes Write Structured Field inline
|
|
90
|
-
CLEAR_UNIT = 0x40 # erase the display and format table
|
|
91
92
|
WRITE_TO_DISPLAY = (
|
|
92
93
|
0x11 # main screen update command; followed by CC1/CC2 and orders
|
|
93
94
|
)
|
|
@@ -105,16 +106,16 @@ class Command:
|
|
|
105
106
|
|
|
106
107
|
# Write to Display control characters (CC1 / CC2)
|
|
107
108
|
class CC1:
|
|
108
|
-
"""Control Character 1 — first byte after WTD command.
|
|
109
|
+
"""Control Character 1 — first byte after the WTD command.
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
CC1 selects which Modified Data Tags and input-field buffers the host wants
|
|
112
|
+
reset before the write is applied. It does **not** erase the screen and it
|
|
113
|
+
does **not** change the screen dimensions — those are driven exclusively by
|
|
114
|
+
the Clear Unit (:data:`Command.CLEAR_UNIT`) and Clear Unit Alternate
|
|
115
|
+
(:data:`Command.CLEAR_UNIT_ALTERNATE`) commands.
|
|
112
116
|
"""
|
|
113
117
|
|
|
114
118
|
RESET = 0x00
|
|
115
|
-
CLEAR_UNIT = 0x20 # Erase entire screen before write
|
|
116
|
-
CLEAR_UNIT_ALT = 0x40 # Erase screen using alternate screen dimensions
|
|
117
|
-
CLEAR_FORMAT_TABLE = 0x60 # Discard field definitions only; no screen erase
|
|
118
119
|
SOUND_ALARM = 0x80 # Audible alarm
|
|
119
120
|
|
|
120
121
|
|
|
@@ -133,22 +134,32 @@ class CC2:
|
|
|
133
134
|
# ---------------------------------------------------------------------------
|
|
134
135
|
|
|
135
136
|
class Order:
|
|
137
|
+
"""5250 orders embedded in a Write to Display data stream.
|
|
138
|
+
|
|
139
|
+
Note these are *5250* order codes -- they differ from the 3270 codes of the
|
|
140
|
+
same name (e.g. 5250 Repeat to Address is 0x02, not 0x3C).
|
|
141
|
+
"""
|
|
142
|
+
|
|
136
143
|
START_OF_HEADER = 0x01 # Start of Header (variable-length; skip past it)
|
|
144
|
+
REPEAT_TO_ADDRESS = 0x02 # Repeat to Address — 2 address bytes + 1 fill char
|
|
145
|
+
ERASE_TO_ADDRESS = (
|
|
146
|
+
0x03 # Erase to Address — 2 address bytes + length + attribute types
|
|
147
|
+
)
|
|
137
148
|
TRANSPARENT_DATA = 0x10 # Transparent Data — 2-byte length + raw EBCDIC
|
|
138
149
|
SET_BUFFER_ADDRESS = 0x11 # Set Buffer Address — 2 address bytes
|
|
139
|
-
|
|
140
|
-
0x12 #
|
|
150
|
+
WRITE_EXTENDED_ATTRIBUTE = (
|
|
151
|
+
0x12 # Write Extended Attribute — 2 bytes; not emulated
|
|
141
152
|
)
|
|
142
153
|
INSERT_CURSOR = 0x13 # Insert Cursor — 2 address bytes; marks cursor pos
|
|
143
154
|
MOVE_CURSOR = 0x14 # Move Cursor — 2 address bytes
|
|
144
|
-
|
|
155
|
+
WRITE_DISPLAY_STRUCTURED_FIELD = (
|
|
156
|
+
0x15 # Write to Display Structured Field — 2-byte length + payload
|
|
157
|
+
)
|
|
145
158
|
START_FIELD = 0x1D # Start Field — 2-byte FFW [+ 2-byte FCW pairs]
|
|
146
|
-
SET_ATTRIBUTE = 0x28 # Set Attribute — 1 attribute type + 1 value byte
|
|
147
|
-
MODIFY_FIELD = 0x2C # Modify Field — like SF but doesn't clear data
|
|
148
|
-
REPEAT_TO_ADDRESS = 0x3C # Repeat to Address — 2 address bytes + 1 fill char
|
|
149
159
|
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
|
|
161
|
+
KNOWN_ORDERS = frozenset(
|
|
162
|
+
v for k, v in vars(Order).items() if isinstance(v, int) and not k.startswith("_")
|
|
152
163
|
)
|
|
153
164
|
|
|
154
165
|
# ---------------------------------------------------------------------------
|
|
@@ -218,36 +229,56 @@ class AID:
|
|
|
218
229
|
|
|
219
230
|
|
|
220
231
|
class FFW:
|
|
221
|
-
"""Field Format Word bits (byte 0 and byte 1 of the 2-byte FFW).
|
|
232
|
+
"""Field Format Word bits (byte 0 and byte 1 of the 2-byte FFW).
|
|
222
233
|
|
|
223
|
-
|
|
234
|
+
Note the FFW carries *behaviour* only. A field's visual appearance comes
|
|
235
|
+
from the separate attribute byte that follows the FFW/FCWs in the SF order
|
|
236
|
+
(see :class:`Attr`).
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
# Byte 0 — bit 0x40 is the "this is an FFW" identifier and is always set.
|
|
240
|
+
IDENTIFIER = 0x40 # Marks the byte as a Field Format Word rather than an attribute
|
|
224
241
|
BYPASS = 0x20 # Protected field — no input allowed (read-only)
|
|
225
242
|
DUP_ENABLE = 0x10 # Dup key enabled — user can duplicate the previous field's value
|
|
226
|
-
MDT =
|
|
227
|
-
|
|
228
|
-
FER = 0x04 # Field Exit Required — user must press Field Exit before leaving
|
|
229
|
-
|
|
230
|
-
# Byte 1 (keyboard shift / display attribute)
|
|
243
|
+
MDT = 0x08 # Modified Data Tag — set when the field has been touched
|
|
244
|
+
# Byte 0, low 3 bits — field type (keyboard shift)
|
|
231
245
|
ALPHA_SHIFT = 0x00 # Alphabetic shift — accepts any character
|
|
232
|
-
ALPHA_ONLY =
|
|
233
|
-
NUM_SHIFT =
|
|
234
|
-
NUM_ONLY =
|
|
235
|
-
KATA_SHIFT =
|
|
236
|
-
DIGITS_ONLY =
|
|
237
|
-
|
|
238
|
-
SIGNED_NUM =
|
|
239
|
-
#
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
ALPHA_ONLY = 0x01 # Alphabetic only — rejects digits and special characters
|
|
247
|
+
NUM_SHIFT = 0x02 # Numeric shift — defaults keyboard to numeric mode
|
|
248
|
+
NUM_ONLY = 0x03 # Numeric only — rejects non-numeric input
|
|
249
|
+
KATA_SHIFT = 0x04 # Katakana shift — Japanese katakana character entry
|
|
250
|
+
DIGITS_ONLY = 0x05 # Digits only — accepts 0–9 with no sign
|
|
251
|
+
MAG_READER = 0x06 # Magnetic stripe reader input only
|
|
252
|
+
SIGNED_NUM = 0x07 # Signed numeric — accepts digits plus a trailing sign character
|
|
253
|
+
FIELD_TYPE_MASK = 0x07 # Bitmask for the field type bits of byte 0
|
|
254
|
+
|
|
255
|
+
# Byte 1
|
|
256
|
+
AUTO_ENTER = 0x80 # Auto Enter — automatically submit when the field is filled
|
|
257
|
+
FER = 0x40 # Field Exit Required — user must press Field Exit before leaving
|
|
258
|
+
MONOCASE = 0x20 # Monocase — lower-case input is folded to upper case
|
|
259
|
+
MANDATORY = 0x08 # Mandatory entry — the field may not be left empty
|
|
260
|
+
RIGHT_ZERO = 0x05 # Right-adjust, zero fill
|
|
261
|
+
RIGHT_BLANK = 0x06 # Right-adjust, blank fill
|
|
262
|
+
MANDATORY_FILL = 0x07 # Mandatory fill — the field must be filled completely
|
|
263
|
+
MANDATORY_FILL_MASK = 0x07 # Bitmask for the adjust/fill bits of byte 1
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Attr:
|
|
267
|
+
"""5250 display attribute byte values (0x20-0x3F).
|
|
268
|
+
|
|
269
|
+
Every attribute occupies one display position and governs the appearance of
|
|
270
|
+
the characters that follow it until the next attribute byte.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
NORMAL = 0x20 # Green
|
|
274
|
+
REVERSE = 0x21 # Green, reverse image
|
|
275
|
+
HIGH_INTENSITY = 0x22 # White
|
|
276
|
+
UNDERLINE = 0x24 # Green, underlined
|
|
277
|
+
NON_DISPLAY = 0x27 # Invisible — used for passwords and command strings
|
|
278
|
+
RED = 0x28 # Red
|
|
279
|
+
BASE = 0x20 # All attribute bytes have this bit pattern in their high bits
|
|
280
|
+
MASK = 0xE0 # ``byte & MASK == BASE`` identifies an attribute byte
|
|
281
|
+
NON_DISPLAY_MASK = 0x07 # ``byte & MASK == 0x07`` means non-display
|
|
251
282
|
|
|
252
283
|
|
|
253
284
|
# ---------------------------------------------------------------------------
|
|
@@ -436,6 +467,10 @@ SCREEN_COLS = 80
|
|
|
436
467
|
SCREEN_ROWS_27 = 27 # 27×132 model
|
|
437
468
|
SCREEN_COLS_132 = 132
|
|
438
469
|
|
|
470
|
+
# Length of the screen identifier the workstation returns in its Save Screen
|
|
471
|
+
# acknowledgment and that the host echoes back in a Restore Screen command.
|
|
472
|
+
SAVE_SCREEN_ID_LEN = 2
|
|
473
|
+
|
|
439
474
|
EBCDIC_SPACE = 0x40
|
|
440
475
|
EBCDIC_NULL = 0x00
|
|
441
476
|
|
|
@@ -19,7 +19,11 @@ The **TN5250Connection** manages the TCP socket to the IBM i (AS/400) host.
|
|
|
19
19
|
1. Reads bytes from the socket into a buffer.
|
|
20
20
|
2. Scans for the IAC-EOR (Interpret As Command — End Of Record) sequence (0xFF 0xEF) which marks the end of a logical record.
|
|
21
21
|
3. Un-escapes any doubled IAC bytes (0xFF 0xFF → 0xFF).
|
|
22
|
-
4. If TN5250E is active, strips the
|
|
22
|
+
4. If TN5250E is active, strips the header → returns the raw 5250 payload. The
|
|
23
|
+
header is six fixed bytes followed by a variable part whose **length is byte 6**
|
|
24
|
+
(normally 4, giving the familiar 10-byte total), so the payload starts at
|
|
25
|
+
`6 + frame[6]`. Byte 9 is the _opcode_, not the header length — confusing the
|
|
26
|
+
two silently truncates the first bytes of the payload.
|
|
23
27
|
5. If the socket has no data within the timeout, raises **RecvTimeout**.
|
|
24
28
|
|
|
25
29
|
### Sending (`send_record`)
|
|
@@ -33,7 +37,12 @@ The **TN5250Connection** manages the TCP socket to the IBM i (AS/400) host.
|
|
|
33
37
|
|
|
34
38
|
### Save Screen Response (`send_save_screen_response`)
|
|
35
39
|
|
|
36
|
-
When the server issues a SAVE_SCREEN command, the terminal must reply with a
|
|
40
|
+
When the server issues a SAVE_SCREEN command, the terminal must reply with a
|
|
41
|
+
TN5250E-framed acknowledgment carrying `ESC + RESTORE_SCREEN + a two-byte
|
|
42
|
+
identifier`. This tells the server "I've saved the screen under this name; you
|
|
43
|
+
can now overlay it." **Every save must use a distinct identifier** — the server
|
|
44
|
+
stores it and echoes it back later to say _which_ saved screen it wants
|
|
45
|
+
restored. See [Save/Restore Screen Flow](#saverestore-screen-flow).
|
|
37
46
|
|
|
38
47
|
---
|
|
39
48
|
|
|
@@ -43,28 +52,37 @@ The **DataStreamParser** takes a raw 5250 payload (one record from the connectio
|
|
|
43
52
|
|
|
44
53
|
### Record Structure
|
|
45
54
|
|
|
46
|
-
Every
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
55
|
+
Every command in a record is introduced by **ESC (`0x04`)**, and one record may
|
|
56
|
+
carry several commands back to back:
|
|
57
|
+
|
|
58
|
+
| Command | Meaning |
|
|
59
|
+
| ------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
60
|
+
| `0x11` (Write to Display) | Main screen update — followed by CC1, CC2, then orders/data |
|
|
61
|
+
| `0x40` (Clear Unit) | Erases the display and field definitions and selects the **primary** size (24×80) |
|
|
62
|
+
| `0x20` (Clear Unit Alternate) | Erases the display and selects the **alternate** size (27×132); takes one parameter byte |
|
|
63
|
+
| `0x02` (Save Screen) | Server wants the client to save the current screen |
|
|
64
|
+
| `0x12` (Restore Screen) | Server wants a previously saved screen restored; followed by its two-byte identifier |
|
|
65
|
+
| `0xF3` (Write Structured Field) | Carries structured data like Query commands |
|
|
66
|
+
| `0x42/0x52/0x72/0x62` | Read commands — server asks client to send back screen/field data |
|
|
67
|
+
| `0x21` (Write Error Code) | Display error on status line |
|
|
68
|
+
| `0x50` (Clear Format Table) | Discard field definitions |
|
|
69
|
+
|
|
70
|
+
### Screen size
|
|
71
|
+
|
|
72
|
+
The display size is **absolute and set only by the clear commands**: Clear Unit
|
|
73
|
+
selects 24×80, Clear Unit Alternate selects 27×132. It is never a toggle, and it
|
|
74
|
+
is never inferred from out-of-range buffer addresses. Getting this wrong is what
|
|
75
|
+
made a screen returning from a 132-column layout keep rendering as 27×132 with
|
|
76
|
+
stale content bleeding through.
|
|
59
77
|
|
|
60
78
|
### Control Characters (CC1 / CC2)
|
|
61
79
|
|
|
62
|
-
|
|
80
|
+
Write to Display is followed by two control bytes:
|
|
63
81
|
|
|
64
|
-
- **CC1** —
|
|
65
|
-
|
|
66
|
-
- `
|
|
67
|
-
- `
|
|
82
|
+
- **CC1** — Lock/reset flags. Notably CC1 **neither erases the screen nor
|
|
83
|
+
changes its dimensions**; only the clear commands above do that.
|
|
84
|
+
- `0x01`/`0x02`: Reset pending AIDs / unlock after the write
|
|
85
|
+
- `0x04`: Sound the alarm
|
|
68
86
|
|
|
69
87
|
- **CC2** — Keyboard and field flags:
|
|
70
88
|
- `0x08`: Unlock the keyboard (let the user type)
|
|
@@ -75,24 +93,49 @@ After a WTD or Save Screen command, two bytes control screen-level behavior:
|
|
|
75
93
|
|
|
76
94
|
After the command + CC bytes, the rest of the record is a stream of **orders** intermixed with **data bytes**:
|
|
77
95
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
| **
|
|
84
|
-
| **
|
|
85
|
-
| **
|
|
86
|
-
| **
|
|
87
|
-
| **
|
|
88
|
-
| **
|
|
89
|
-
| **
|
|
96
|
+
These are the **5250** order codes; several share a name with a 3270 order but
|
|
97
|
+
use a different byte value (5250 Repeat to Address is `0x02`, not `0x3C`).
|
|
98
|
+
|
|
99
|
+
| Order | What it does |
|
|
100
|
+
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| **SOH** — Start of Header (`0x01`) + length + data | Metadata block, skipped |
|
|
102
|
+
| **RA** — Repeat to Address (`0x02`) + row + col + char | Repeat a character from the current position up to **and including** the target |
|
|
103
|
+
| **EA** — Erase to Address (`0x03`) + row + col + len + types | Erase (fill with nulls) from the current position through the target. `len` counts itself plus the extended-attribute type bytes that follow |
|
|
104
|
+
| **TD** — Transparent Data (`0x10`) + length + data | Write raw bytes (transparent, no order interpretation) |
|
|
105
|
+
| **SBA** — Set Buffer Address (`0x11`) + row + col | Move the write position to a specific screen cell |
|
|
106
|
+
| **WEA** — Write Extended Attribute (`0x12`) + type + value | Extended character buffer attribute; not emulated, operands are consumed |
|
|
107
|
+
| **IC** — Insert Cursor (`0x13`) | Place the cursor at the 2-byte address that follows (falls back to the current position only when no address bytes remain) |
|
|
108
|
+
| **MC** — Move Cursor (`0x14`) + row + col | Move the cursor to a specific position |
|
|
109
|
+
| **WDSF** — Write to Display Structured Field (`0x15`) | GUI constructs (windows, scrollbars); not emulated, the payload is skipped |
|
|
110
|
+
| **SF** — Start Field (`0x1D`) | Define a field at the current position — see below |
|
|
90
111
|
|
|
91
112
|
Anything that isn't a recognized order byte is treated as **EBCDIC (Extended Binary Coded Decimal Interchange Code)** character data and written sequentially into the screen buffer.
|
|
92
113
|
|
|
93
|
-
###
|
|
114
|
+
### Attribute bytes
|
|
115
|
+
|
|
116
|
+
Bytes in the range **`0x20`–`0x3F`** are display attributes. They are _data_:
|
|
117
|
+
each one occupies a screen position (rendered as a blank) and governs the
|
|
118
|
+
appearance of everything after it until the next attribute. `0x27` — more
|
|
119
|
+
precisely any attribute whose low three bits are all set — means _non-display_,
|
|
120
|
+
which is how password fields and hidden command strings are rendered blank.
|
|
121
|
+
|
|
122
|
+
Because attributes are the only bytes with that bit pattern, they double as the
|
|
123
|
+
terminator for the variable-length part of an SF order.
|
|
124
|
+
|
|
125
|
+
### Start Field (SF)
|
|
94
126
|
|
|
95
|
-
|
|
127
|
+
SF (0x1D) [FFW1 FFW2 [FCW1 FCW2]...] ATTR LEN_HI LEN_LO
|
|
128
|
+
|
|
129
|
+
- An **output-only** field omits the FFW and FCWs entirely: the byte straight
|
|
130
|
+
after SF is then the attribute.
|
|
131
|
+
- The **FFW** (Field Format Word) defines field behaviour — bypass/input,
|
|
132
|
+
keyboard shift, mandatory entry, and so on. Byte 0 always has bit `0x40` set,
|
|
133
|
+
which is how it is told apart from an attribute byte.
|
|
134
|
+
- Zero or more **FCW** (Field Control Word) pairs may follow; the chain ends at
|
|
135
|
+
the first attribute byte, _not_ at a continuation bit.
|
|
136
|
+
- The attribute byte is written at the field's start position, and the field's
|
|
137
|
+
length is the **two bytes after it** — it is not derived from an FCW and it is
|
|
138
|
+
not inferred from where the next field begins.
|
|
96
139
|
|
|
97
140
|
### Write Structured Field (WSF / Query)
|
|
98
141
|
|
|
@@ -167,8 +210,9 @@ This is the central method — it:
|
|
|
167
210
|
2. Feeds it through `parser.parse()` to get a list of `DSEvent` objects.
|
|
168
211
|
3. Iterates through events looking for special commands:
|
|
169
212
|
- **QUERY** → sends back a Query Reply with terminal capabilities
|
|
170
|
-
- **SAVE_SCREEN** → deep-copies the current `Screen
|
|
171
|
-
- **RESTORE_SCREEN** →
|
|
213
|
+
- **SAVE_SCREEN** → deep-copies the current `Screen`, files it under a fresh identifier, and returns that identifier in the save-screen acknowledgment
|
|
214
|
+
- **RESTORE_SCREEN** → looks the identifier the server sent up among the saved screens, makes it current (dropping any newer saves), resizes the parser to its dimensions, then applies any remaining events from the same record on top
|
|
215
|
+
- **RESIZE_SCREEN** → re-creates the parser and screen at the new dimensions
|
|
172
216
|
4. If no RESTORE occurred, applies all events to the screen normally.
|
|
173
217
|
5. **Post-RESTORE keyboard drain**: After restoring a screen, the keyboard is locked (because it was locked when saved). The server sends a follow-up record to unlock it. The terminal tries one more `_recv_and_apply()` to consume that unlock record. If nothing comes (RecvTimeout), it moves on.
|
|
174
218
|
|
|
@@ -194,12 +238,30 @@ This is the central method — it:
|
|
|
194
238
|
This is how the server does "popup" screens (like overlaying a detail view on a list):
|
|
195
239
|
|
|
196
240
|
1. Server sends **SAVE_SCREEN** → terminal saves a copy of the current screen
|
|
241
|
+
under a fresh two-byte identifier and returns that identifier in the Save
|
|
242
|
+
Screen response
|
|
197
243
|
2. Server sends a normal WTD with new content → terminal shows the popup
|
|
198
244
|
3. User interacts with the popup (sends keys, gets responses)
|
|
199
|
-
4. Server sends **RESTORE_SCREEN** → terminal swaps back to
|
|
245
|
+
4. Server sends **RESTORE_SCREEN + identifier** → terminal swaps back to _that_
|
|
246
|
+
saved screen and discards any screens saved after it
|
|
200
247
|
5. Server sends a follow-up WTD to unlock the keyboard on the restored screen
|
|
201
248
|
|
|
202
|
-
|
|
249
|
+
Saves nest, so this is **not** a stack the terminal may simply pop: the server
|
|
250
|
+
decides how far to unwind and names the screen it wants. Unwinding two levels
|
|
251
|
+
in one go — restore `04 05`, Clear Unit, restore `03 04` — is normal, and
|
|
252
|
+
popping blindly restores the wrong screen.
|
|
253
|
+
|
|
254
|
+
A restored screen brings its own dimensions with it, so the parser must be
|
|
255
|
+
re-created to match whenever one is restored.
|
|
256
|
+
|
|
257
|
+
The other tricky part: after step 4, the restored screen has
|
|
258
|
+
`keyboard_locked = True` (it was saved right after the client sent an AID key).
|
|
259
|
+
The terminal must consume the unlock record (step 5) before attempting the next
|
|
260
|
+
`send_key`, otherwise the server rejects the input.
|
|
261
|
+
|
|
262
|
+
Screens restored this way are usually followed by _partial_ updates that repaint
|
|
263
|
+
only the parts of the screen that changed, so the restored snapshot has to be
|
|
264
|
+
byte-accurate — any error in it survives into the final display.
|
|
203
265
|
|
|
204
266
|
---
|
|
205
267
|
|