ibm5250 0.1.0.dev7__tar.gz → 0.1.0.dev9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ibm5250
3
- Version: 0.1.0.dev7
3
+ Version: 0.1.0.dev9
4
4
  Summary: IBM 5250 terminal automation library
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ibm5250"
3
- version = "0.1.0.dev7"
3
+ version = "0.1.0.dev9"
4
4
  description = "IBM 5250 terminal automation library"
5
5
  requires-python = ">=3.11"
6
6
 
@@ -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,
@@ -34,6 +35,9 @@ from .datastream import (
34
35
  DSEventType,
35
36
  FieldResponse,
36
37
  build_aid_response,
38
+ build_save_screen_response,
39
+ decode_save_screen_fields,
40
+ decode_save_screen_image,
37
41
  ebcdic_decode,
38
42
  ebcdic_decode_stripped,
39
43
  ebcdic_encode,
@@ -65,6 +69,7 @@ __all__ = [
65
69
  "TelnetCmd",
66
70
  "TelnetOpt",
67
71
  "FFW",
72
+ "Attr",
68
73
  "SCREEN_ROWS",
69
74
  "SCREEN_COLS",
70
75
  # Address helpers
@@ -79,6 +84,9 @@ __all__ = [
79
84
  "DSEventType",
80
85
  "FieldResponse",
81
86
  "build_aid_response",
87
+ "build_save_screen_response",
88
+ "decode_save_screen_image",
89
+ "decode_save_screen_fields",
82
90
  # EBCDIC helpers
83
91
  "ebcdic_encode",
84
92
  "ebcdic_decode",
@@ -26,8 +26,6 @@ from dataclasses import dataclass
26
26
 
27
27
  from .constants import (
28
28
  AID,
29
- CC2,
30
- Command,
31
29
  TelnetCmd,
32
30
  TelnetOpt,
33
31
  TN5250EDataType,
@@ -49,7 +47,6 @@ _TELNET_OPT_CMD_LEN = 3 # bytes: IAC + command + option (WILL/WONT/DO/DONT)
49
47
  # SNA / TN5250E wire-format framing constants
50
48
  _GDS_RECORD_ID_HI = 0x12 # SNA GDS record-type identifier, high byte
51
49
  _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
50
 
54
51
  # TN5250E header byte 7 (request/response flags). Real emulators set bit 0x40
55
52
  # here to signal the Attention (Attn) key -- a header-only INPUT record with no
@@ -64,6 +61,9 @@ _TN5250E_ATTN_FLAG = 0x40
64
61
  # host will run its Attention program (e.g. the System Request / Assist menu).
65
62
  _TN5250E_OP_CANCEL_INVITE = 0x0A
66
63
 
64
+ # Byte 9 operation code for the Save Screen response record (RFC 2877 §4.3).
65
+ _TN5250E_OP_SAVE_SCREEN_RESPONSE = 0x04
66
+
67
67
  _MAX_NEG_PASSES = 40 # Maximum Telnet option exchange iterations during negotiation
68
68
 
69
69
 
@@ -73,6 +73,15 @@ _MAX_NEG_PASSES = 40 # Maximum Telnet option exchange iterations during negotia
73
73
 
74
74
  _TN5250E_HEADER_LEN = 10
75
75
 
76
+ # The 5250 payload starts after the fixed ten-byte TN5250E header. Byte 9 is
77
+ # the per-direction sequence number, not a variable-header length.
78
+ _TN5250E_FIXED_HEADER_LEN = 6
79
+
80
+
81
+ def _payload_offset(frame: bytes) -> int:
82
+ """Return the offset of the 5250 payload within a TN5250E *frame*."""
83
+ return _TN5250E_HEADER_LEN
84
+
76
85
 
77
86
  @dataclass
78
87
  class TN5250EHeader:
@@ -101,7 +110,7 @@ class TN5250EHeader:
101
110
  self.data_type, # 6: data type
102
111
  0x00, # 7: request/response
103
112
  self.err_flag, # 8: error recovery
104
- var_len, # 9: variable header length
113
+ var_len, # 9: operation code (var-header length; 3 = Put/Get)
105
114
  ]
106
115
  )
107
116
  + var_hdr
@@ -325,24 +334,28 @@ class TN5250Connection:
325
334
  frame = _build_tn5250e_frame(req_resp=0x00, opcode=_TN5250E_OP_CANCEL_INVITE)
326
335
  self._send_frame(frame, description="Sending Cancel-Invite response")
327
336
 
328
- def send_save_screen_response(self) -> None:
337
+ def send_save_screen_response(self, screen_data: bytes) -> None:
329
338
  """Send the TN5250E-level Save Screen acknowledgment.
330
339
 
331
340
  When the server issues an ESC+SAVE_SCREEN (0x04 0x02) command the
332
- terminal must reply with a matching TN5250E frame whose variable header
333
- encodes ESC+RESTORE_SCREEN+CC1+CC2 (mirroring what working emulators
334
- send). This is a protocol-level exchange with no 5250 data stream
335
- payload.
341
+ terminal replies with a complete, serialized presentation space. The
342
+ host stores this opaque regeneration buffer and returns it in a later
343
+ Restore Screen command.
344
+
345
+ Parameters
346
+ ----------
347
+ screen_data:
348
+ A complete length-prefixed Restore Screen response produced by the
349
+ datastream encoder.
336
350
  """
337
351
  if not self._tn5250e_active:
338
352
  return # save/restore handshake only occurs in TN5250E mode
339
353
 
340
- # 4-byte variable header: [ESC, RESTORE_SCREEN, CC1, CC2.RESET_MDT].
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]
354
+ frame = _build_tn5250e_frame(
355
+ req_resp=0x00,
356
+ opcode=_TN5250E_OP_SAVE_SCREEN_RESPONSE,
357
+ var_hdr=screen_data,
344
358
  )
345
- frame = _build_tn5250e_frame(req_resp=0x00, opcode=len(var_hdr), var_hdr=var_hdr)
346
359
  self._send_frame(frame, description="Sending Save Screen response")
347
360
 
348
361
  def recv_record(self) -> bytes:
@@ -354,8 +367,8 @@ class TN5250Connection:
354
367
  log.debug("Short TN5250E frame (%d bytes), skipping", len(frame))
355
368
  continue
356
369
  hdr = TN5250EHeader.from_bytes(frame)
357
- self._last_recv_opcode = frame[9] # byte 9: opcode / var_len
358
- payload = bytes(frame[_TN5250E_HEADER_LEN:])
370
+ self._last_recv_opcode = frame[9] # byte 9: opcode
371
+ payload = bytes(frame[_payload_offset(frame) :])
359
372
  log.debug(
360
373
  "← TN5250E type=%02X data_type=%02X payload=%d bytes",
361
374
  hdr.record_type,
@@ -365,20 +378,16 @@ class TN5250Connection:
365
378
  else:
366
379
  frame_bytes = bytes(frame)
367
380
  # IBM i sends TN5250E-formatted records even when TN5250E was
368
- # not formally negotiated. The 10-byte fixed header begins
369
- # with a big-endian 16-bit record length that equals the total
370
- # frame size. Byte 9 of the header is the variable-header
371
- # length — that many additional bytes follow before the real
372
- # 5250 payload.
381
+ # not formally negotiated. The header begins with a big-endian
382
+ # 16-bit record length that equals the total frame size, and
383
+ # byte 6 holds the length of the variable header that follows
384
+ # it — the real 5250 payload starts after that.
373
385
  if len(frame_bytes) >= _TN5250E_HEADER_LEN and int.from_bytes(
374
386
  frame_bytes[:2], "big"
375
387
  ) == len(frame_bytes):
376
388
  hdr = TN5250EHeader.from_bytes(frame_bytes)
377
389
  self._last_recv_opcode = frame_bytes[9] # byte 9: opcode
378
- var_hdr_len = frame_bytes[
379
- 9
380
- ] # RFC 2877 variable-header length field
381
- skip = _TN5250E_HEADER_LEN + var_hdr_len
390
+ skip = _payload_offset(frame_bytes)
382
391
  payload = frame_bytes[skip:]
383
392
  # Server is speaking TN5250E — our responses must include the header too
384
393
  if not self._tn5250e_active:
@@ -387,9 +396,8 @@ class TN5250Connection:
387
396
  )
388
397
  self._tn5250e_active = True
389
398
  log.debug(
390
- "← implicit TN5250E header stripped (fixed=%d var=%d) data_type=%02X payload=%d bytes",
391
- _TN5250E_HEADER_LEN,
392
- var_hdr_len,
399
+ "← implicit TN5250E header stripped (%d bytes) data_type=%02X payload=%d bytes",
400
+ skip,
393
401
  hdr.data_type,
394
402
  len(payload),
395
403
  )
@@ -768,7 +776,7 @@ def _iac_escape(data: bytes) -> bytes:
768
776
 
769
777
 
770
778
  def _build_tn5250e_frame(*, req_resp: int, opcode: int, var_hdr: bytes = b"") -> bytes:
771
- """Build an IAC-EOR-terminated TN5250E frame with no 5250 payload.
779
+ """Build an IAC-EOR-terminated TN5250E frame with an optional 5250 payload.
772
780
 
773
781
  Produces the fixed 10-byte header (RFC 2877 §3.3) followed by an optional
774
782
  variable header, IAC-escapes it, and appends the IAC-EOR record terminator.
@@ -780,7 +788,7 @@ def _build_tn5250e_frame(*, req_resp: int, opcode: int, var_hdr: bytes = b"") ->
780
788
  req_resp:
781
789
  Byte 7 (request/response flags) — e.g. the Attn flag.
782
790
  opcode:
783
- Byte 9 (operation code / variable-header length).
791
+ Byte 9 operation code (RFC 2877 §4.3) — how the host identifies the record.
784
792
  var_hdr:
785
793
  Optional variable-header bytes appended after the fixed header.
786
794
  """
@@ -796,7 +804,7 @@ def _build_tn5250e_frame(*, req_resp: int, opcode: int, var_hdr: bytes = b"") ->
796
804
  TN5250EDataType.INPUT, # 6: data type (workstation → host)
797
805
  req_resp, # 7: request/response flags
798
806
  0x00, # 8: error recovery
799
- opcode, # 9: opcode / variable-header length
807
+ opcode, # 9: operation code
800
808
  ]
801
809
  )
802
810
  return _iac_escape(header + var_hdr) + bytes(
@@ -83,11 +83,12 @@ class TN5250ESubNeg:
83
83
 
84
84
 
85
85
  class Command:
86
- INLINE_WRITE_STRUCTURED_FIELD = (
87
- 0x02 # Introduces an inline WSF record within a WTD stream
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
- Only bits 0-2 are defined by the IBM 5250 spec; bits 3-7 are reserved.
111
- Keyboard lock/unlock is controlled exclusively by CC2, not CC1.
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
- ERASE_TO_ADDRESS = (
140
- 0x12 # Erase to Address — 2 address bytes (fill with nulls)
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
- GROUP_ERASE_TO_ADDRESS = 0x16 # Group Erase to Address — 2 address bytes (fill with nulls, within field group)
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
- _KNOWN_ORDERS = frozenset(
151
- v for k, v in vars(Order).items() if not k.startswith("_")
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
- # Byte 0
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 = 0x01 # Modified Data Tag — set when the field has been touched
227
- AUTO_ENTER = 0x08 # Auto Enter — automatically submit when the field is filled
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 = 0x04 # Alphabetic only — rejects digits and special characters
233
- NUM_SHIFT = 0x08 # Numeric shift — defaults keyboard to numeric mode
234
- NUM_ONLY = 0x18 # Numeric only — rejects non-numeric input
235
- KATA_SHIFT = 0x0C # Katakana shift — Japanese katakana character entry
236
- DIGITS_ONLY = 0x1C # Digits only — accepts 0–9 with no sign
237
- IO_ONLY = 0x10 # Inhibit Output only — field is write-protected from the keyboard
238
- SIGNED_NUM = 0x14 # Signed numeric — accepts digits plus a trailing sign character
239
- # Display attribute (high nibble of byte 1)
240
- DISP_NORM = 0x00 # Normal — green on black
241
- DISP_HIGH = 0x20 # High intensity — bright green on black
242
- DISP_RI = 0x40 # Reverse image — black on green
243
- DISP_RI_HIGH = 0x60 # Reverse image high intensity — bright black on green
244
- DISP_UNDER = 0x80 # Underline — green with underline
245
- DISP_UNDER_HI = 0xA0 # Underline high intensity — bright green with underline
246
- DISP_BLINK = 0xC0 # Blink — flashing field
247
- NON_DISPLAY = 0xE0 # Non-display — invisible; used for password entry
248
- DISPLAY_ATTR_MASK = (
249
- 0xE0 # Bitmask for the display attribute field (high 3 bits of byte 1)
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 10-byte header + variable header → returns the raw 5250 payload.
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,14 @@ 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 special TN5250E-framed acknowledgment (no 5250 payload — just a variable header encoding ESC + RESTORE_SCREEN + CC1 + CC2). This tells the server "I've saved the screen; you can now overlay it."
40
+ When the server issues a SAVE_SCREEN command, the terminal must reply with a
41
+ TN5250E-framed record carrying `ESC + RESTORE_SCREEN + a two-byte length + the
42
+ current presentation space`. The image is freshly serialized from the current
43
+ screen model: geometry, cursor/home positions, the complete character/attribute
44
+ plane, and the field table. Host-to-terminal WTD, Clear Unit, and Read commands
45
+ are never replayed. The host stores this opaque regeneration buffer and returns
46
+ it in a later Restore Screen command. See
47
+ [Save/Restore Screen Flow](#saverestore-screen-flow).
37
48
 
38
49
  ---
39
50
 
@@ -43,28 +54,37 @@ The **DataStreamParser** takes a raw 5250 payload (one record from the connectio
43
54
 
44
55
  ### Record Structure
45
56
 
46
- Every 5250 record starts with a **command byte**:
47
-
48
- | Command | Meaning |
49
- | ------------------------------- | ----------------------------------------------------------------------------------- |
50
- | `0x11` (Write to Display) | Main screen update — followed by CC1, CC2, then orders/data |
51
- | `0x40` (Clear Unit) | Clears the display and field definitions |
52
- | `0x02` (Save Screen) | Server wants to save the current screen (same format as WTD) |
53
- | `0x12` (Restore Screen) | Server wants to restore a previously saved screen |
54
- | `0x04` (ESC) | Escape prefix — next byte is the real command (Clear Unit, WTD, WSF, Save, Restore) |
55
- | `0xF3` (Write Structured Field) | Carries structured data like Query commands |
56
- | `0x42/0x52/0x72/0x62` | Read commands — server asks client to send back screen/field data |
57
- | `0x21` (Write Error Code) | Display error on status line |
58
- | `0x50` (Clear Format Table) | Discard field definitions |
57
+ Every command in a record is introduced by **ESC (`0x04`)**, and one record may
58
+ carry several commands back to back:
59
+
60
+ | Command | Meaning |
61
+ | ------------------------------- | ---------------------------------------------------------------------------------------- |
62
+ | `0x11` (Write to Display) | Main screen update — followed by CC1, CC2, then orders/data |
63
+ | `0x40` (Clear Unit) | Erases the display and field definitions and selects the **primary** size (24×80) |
64
+ | `0x20` (Clear Unit Alternate) | Erases the display and selects the **alternate** size (27×132); takes one parameter byte |
65
+ | `0x02` (Save Screen) | Server wants the client to save the current screen |
66
+ | `0x12` (Restore Screen) | Server wants a previously saved screen restored; followed by its two-byte identifier |
67
+ | `0xF3` (Write Structured Field) | Carries structured data like Query commands |
68
+ | `0x42/0x52/0x72/0x62` | Read commands — server asks client to send back screen/field data |
69
+ | `0x21` (Write Error Code) | Display error on status line |
70
+ | `0x50` (Clear Format Table) | Discard field definitions |
71
+
72
+ ### Screen size
73
+
74
+ The display size is **absolute and set only by the clear commands**: Clear Unit
75
+ selects 24×80, Clear Unit Alternate selects 27×132. It is never a toggle, and it
76
+ is never inferred from out-of-range buffer addresses. Getting this wrong is what
77
+ made a screen returning from a 132-column layout keep rendering as 27×132 with
78
+ stale content bleeding through.
59
79
 
60
80
  ### Control Characters (CC1 / CC2)
61
81
 
62
- After a WTD or Save Screen command, two bytes control screen-level behavior:
82
+ Write to Display is followed by two control bytes:
63
83
 
64
- - **CC1** — Screen clear mode:
65
- - `0x20`: Clear the entire screen before writing new data
66
- - `0x40`: Clear and switch to alternate screen size (24×80 ↔ 27×132)
67
- - `0x00`: Don't clear — overlay on existing content
84
+ - **CC1** — Lock/reset flags. Notably CC1 **neither erases the screen nor
85
+ changes its dimensions**; only the clear commands above do that.
86
+ - `0x01`/`0x02`: Reset pending AIDs / unlock after the write
87
+ - `0x04`: Sound the alarm
68
88
 
69
89
  - **CC2** — Keyboard and field flags:
70
90
  - `0x08`: Unlock the keyboard (let the user type)
@@ -75,24 +95,56 @@ After a WTD or Save Screen command, two bytes control screen-level behavior:
75
95
 
76
96
  After the command + CC bytes, the rest of the record is a stream of **orders** intermixed with **data bytes**:
77
97
 
78
- | Order | What it does |
79
- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
- | **SBA** — Set Buffer Address (`0x11`) + row + col | Move the write position to a specific screen cell |
81
- | **SF** — Start Field (`0x1D`) + FFW [+ FCWs] | Define a new input/output field at the current position. The FFW (Field Format Word, 2 bytes) defines field type — bypass/input, numeric, non-display, etc. Optional FCW (Field Control Word) pairs add behaviours like mandatory entry, right-adjust, or monocase. |
82
- | **MF** — Modify Field (`0x2C`) + FFW [+ FCWs] | Modify an existing field's attributes |
83
- | **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) |
84
- | **MC** — Move Cursor (`0x14`) + row + col | Move the cursor to a specific position |
85
- | **RA** — Repeat to Address (`0x3C`) + row + col + char | Repeat a character from current position up to the target |
86
- | **EA** — Erase to Address (`0x12`) + row + col | Erase (fill with nulls) from current position to target |
87
- | **TD** — Transparent Data (`0x10`) + length + data | Write raw bytes (transparent, no order interpretation) |
88
- | **SA** — Set Attribute (`0x28`) + type + value | Set a display attribute (colour, underline, etc.) |
89
- | **SOH** — Start of Header (`0x01`) + length + data | Metadata block, skipped |
98
+ These are the **5250** order codes; several share a name with a 3270 order but
99
+ use a different byte value (5250 Repeat to Address is `0x02`, not `0x3C`).
100
+
101
+ | Order | What it does |
102
+ | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | **SOH** — Start of Header (`0x01`) + length + data | Metadata block, skipped |
104
+ | **RA** — Repeat to Address (`0x02`) + row + col + char | Repeat a character from the current position up to **and including** the target |
105
+ | **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 |
106
+ | **TD** — Transparent Data (`0x10`) + length + data | Write raw bytes (transparent, no order interpretation) |
107
+ | **SBA** — Set Buffer Address (`0x11`) + row + col | Move the write position to a specific screen cell |
108
+ | **WEA** — Write Extended Attribute (`0x12`) + type + value | Extended character buffer attribute; not emulated, operands are consumed |
109
+ | **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) |
110
+ | **MC** — Move Cursor (`0x14`) + row + col | Move the cursor to a specific position |
111
+ | **WDSF** — Write to Display Structured Field (`0x15`) | GUI constructs (windows, scrollbars); not emulated, the payload is skipped |
112
+ | **SF** — Start Field (`0x1D`) | Define a field at the current position — see below |
90
113
 
91
114
  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
115
 
93
- ### Inline Structured Fields (0x02 / 0x04 in the order stream)
94
-
95
- IBM i sometimes embeds 3–4 byte attribute markers (`0x02 XX YY` or `0x04 XX YY ZZ`) inline within the data stream to carry extended colour/highlight info. The parser identifies these and skips over them without writing them as screen text.
116
+ ### Attribute bytes
117
+
118
+ Bytes in the range **`0x20`–`0x3F`** are display attributes. They are _data_:
119
+ each one occupies a screen position (rendered as a blank) and governs the
120
+ appearance of everything after it until the next attribute. `0x27` — more
121
+ precisely any attribute whose low three bits are all set — means _non-display_,
122
+ which is how password fields and hidden command strings are rendered blank.
123
+
124
+ Because attributes are the only bytes with that bit pattern, they double as the
125
+ terminator for the variable-length part of an SF order.
126
+
127
+ ### Start Field (SF)
128
+
129
+ SF (0x1D) [FFW1 FFW2 [FCW1 FCW2]...] ATTR LEN_HI LEN_LO
130
+
131
+ - An **output-only** field omits the FFW and FCWs entirely: the byte straight
132
+ after SF is then the attribute.
133
+ - The **FFW** (Field Format Word) defines field behaviour — bypass/input,
134
+ keyboard shift, mandatory entry, and so on. Byte 0 always has bit `0x40` set,
135
+ which is how it is told apart from an attribute byte. Byte 1 bit `0x80` is
136
+ **Auto-Enter**: the host expects an Enter AID to be sent automatically the
137
+ moment the field is completely filled (`Field.auto_enter`). On the wire this
138
+ looks like e.g. `1D 40 A0 26 00 01` — FFW `40 A0`, attribute `26`, length `1`
139
+ — which is a plain auto-enter field, _not_ an FCW pair.
140
+ - Zero or more **FCW** (Field Control Word) pairs may follow; the chain ends at
141
+ the first attribute byte, _not_ at a continuation bit.
142
+ - The attribute byte is written at the field's start position, and the field's
143
+ length is the **two bytes after it** — it is not derived from an FCW and it is
144
+ not inferred from where the next field begins.
145
+ - A **Start of Header (SOH)** begins a new format table. Applying it discards
146
+ existing field definitions without clearing display characters; subsequent
147
+ SF orders rebuild the table for the current write.
96
148
 
97
149
  ### Write Structured Field (WSF / Query)
98
150
 
@@ -127,6 +179,8 @@ The **Screen** holds the actual state that represents what you'd see on a physic
127
179
  - **Field list** — all SF/MF-defined fields with their position, length, format word, and modified-data-tag.
128
180
  - **Cursor position** — where the blinking cursor sits.
129
181
  - **Keyboard locked flag** — when True, the terminal won't send AID keys.
182
+ - **Last AID** — the most recent AID successfully sent to the host; included
183
+ in Save Screen device state.
130
184
 
131
185
  ### Applying Events
132
186
 
@@ -167,8 +221,9 @@ This is the central method — it:
167
221
  2. Feeds it through `parser.parse()` to get a list of `DSEvent` objects.
168
222
  3. Iterates through events looking for special commands:
169
223
  - **QUERY** → sends back a Query Reply with terminal capabilities
170
- - **SAVE_SCREEN** → deep-copies the current `Screen` onto a stack, sends the save-screen acknowledgment to the server
171
- - **RESTORE_SCREEN** → pops the saved screen off the stack, replaces the current screen with it, then applies any remaining events from the same record on top
224
+ - **SAVE_SCREEN** → deep-copies the current `Screen` onto a stack, serializes one complete regeneration buffer from that model, and returns it in the save-screen response
225
+ - **RESTORE_SCREEN** → pops the most recently saved `Screen`; the host-returned regeneration buffer is opaque because the local copy is authoritative, then any remaining events from the same record are applied on top
226
+ - **RESIZE_SCREEN** → re-creates the parser and screen at the new dimensions
172
227
  4. If no RESTORE occurred, applies all events to the screen normally.
173
228
  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
229
 
@@ -180,6 +235,16 @@ This is the central method — it:
180
235
  4. Sets `keyboard_locked = True` (the server will unlock it when it replies).
181
236
  5. Calls `_recv_and_apply()` to wait for and process the server's response.
182
237
 
238
+ ### Auto-Enter fields
239
+
240
+ When `type_into()` fills a field whose FFW has the Auto-Enter bit set, it
241
+ submits `AID.ENTER` itself (via the normal `send_key` path) rather than waiting
242
+ for the caller — mirroring a physical 5250. The cursor is placed on the
243
+ auto-enter field first, because `set_field_text` otherwise advances it to the
244
+ next field once the current one is full. Only the modified-data tags are cleared
245
+ locally; the resulting screen (often a Save/Restore popup handshake) is driven
246
+ entirely by the host and handled by `_recv_and_apply()`.
247
+
183
248
  ### Waiting: `wait_for_text(text)`
184
249
 
185
250
  1. Checks if the text already appears on screen (from events already applied).
@@ -194,12 +259,30 @@ This is the central method — it:
194
259
  This is how the server does "popup" screens (like overlaying a detail view on a list):
195
260
 
196
261
  1. Server sends **SAVE_SCREEN** → terminal saves a copy of the current screen
262
+ under a fresh two-byte identifier and returns that identifier in the Save
263
+ Screen response
197
264
  2. Server sends a normal WTD with new content → terminal shows the popup
198
265
  3. User interacts with the popup (sends keys, gets responses)
199
- 4. Server sends **RESTORE_SCREEN** → terminal swaps back to the saved screen
266
+ 4. Server sends **RESTORE_SCREEN + identifier** → terminal swaps back to _that_
267
+ saved screen and discards any screens saved after it
200
268
  5. Server sends a follow-up WTD to unlock the keyboard on the restored screen
201
269
 
202
- The tricky part: after step 4, the restored screen has `keyboard_locked = True` (it was saved right after the client sent an AID key). The terminal must consume the unlock record (step 5) before attempting the next `send_key`, otherwise the server rejects the input.
270
+ Saves nest, so this is **not** a stack the terminal may simply pop: the server
271
+ decides how far to unwind and names the screen it wants. Unwinding two levels
272
+ in one go — restore `04 05`, Clear Unit, restore `03 04` — is normal, and
273
+ popping blindly restores the wrong screen.
274
+
275
+ A restored screen brings its own dimensions with it, so the parser must be
276
+ re-created to match whenever one is restored.
277
+
278
+ The other tricky part: after step 4, the restored screen has
279
+ `keyboard_locked = True` (it was saved right after the client sent an AID key).
280
+ The terminal must consume the unlock record (step 5) before attempting the next
281
+ `send_key`, otherwise the server rejects the input.
282
+
283
+ Screens restored this way are usually followed by _partial_ updates that repaint
284
+ only the parts of the screen that changed, so the restored snapshot has to be
285
+ byte-accurate — any error in it survives into the final display.
203
286
 
204
287
  ---
205
288