mirror of
https://github.com/lowobservable/oec.git
synced 2026-04-19 09:09:47 +00:00
Add EAB support to TN3270
This commit is contained in:
@@ -18,8 +18,7 @@ expect from a later model 3174, but it does provide basic TN3270 and VT100
|
|||||||
emulation.
|
emulation.
|
||||||
|
|
||||||
- [x] TN3270
|
- [x] TN3270
|
||||||
- [x] Basic TN3270
|
- [x] Extended Data Stream
|
||||||
- [ ] Extended Data Stream
|
|
||||||
- [ ] TN3270E
|
- [ ] TN3270E
|
||||||
- [ ] SSL/TLS
|
- [ ] SSL/TLS
|
||||||
- [x] VT100
|
- [x] VT100
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ def _create_interface(args):
|
|||||||
|
|
||||||
def _create_session(args, terminal):
|
def _create_session(args, terminal):
|
||||||
if args.emulator == 'tn3270':
|
if args.emulator == 'tn3270':
|
||||||
return TN3270Session(terminal, args.host, args.port, args.extended_data_stream)
|
return TN3270Session(terminal, args.host, args.port)
|
||||||
|
|
||||||
if args.emulator == 'vt100' and is_vt100_available:
|
if args.emulator == 'vt100' and is_vt100_available:
|
||||||
host_command = [args.command, *args.command_args]
|
host_command = [args.command, *args.command_args]
|
||||||
@@ -91,10 +91,6 @@ def main():
|
|||||||
tn3270_parser.add_argument('host', help='Hostname')
|
tn3270_parser.add_argument('host', help='Hostname')
|
||||||
tn3270_parser.add_argument('port', nargs='?', default=23, type=int)
|
tn3270_parser.add_argument('port', nargs='?', default=23, type=int)
|
||||||
|
|
||||||
tn3270_parser.add_argument('--disable-eds', action='store_false',
|
|
||||||
dest='extended_data_stream',
|
|
||||||
help='Disable extended data stream support')
|
|
||||||
|
|
||||||
if is_vt100_available:
|
if is_vt100_available:
|
||||||
vt100_parser = subparsers.add_parser('vt100', description='VT100 emulator',
|
vt100_parser = subparsers.add_parser('vt100', description='VT100 emulator',
|
||||||
help='VT100 emulator')
|
help='VT100 emulator')
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ oec.controller
|
|||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
import selectors
|
import selectors
|
||||||
|
from textwrap import dedent
|
||||||
from coax import poll, poll_ack, load_control_register, get_features, PollAction, \
|
from coax import poll, poll_ack, load_control_register, get_features, PollAction, \
|
||||||
KeystrokePollResponse, TerminalType, ReceiveTimeout, \
|
KeystrokePollResponse, TerminalType, Feature, ReceiveTimeout, \
|
||||||
ReceiveError, ProtocolError
|
ReceiveError, ProtocolError
|
||||||
|
|
||||||
from .terminal import Terminal, UnsupportedTerminalError, read_terminal_ids
|
from .terminal import Terminal, UnsupportedTerminalError, read_terminal_ids
|
||||||
@@ -104,6 +106,8 @@ class Controller:
|
|||||||
def _handle_terminal_attached(self, poll_response):
|
def _handle_terminal_attached(self, poll_response):
|
||||||
self.logger.info('Terminal attached')
|
self.logger.info('Terminal attached')
|
||||||
|
|
||||||
|
jumbo_write_strategy = _get_jumbo_write_strategy()
|
||||||
|
|
||||||
# Read the terminal identifiers.
|
# Read the terminal identifiers.
|
||||||
(terminal_id, extended_id) = read_terminal_ids(self.interface)
|
(terminal_id, extended_id) = read_terminal_ids(self.interface)
|
||||||
|
|
||||||
@@ -117,18 +121,28 @@ class Controller:
|
|||||||
|
|
||||||
self.logger.info(f'Features = {features}')
|
self.logger.info(f'Features = {features}')
|
||||||
|
|
||||||
|
if Feature.EAB in features:
|
||||||
|
if self.interface.legacy_firmware_detected and jumbo_write_strategy is None:
|
||||||
|
del features[Feature.EAB]
|
||||||
|
|
||||||
|
_print_no_i1_eab_notice()
|
||||||
|
|
||||||
# Get the keymap.
|
# Get the keymap.
|
||||||
keymap = self.get_keymap(terminal_id, extended_id)
|
keymap = self.get_keymap(terminal_id, extended_id)
|
||||||
|
|
||||||
# Initialize the terminal.
|
# Initialize the terminal.
|
||||||
self.terminal = Terminal(self.interface, terminal_id, extended_id,
|
self.terminal = Terminal(self.interface, terminal_id, extended_id,
|
||||||
features, keymap)
|
features, keymap,
|
||||||
|
jumbo_write_strategy=jumbo_write_strategy)
|
||||||
|
|
||||||
(rows, columns) = self.terminal.display.dimensions
|
(rows, columns) = self.terminal.display.dimensions
|
||||||
keymap_name = self.terminal.keyboard.keymap.name
|
keymap_name = self.terminal.keyboard.keymap.name
|
||||||
|
|
||||||
self.logger.info(f'Rows = {rows}, Columns = {columns}, Keymap = {keymap_name}')
|
self.logger.info(f'Rows = {rows}, Columns = {columns}, Keymap = {keymap_name}')
|
||||||
|
|
||||||
|
if self.terminal.display.has_eab:
|
||||||
|
self.terminal.display.load_eab_mask(0xff)
|
||||||
|
|
||||||
self.terminal.display.clear(clear_status_line=True)
|
self.terminal.display.clear(clear_status_line=True)
|
||||||
|
|
||||||
# Show the attached indicator on the status line.
|
# Show the attached indicator on the status line.
|
||||||
@@ -253,3 +267,45 @@ class Controller:
|
|||||||
|
|
||||||
def _load_control_register(self):
|
def _load_control_register(self):
|
||||||
load_control_register(self.interface, self.terminal.get_control_register())
|
load_control_register(self.interface, self.terminal.get_control_register())
|
||||||
|
|
||||||
|
def _get_jumbo_write_strategy():
|
||||||
|
value = os.environ.get('COAX_JUMBO')
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if value in ['split', 'ignore']:
|
||||||
|
return value
|
||||||
|
|
||||||
|
self.logger.warning(f'Unsupported COAX_JUMBO option: {value}')
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _print_no_i1_eab_notice():
|
||||||
|
notice = '''
|
||||||
|
**** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ****
|
||||||
|
|
||||||
|
Your terminal is reporting the existence of an EAB feature that allows extended
|
||||||
|
colors and formatting, however...
|
||||||
|
|
||||||
|
I think you are using an older firmware on the 1st generation, Arduino Mega
|
||||||
|
based, interface which does not support the "jumbo write" required to write a
|
||||||
|
full screen to the regen and EAB buffers.
|
||||||
|
|
||||||
|
I'm going to continue as if the EAB feature did not exist...
|
||||||
|
|
||||||
|
If you want to override this behavior, you can set the COAX_JUMBO environment
|
||||||
|
variable as follows:
|
||||||
|
|
||||||
|
- COAX_JUMBO=split - split large writes into multiple smaller 32-byte writes
|
||||||
|
before sending to the interface, this will result in
|
||||||
|
additional round trips to the interface which may
|
||||||
|
manifest as visible incremental changes being applied
|
||||||
|
to the screen
|
||||||
|
- COAX_JUMBO=ignore - try a jumbo write, anyway, use this option if you
|
||||||
|
believe you are seeing this behavior in error
|
||||||
|
|
||||||
|
**** **** **** **** **** **** **** **** **** **** **** **** **** **** **** ****
|
||||||
|
'''
|
||||||
|
|
||||||
|
print(dedent(notice))
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ oec.display
|
|||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import logging
|
import logging
|
||||||
|
from more_itertools import interleave
|
||||||
from sortedcontainers import SortedSet
|
from sortedcontainers import SortedSet
|
||||||
from coax import read_address_counter_hi, read_address_counter_lo, \
|
from coax import read_address_counter_hi, read_address_counter_lo, \
|
||||||
load_address_counter_hi, load_address_counter_lo, write_data
|
load_address_counter_hi, load_address_counter_lo, write_data, \
|
||||||
|
eab_load_mask, eab_write_alternate
|
||||||
|
|
||||||
_ASCII_CHAR_MAP = {
|
_ASCII_CHAR_MAP = {
|
||||||
'>': 0x08,
|
'>': 0x08,
|
||||||
@@ -168,15 +170,17 @@ def encode_string(string, errors='replace'):
|
|||||||
Dimensions = namedtuple('Dimensions', ['rows', 'columns'])
|
Dimensions = namedtuple('Dimensions', ['rows', 'columns'])
|
||||||
|
|
||||||
class Display:
|
class Display:
|
||||||
def __init__(self, interface, dimensions):
|
def __init__(self, interface, dimensions, eab_address, jumbo_write_strategy=None):
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
self.interface = interface
|
self.interface = interface
|
||||||
self.dimensions = dimensions
|
self.dimensions = dimensions
|
||||||
|
self.eab_address = eab_address
|
||||||
|
|
||||||
(rows, columns) = self.dimensions
|
(rows, columns) = self.dimensions
|
||||||
|
|
||||||
self.buffer = bytearray(rows * columns)
|
self.regen_buffer = bytearray(rows * columns)
|
||||||
|
self.eab_buffer = bytearray(rows * columns)
|
||||||
self.dirty = SortedSet()
|
self.dirty = SortedSet()
|
||||||
|
|
||||||
self.address_counter = None
|
self.address_counter = None
|
||||||
@@ -186,6 +190,12 @@ class Display:
|
|||||||
self.cursor_reverse = False
|
self.cursor_reverse = False
|
||||||
self.cursor_blink = False
|
self.cursor_blink = False
|
||||||
|
|
||||||
|
self.jumbo_write_strategy = jumbo_write_strategy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_eab(self):
|
||||||
|
return self.eab_address is not None
|
||||||
|
|
||||||
def move_cursor(self, index=None, row=None, column=None, force_load=False):
|
def move_cursor(self, index=None, row=None, column=None, force_load=False):
|
||||||
"""Load the address counter."""
|
"""Load the address counter."""
|
||||||
address = self._calculate_address(index=index, row=row, column=column)
|
address = self._calculate_address(index=index, row=row, column=column)
|
||||||
@@ -194,7 +204,14 @@ class Display:
|
|||||||
|
|
||||||
return self._load_address_counter(address, force_load)
|
return self._load_address_counter(address, force_load)
|
||||||
|
|
||||||
def buffered_write(self, byte, index=None, row=None, column=None):
|
def load_eab_mask(self, mask):
|
||||||
|
"""Load the EAB mask."""
|
||||||
|
if not self.has_eab:
|
||||||
|
raise RuntimeError('No EAB feature')
|
||||||
|
|
||||||
|
eab_load_mask(self.interface, self.eab_address, mask)
|
||||||
|
|
||||||
|
def buffered_write(self, regen_byte, eab_byte, index=None, row=None, column=None):
|
||||||
if index is None:
|
if index is None:
|
||||||
if row is None or column is None:
|
if row is None or column is None:
|
||||||
raise ValueError('Either index or row and column is required')
|
raise ValueError('Either index or row and column is required')
|
||||||
@@ -203,10 +220,11 @@ class Display:
|
|||||||
|
|
||||||
# TODO: Verify that index is within range.
|
# TODO: Verify that index is within range.
|
||||||
|
|
||||||
if self.buffer[index] == byte:
|
if self.regen_buffer[index] == regen_byte and (self.eab_buffer[index] == eab_byte or not self.has_eab):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.buffer[index] = byte
|
self.regen_buffer[index] = regen_byte
|
||||||
|
self.eab_buffer[index] = eab_byte if self.has_eab else 0x00
|
||||||
|
|
||||||
self.dirty.add(index)
|
self.dirty.add(index)
|
||||||
|
|
||||||
@@ -227,11 +245,12 @@ class Display:
|
|||||||
address = columns
|
address = columns
|
||||||
count = rows * columns
|
count = rows * columns
|
||||||
|
|
||||||
self._write((b'\x00', count), address=address)
|
self._write((b'\x00', count), (b'\x00', count) if self.has_eab else None, address=address)
|
||||||
|
|
||||||
# Update the buffer and dirty indicators to reflect the cleared screen.
|
# Update the buffers and dirty indicators to reflect the cleared screen.
|
||||||
for index in range(rows * columns):
|
for index in range(rows * columns):
|
||||||
self.buffer[index] = 0x00
|
self.regen_buffer[index] = 0x00
|
||||||
|
self.eab_buffer[index] = 0x00
|
||||||
|
|
||||||
self.dirty.clear()
|
self.dirty.clear()
|
||||||
|
|
||||||
@@ -303,12 +322,13 @@ class Display:
|
|||||||
if self.logger.isEnabledFor(logging.DEBUG):
|
if self.logger.isEnabledFor(logging.DEBUG):
|
||||||
self.logger.debug(f'Flushing changes for range {start_index}-{end_index}')
|
self.logger.debug(f'Flushing changes for range {start_index}-{end_index}')
|
||||||
|
|
||||||
data = self.buffer[start_index:end_index+1]
|
regen_data = self.regen_buffer[start_index:end_index+1]
|
||||||
|
eab_data = self.eab_buffer[start_index:end_index+1] if self.has_eab else None
|
||||||
|
|
||||||
address = self._calculate_address(start_index)
|
address = self._calculate_address(start_index)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._write(data, address=address)
|
self._write(regen_data, eab_data, address=address)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
# TODO: This could leave the address_counter incorrect.
|
# TODO: This could leave the address_counter incorrect.
|
||||||
self.logger.error(f'Write error: {error}', exc_info=error)
|
self.logger.error(f'Write error: {error}', exc_info=error)
|
||||||
@@ -318,7 +338,23 @@ class Display:
|
|||||||
|
|
||||||
return self.address_counter
|
return self.address_counter
|
||||||
|
|
||||||
def _write(self, data, address=None, restore_original_address=False):
|
def _write(self, regen_data, eab_data, address=None, restore_original_address=False):
|
||||||
|
if eab_data is not None:
|
||||||
|
if not self.has_eab:
|
||||||
|
raise RuntimeError('No EAB feature')
|
||||||
|
|
||||||
|
if isinstance(regen_data, tuple) and isinstance(eab_data, tuple):
|
||||||
|
if len(regen_data[0]) != len(eab_data[0]):
|
||||||
|
raise ValueError('Regen and EAB pattern length must be equal')
|
||||||
|
|
||||||
|
if regen_data[1] != eab_data[1]:
|
||||||
|
raise ValueError('Regen and EAB pattern count must be equal')
|
||||||
|
elif not isinstance(regen_data, tuple) and not isinstance(eab_data, tuple):
|
||||||
|
if len(regen_data) != len(eab_data):
|
||||||
|
raise ValueError('Regen and EAB data length must be equal')
|
||||||
|
else:
|
||||||
|
raise ValueError('Regen and EAB data must be provided in same form')
|
||||||
|
|
||||||
if restore_original_address:
|
if restore_original_address:
|
||||||
original_address = self.address_counter
|
original_address = self.address_counter
|
||||||
|
|
||||||
@@ -328,12 +364,23 @@ class Display:
|
|||||||
if address is not None:
|
if address is not None:
|
||||||
self._load_address_counter(address, force_load=False)
|
self._load_address_counter(address, force_load=False)
|
||||||
|
|
||||||
write_data(self.interface, data)
|
if eab_data is not None:
|
||||||
|
# Validation of regen and EAB data form has been performed.
|
||||||
|
if isinstance(regen_data, tuple):
|
||||||
|
data = (bytes(interleave(regen_data[0], eab_data[0])), regen_data[1])
|
||||||
|
else:
|
||||||
|
data = bytes(interleave(regen_data, eab_data))
|
||||||
|
|
||||||
if isinstance(address, tuple):
|
eab_write_alternate(self.interface, self.eab_address, data,
|
||||||
length = len(data[0]) * data[1]
|
jumbo_write_strategy=self.jumbo_write_strategy)
|
||||||
else:
|
else:
|
||||||
length = len(data)
|
write_data(self.interface, regen_data,
|
||||||
|
jumbo_write_strategy=self.jumbo_write_strategy)
|
||||||
|
|
||||||
|
if isinstance(regen_data, tuple):
|
||||||
|
length = len(regen_data[0]) * regen_data[1]
|
||||||
|
else:
|
||||||
|
length = len(regen_data)
|
||||||
|
|
||||||
self.address_counter = self._calculate_address_after_write(address, length)
|
self.address_counter = self._calculate_address_after_write(address, length)
|
||||||
|
|
||||||
@@ -348,7 +395,7 @@ class StatusLine:
|
|||||||
self.columns = display.dimensions.columns
|
self.columns = display.dimensions.columns
|
||||||
|
|
||||||
def write(self, column, data):
|
def write(self, column, data):
|
||||||
self.display._write(data, address=column, restore_original_address=True)
|
self.display._write(data, None, address=column, restore_original_address=True)
|
||||||
|
|
||||||
def write_string(self, column, string):
|
def write_string(self, column, string):
|
||||||
self.write(column, encode_string(string))
|
self.write(column, encode_string(string))
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ oec.terminal
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from coax import read_terminal_id, read_extended_id, PollAction, Control, ReceiveError, \
|
from coax import read_terminal_id, read_extended_id, PollAction, Control, Feature, \
|
||||||
ProtocolError
|
ReceiveError, ProtocolError
|
||||||
|
|
||||||
from .display import Dimensions, Display
|
from .display import Dimensions, Display
|
||||||
from .keyboard import Keyboard
|
from .keyboard import Keyboard
|
||||||
@@ -59,7 +59,8 @@ def read_terminal_ids(interface, extended_id_retry_attempts=3):
|
|||||||
class Terminal:
|
class Terminal:
|
||||||
"""Terminal information and devices."""
|
"""Terminal information and devices."""
|
||||||
|
|
||||||
def __init__(self, interface, terminal_id, extended_id, features, keymap):
|
def __init__(self, interface, terminal_id, extended_id, features, keymap,
|
||||||
|
jumbo_write_strategy=None):
|
||||||
self.interface = interface
|
self.interface = interface
|
||||||
self.terminal_id = terminal_id
|
self.terminal_id = terminal_id
|
||||||
self.extended_id = extended_id
|
self.extended_id = extended_id
|
||||||
@@ -67,7 +68,8 @@ class Terminal:
|
|||||||
|
|
||||||
dimensions = get_dimensions(self.terminal_id, self.extended_id)
|
dimensions = get_dimensions(self.terminal_id, self.extended_id)
|
||||||
|
|
||||||
self.display = Display(interface, dimensions)
|
self.display = Display(interface, dimensions, features.get(Feature.EAB),
|
||||||
|
jumbo_write_strategy=jumbo_write_strategy)
|
||||||
self.keyboard = Keyboard(keymap)
|
self.keyboard = Keyboard(keymap)
|
||||||
|
|
||||||
self.alarm = False
|
self.alarm = False
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ oec.tn3270
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from tn3270 import Telnet, Emulator, AttributeCell, CharacterCell, AID, OperatorError, \
|
from tn3270 import Telnet, Emulator, AttributeCell, CharacterCell, AID, Color, Highlight, \
|
||||||
ProtectedCellOperatorError, FieldOverflowOperatorError
|
OperatorError, ProtectedCellOperatorError, FieldOverflowOperatorError
|
||||||
from tn3270.ebcdic import DUP, FM
|
from tn3270.ebcdic import DUP, FM
|
||||||
|
|
||||||
from .session import Session, SessionDisconnectedError
|
from .session import Session, SessionDisconnectedError
|
||||||
@@ -47,13 +47,12 @@ AID_KEY_MAP = {
|
|||||||
class TN3270Session(Session):
|
class TN3270Session(Session):
|
||||||
"""TN3270 session."""
|
"""TN3270 session."""
|
||||||
|
|
||||||
def __init__(self, terminal, host, port, extended_data_stream=True):
|
def __init__(self, terminal, host, port):
|
||||||
self.logger = logging.getLogger(__name__)
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
self.terminal = terminal
|
self.terminal = terminal
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = port
|
self.port = port
|
||||||
self.extended_data_stream = extended_data_stream
|
|
||||||
|
|
||||||
self.telnet = None
|
self.telnet = None
|
||||||
self.emulator = None
|
self.emulator = None
|
||||||
@@ -71,7 +70,14 @@ class TN3270Session(Session):
|
|||||||
|
|
||||||
(rows, columns) = self.terminal.display.dimensions
|
(rows, columns) = self.terminal.display.dimensions
|
||||||
|
|
||||||
self.emulator = Emulator(self.telnet, rows, columns)
|
if self.terminal.display.has_eab:
|
||||||
|
supported_colors = 8
|
||||||
|
supported_highlights = [Highlight.BLINK, Highlight.REVERSE, Highlight.UNDERSCORE]
|
||||||
|
else:
|
||||||
|
supported_colors = 1
|
||||||
|
supported_highlights = []
|
||||||
|
|
||||||
|
self.emulator = Emulator(self.telnet, rows, columns, supported_colors, supported_highlights)
|
||||||
|
|
||||||
self.emulator.alarm = lambda: self.terminal.sound_alarm()
|
self.emulator.alarm = lambda: self.terminal.sound_alarm()
|
||||||
|
|
||||||
@@ -156,10 +162,17 @@ class TN3270Session(Session):
|
|||||||
self.terminal.display.status_line.write_keyboard_insert(self.keyboard_insert)
|
self.terminal.display.status_line.write_keyboard_insert(self.keyboard_insert)
|
||||||
|
|
||||||
def _connect_host(self):
|
def _connect_host(self):
|
||||||
terminal_type = f'IBM-3278-{self.terminal.terminal_id.model}'
|
# We will pretend a 3279 without EAB is a 3278.
|
||||||
|
if self.terminal.display.has_eab:
|
||||||
|
type = '3279'
|
||||||
|
else:
|
||||||
|
type = '3278'
|
||||||
|
|
||||||
if self.extended_data_stream:
|
# Although a IBM 3278 does not support the formatting enabled by the extended
|
||||||
terminal_type += '-E'
|
# data stream, the capabilities will be reported in the query reply.
|
||||||
|
terminal_type = f'IBM-{type}-{self.terminal.terminal_id.model}-E'
|
||||||
|
|
||||||
|
self.logger.info(f'Terminal Type = {terminal_type}')
|
||||||
|
|
||||||
self.telnet = Telnet(terminal_type)
|
self.telnet = Telnet(terminal_type)
|
||||||
|
|
||||||
@@ -171,17 +184,21 @@ class TN3270Session(Session):
|
|||||||
self.telnet = None
|
self.telnet = None
|
||||||
|
|
||||||
def _apply(self):
|
def _apply(self):
|
||||||
|
has_eab = self.terminal.display.has_eab
|
||||||
|
|
||||||
for address in self.emulator.dirty:
|
for address in self.emulator.dirty:
|
||||||
cell = self.emulator.cells[address]
|
cell = self.emulator.cells[address]
|
||||||
|
|
||||||
byte = 0x00
|
regen_byte = 0x00
|
||||||
|
|
||||||
if isinstance(cell, AttributeCell):
|
if isinstance(cell, AttributeCell):
|
||||||
byte = self._map_attribute(cell)
|
regen_byte = self._map_attribute(cell)
|
||||||
elif isinstance(cell, CharacterCell):
|
elif isinstance(cell, CharacterCell):
|
||||||
byte = self._map_character(cell)
|
regen_byte = self._map_character(cell)
|
||||||
|
|
||||||
self.terminal.display.buffered_write(byte, index=address)
|
eab_byte = self._map_formatting(cell.formatting) if has_eab else None
|
||||||
|
|
||||||
|
self.terminal.display.buffered_write(regen_byte, eab_byte, index=address)
|
||||||
|
|
||||||
self.emulator.dirty.clear()
|
self.emulator.dirty.clear()
|
||||||
|
|
||||||
@@ -216,17 +233,44 @@ class TN3270Session(Session):
|
|||||||
if byte == FM:
|
if byte == FM:
|
||||||
return encode_ascii_character(ord(';'))
|
return encode_ascii_character(ord(';'))
|
||||||
|
|
||||||
# TODO: Temporary workaround to show empty reverse video fields until EAB
|
|
||||||
# support is added.
|
|
||||||
if byte == 0x40 and cell.formatting is not None and cell.formatting.reverse:
|
|
||||||
return encode_ascii_character(ord('#'))
|
|
||||||
|
|
||||||
# TODO: Temporary workaround until character set support is added.
|
# TODO: Temporary workaround until character set support is added.
|
||||||
if cell.character_set is not None:
|
if cell.character_set is not None:
|
||||||
return encode_ascii_character(ord('ß'))
|
return encode_ascii_character(ord('ß'))
|
||||||
|
|
||||||
return encode_ebcdic_character(byte)
|
return encode_ebcdic_character(byte)
|
||||||
|
|
||||||
|
def _map_formatting(self, formatting):
|
||||||
|
if formatting is None:
|
||||||
|
return 0x00
|
||||||
|
|
||||||
|
byte = 0x00
|
||||||
|
|
||||||
|
# Map the 3270 color to EAB color.
|
||||||
|
if formatting.color == Color.BLUE:
|
||||||
|
byte |= 0x08
|
||||||
|
elif formatting.color == Color.RED:
|
||||||
|
byte |= 0x10
|
||||||
|
elif formatting.color == Color.PINK:
|
||||||
|
byte |= 0x18
|
||||||
|
elif formatting.color == Color.GREEN:
|
||||||
|
byte |= 0x20
|
||||||
|
elif formatting.color == Color.TURQUOISE:
|
||||||
|
byte |= 0x28
|
||||||
|
elif formatting.color == Color.YELLOW:
|
||||||
|
byte |= 0x30
|
||||||
|
elif formatting.color == Color.WHITE:
|
||||||
|
byte |= 0x38
|
||||||
|
|
||||||
|
# Map the 3270 highlight to EAB highlight.
|
||||||
|
if formatting.blink:
|
||||||
|
byte |= 0x40
|
||||||
|
elif formatting.reverse:
|
||||||
|
byte |= 0x80
|
||||||
|
elif formatting.underscore:
|
||||||
|
byte |= 0xc0
|
||||||
|
|
||||||
|
return byte
|
||||||
|
|
||||||
def _format_message_area(self):
|
def _format_message_area(self):
|
||||||
message_area = b''
|
message_area = b''
|
||||||
|
|
||||||
|
|||||||
@@ -192,9 +192,9 @@ class VT100Session(Session):
|
|||||||
character = row_buffer[column]
|
character = row_buffer[column]
|
||||||
|
|
||||||
# TODO: Investigate multi-byte or zero-byte cases further.
|
# TODO: Investigate multi-byte or zero-byte cases further.
|
||||||
byte = encode_ascii_character(ord(character.data)) if len(character.data) == 1 else 0x00
|
regen_byte = encode_ascii_character(ord(character.data)) if len(character.data) == 1 else 0x00
|
||||||
|
|
||||||
self.terminal.display.buffered_write(byte, row=row, column=column)
|
self.terminal.display.buffered_write(regen_byte, 0x00, row=row, column=column)
|
||||||
|
|
||||||
self.vt100_screen.dirty.clear()
|
self.vt100_screen.dirty.clear()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
more-itertools==8.7.0
|
||||||
ptyprocess==0.7.0
|
ptyprocess==0.7.0
|
||||||
pycoax==0.7.0
|
pycoax==0.8.0
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
pyte==0.8.0
|
pyte==0.8.0
|
||||||
pytn3270==0.9.1
|
pytn3270==0.10.0
|
||||||
sliplib==0.6.2
|
sliplib==0.6.2
|
||||||
sortedcontainers==2.3.0
|
sortedcontainers==2.3.0
|
||||||
wcwidth==0.2.5
|
wcwidth==0.2.5
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class DisplayMoveCursorTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, dimensions, None)
|
||||||
|
|
||||||
self.display._load_address_counter = Mock(wraps=self.display._load_address_counter)
|
self.display._load_address_counter = Mock(wraps=self.display._load_address_counter)
|
||||||
|
|
||||||
@@ -58,40 +58,57 @@ class DisplayBufferedWriteTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, Dimensions(24, 80), None)
|
||||||
|
|
||||||
def test(self):
|
def test_with_no_eab(self):
|
||||||
# Act
|
# Act
|
||||||
self.display.buffered_write(0x01, index=15)
|
self.display.buffered_write(0x01, 0x00, index=15)
|
||||||
self.display.buffered_write(0x02, index=97)
|
self.display.buffered_write(0x02, 0x00, index=97)
|
||||||
|
self.display.buffered_write(0x00, 0x01, index=98)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.buffer[15], 0x01)
|
self.assertEqual(self.display.regen_buffer[15], 0x01)
|
||||||
self.assertEqual(self.display.buffer[97], 0x02)
|
self.assertEqual(self.display.regen_buffer[97], 0x02)
|
||||||
|
self.assertEqual(self.display.eab_buffer[98], 0x00)
|
||||||
self.assertSequenceEqual(self.display.dirty, [15, 97])
|
self.assertSequenceEqual(self.display.dirty, [15, 97])
|
||||||
|
|
||||||
|
def test_with_eab(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.display.buffered_write(0x01, 0x00, index=15)
|
||||||
|
self.display.buffered_write(0x02, 0x00, index=97)
|
||||||
|
self.display.buffered_write(0x00, 0x01, index=98)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertEqual(self.display.regen_buffer[15], 0x01)
|
||||||
|
self.assertEqual(self.display.regen_buffer[97], 0x02)
|
||||||
|
self.assertEqual(self.display.eab_buffer[98], 0x01)
|
||||||
|
self.assertSequenceEqual(self.display.dirty, [15, 97, 98])
|
||||||
|
|
||||||
def test_with_row_and_column(self):
|
def test_with_row_and_column(self):
|
||||||
# Act
|
# Act
|
||||||
self.display.buffered_write(0x01, row=0, column=15)
|
self.display.buffered_write(0x01, 0x00, row=0, column=15)
|
||||||
self.display.buffered_write(0x02, row=1, column=17)
|
self.display.buffered_write(0x02, 0x00, row=1, column=17)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.buffer[15], 0x01)
|
self.assertEqual(self.display.regen_buffer[15], 0x01)
|
||||||
self.assertEqual(self.display.buffer[97], 0x02)
|
self.assertEqual(self.display.regen_buffer[97], 0x02)
|
||||||
self.assertSequenceEqual(self.display.dirty, [15, 97])
|
self.assertSequenceEqual(self.display.dirty, [15, 97])
|
||||||
|
|
||||||
def test_change(self):
|
def test_change(self):
|
||||||
self.assertTrue(self.display.buffered_write(0x01, index=0))
|
self.assertTrue(self.display.buffered_write(0x01, 0x00, index=0))
|
||||||
self.assertTrue(self.display.buffered_write(0x02, index=0))
|
self.assertTrue(self.display.buffered_write(0x02, 0x00, index=0))
|
||||||
|
|
||||||
self.assertEqual(self.display.buffer[0], 0x02)
|
self.assertEqual(self.display.regen_buffer[0], 0x02)
|
||||||
self.assertSequenceEqual(self.display.dirty, [0])
|
self.assertSequenceEqual(self.display.dirty, [0])
|
||||||
|
|
||||||
def test_no_change(self):
|
def test_no_change(self):
|
||||||
self.assertTrue(self.display.buffered_write(0x01, index=0))
|
self.assertTrue(self.display.buffered_write(0x01, 0x00, index=0))
|
||||||
self.assertFalse(self.display.buffered_write(0x01, index=0))
|
self.assertFalse(self.display.buffered_write(0x01, 0x00, index=0))
|
||||||
|
|
||||||
self.assertEqual(self.display.buffer[0], 0x01)
|
self.assertEqual(self.display.regen_buffer[0], 0x01)
|
||||||
self.assertSequenceEqual(self.display.dirty, [0])
|
self.assertSequenceEqual(self.display.dirty, [0])
|
||||||
|
|
||||||
class DisplayFlushTestCase(unittest.TestCase):
|
class DisplayFlushTestCase(unittest.TestCase):
|
||||||
@@ -100,7 +117,7 @@ class DisplayFlushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, dimensions, None)
|
||||||
|
|
||||||
self.display._flush_range = Mock()
|
self.display._flush_range = Mock()
|
||||||
|
|
||||||
@@ -111,11 +128,12 @@ class DisplayFlushTestCase(unittest.TestCase):
|
|||||||
# Assert
|
# Assert
|
||||||
self.display._flush_range.assert_not_called()
|
self.display._flush_range.assert_not_called()
|
||||||
|
|
||||||
def test_single_range(self):
|
def test_single_range_with_no_eab(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
self.display.buffered_write(0x01, index=0)
|
self.display.buffered_write(0x01, 0x00, index=0)
|
||||||
self.display.buffered_write(0x02, index=1)
|
self.display.buffered_write(0x02, 0x00, index=1)
|
||||||
self.display.buffered_write(0x03, index=2)
|
self.display.buffered_write(0x03, 0x00, index=2)
|
||||||
|
self.display.buffered_write(0x00, 0x01, index=3)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display.flush()
|
self.display.flush()
|
||||||
@@ -123,14 +141,32 @@ class DisplayFlushTestCase(unittest.TestCase):
|
|||||||
# Assert
|
# Assert
|
||||||
self.display._flush_range.assert_called_with(0, 2)
|
self.display._flush_range.assert_called_with(0, 2)
|
||||||
|
|
||||||
def test_multiple_ranges(self):
|
def test_single_range_with_eab(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
self.display.buffered_write(0x01, index=0)
|
self.display.eab_address = 7
|
||||||
self.display.buffered_write(0x02, index=1)
|
|
||||||
self.display.buffered_write(0x03, index=2)
|
self.display.buffered_write(0x01, 0x00, index=0)
|
||||||
self.display.buffered_write(0x05, index=30)
|
self.display.buffered_write(0x02, 0x00, index=1)
|
||||||
self.display.buffered_write(0x06, index=31)
|
self.display.buffered_write(0x03, 0x00, index=2)
|
||||||
self.display.buffered_write(0x04, index=20)
|
self.display.buffered_write(0x00, 0x01, index=3)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.display.flush()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.display._flush_range.assert_called_with(0, 3)
|
||||||
|
|
||||||
|
def test_multiple_ranges_with_no_eab(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.buffered_write(0x01, 0x00, index=0)
|
||||||
|
self.display.buffered_write(0x02, 0x00, index=1)
|
||||||
|
self.display.buffered_write(0x03, 0x01, index=2)
|
||||||
|
self.display.buffered_write(0x00, 0x02, index=3)
|
||||||
|
self.display.buffered_write(0x05, 0x00, index=30)
|
||||||
|
self.display.buffered_write(0x06, 0x00, index=31)
|
||||||
|
self.display.buffered_write(0x00, 0x05, index=32)
|
||||||
|
self.display.buffered_write(0x04, 0x03, index=20)
|
||||||
|
self.display.buffered_write(0x00, 0x04, index=21)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display.flush()
|
self.display.flush()
|
||||||
@@ -138,13 +174,33 @@ class DisplayFlushTestCase(unittest.TestCase):
|
|||||||
# Assert
|
# Assert
|
||||||
self.display._flush_range.assert_called_with(0, 31)
|
self.display._flush_range.assert_called_with(0, 31)
|
||||||
|
|
||||||
|
def test_multiple_ranges_with_no_eab(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
self.display.buffered_write(0x01, 0x00, index=0)
|
||||||
|
self.display.buffered_write(0x02, 0x00, index=1)
|
||||||
|
self.display.buffered_write(0x03, 0x01, index=2)
|
||||||
|
self.display.buffered_write(0x00, 0x02, index=3)
|
||||||
|
self.display.buffered_write(0x05, 0x00, index=30)
|
||||||
|
self.display.buffered_write(0x06, 0x00, index=31)
|
||||||
|
self.display.buffered_write(0x00, 0x05, index=32)
|
||||||
|
self.display.buffered_write(0x04, 0x03, index=20)
|
||||||
|
self.display.buffered_write(0x00, 0x04, index=21)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.display.flush()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.display._flush_range.assert_called_with(0, 32)
|
||||||
|
|
||||||
class DisplayClearTestCase(unittest.TestCase):
|
class DisplayClearTestCase(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.interface = Mock()
|
self.interface = Mock()
|
||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, dimensions, None)
|
||||||
|
|
||||||
self.display._load_address_counter = Mock(wraps=self.display._load_address_counter)
|
self.display._load_address_counter = Mock(wraps=self.display._load_address_counter)
|
||||||
self.display._write = Mock(wraps=self.display._write)
|
self.display._write = Mock(wraps=self.display._write)
|
||||||
@@ -161,40 +217,90 @@ class DisplayClearTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.write_data_mock = patcher.start()
|
self.write_data_mock = patcher.start()
|
||||||
|
|
||||||
|
patcher = patch('oec.display.eab_write_alternate')
|
||||||
|
|
||||||
|
self.eab_write_alternate_mock = patcher.start()
|
||||||
|
|
||||||
self.addCleanup(patch.stopall)
|
self.addCleanup(patch.stopall)
|
||||||
|
|
||||||
def test_excluding_status_line(self):
|
def test_excluding_status_line_with_no_eab(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
self.display.buffered_write(0x01, index=0)
|
self.display.buffered_write(0x01, 0x01, index=0)
|
||||||
|
|
||||||
self.assertEqual(self.display.buffer[0], 0x01)
|
self.assertEqual(self.display.regen_buffer[0], 0x01)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x00)
|
||||||
self.assertTrue(self.display.dirty)
|
self.assertTrue(self.display.dirty)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display.clear(clear_status_line=False)
|
self.display.clear(clear_status_line=False)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.display._write.assert_called_with((b'\x00', 1920), address=80)
|
self.display._write.assert_called_with((b'\x00', 1920), None, address=80)
|
||||||
self.display._load_address_counter.assert_called_with(80, True)
|
self.display._load_address_counter.assert_called_with(80, True)
|
||||||
|
|
||||||
self.assertEqual(self.display.buffer[0], 0x00)
|
self.assertEqual(self.display.regen_buffer[0], 0x00)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x00)
|
||||||
self.assertFalse(self.display.dirty)
|
self.assertFalse(self.display.dirty)
|
||||||
|
|
||||||
def test_including_status_line(self):
|
def test_excluding_status_line_with_eab(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
self.display.buffered_write(0x01, index=0)
|
self.display.eab_address = 7
|
||||||
|
|
||||||
self.assertEqual(self.display.buffer[0], 0x01)
|
self.display.buffered_write(0x01, 0x01, index=0)
|
||||||
|
|
||||||
|
self.assertEqual(self.display.regen_buffer[0], 0x01)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x01)
|
||||||
|
self.assertTrue(self.display.dirty)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.display.clear(clear_status_line=False)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.display._write.assert_called_with((b'\x00', 1920), (b'\x00', 1920), address=80)
|
||||||
|
self.display._load_address_counter.assert_called_with(80, True)
|
||||||
|
|
||||||
|
self.assertEqual(self.display.regen_buffer[0], 0x00)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x00)
|
||||||
|
self.assertFalse(self.display.dirty)
|
||||||
|
|
||||||
|
def test_including_status_line_with_no_eab(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.buffered_write(0x01, 0x01, index=0)
|
||||||
|
|
||||||
|
self.assertEqual(self.display.regen_buffer[0], 0x01)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x00)
|
||||||
self.assertTrue(self.display.dirty)
|
self.assertTrue(self.display.dirty)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display.clear(clear_status_line=True)
|
self.display.clear(clear_status_line=True)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.display._write.assert_called_with((b'\x00', 2000), address=0)
|
self.display._write.assert_called_with((b'\x00', 2000), None, address=0)
|
||||||
self.display._load_address_counter.assert_called_with(80, True)
|
self.display._load_address_counter.assert_called_with(80, True)
|
||||||
|
|
||||||
self.assertEqual(self.display.buffer[0], 0x00)
|
self.assertEqual(self.display.regen_buffer[0], 0x00)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x00)
|
||||||
|
self.assertFalse(self.display.dirty)
|
||||||
|
|
||||||
|
def test_including_status_line_with_eab(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
self.display.buffered_write(0x01, 0x01, index=0)
|
||||||
|
|
||||||
|
self.assertEqual(self.display.regen_buffer[0], 0x01)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x01)
|
||||||
|
self.assertTrue(self.display.dirty)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.display.clear(clear_status_line=True)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.display._write.assert_called_with((b'\x00', 2000), (b'\x00', 2000), address=0)
|
||||||
|
self.display._load_address_counter.assert_called_with(80, True)
|
||||||
|
|
||||||
|
self.assertEqual(self.display.regen_buffer[0], 0x00)
|
||||||
|
self.assertEqual(self.display.eab_buffer[0], 0x00)
|
||||||
self.assertFalse(self.display.dirty)
|
self.assertFalse(self.display.dirty)
|
||||||
|
|
||||||
class DisplayFlushRangeTestCase(unittest.TestCase):
|
class DisplayFlushRangeTestCase(unittest.TestCase):
|
||||||
@@ -203,7 +309,7 @@ class DisplayFlushRangeTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, dimensions, None)
|
||||||
|
|
||||||
self.display._write = Mock(wraps=self.display._write)
|
self.display._write = Mock(wraps=self.display._write)
|
||||||
|
|
||||||
@@ -225,15 +331,15 @@ class DisplayFlushRangeTestCase(unittest.TestCase):
|
|||||||
# Arrange
|
# Arrange
|
||||||
self.display.move_cursor(index=0)
|
self.display.move_cursor(index=0)
|
||||||
|
|
||||||
self.display.buffered_write(0x01, index=0)
|
self.display.buffered_write(0x01, 0x00, index=0)
|
||||||
self.display.buffered_write(0x02, index=1)
|
self.display.buffered_write(0x02, 0x00, index=1)
|
||||||
self.display.buffered_write(0x03, index=2)
|
self.display.buffered_write(0x03, 0x00, index=2)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display.flush()
|
self.display.flush()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.display._write.assert_called_with(bytes.fromhex('01 02 03'), address=80)
|
self.display._write.assert_called_with(bytes.fromhex('01 02 03'), None, address=80)
|
||||||
|
|
||||||
self.assertEqual(self.display.address_counter, 83)
|
self.assertEqual(self.display.address_counter, 83)
|
||||||
self.assertFalse(self.display.dirty)
|
self.assertFalse(self.display.dirty)
|
||||||
@@ -242,15 +348,15 @@ class DisplayFlushRangeTestCase(unittest.TestCase):
|
|||||||
# Arrange
|
# Arrange
|
||||||
self.display.move_cursor(index=70)
|
self.display.move_cursor(index=70)
|
||||||
|
|
||||||
self.display.buffered_write(0x01, index=0)
|
self.display.buffered_write(0x01, 0x00, index=0)
|
||||||
self.display.buffered_write(0x02, index=1)
|
self.display.buffered_write(0x02, 0x00, index=1)
|
||||||
self.display.buffered_write(0x03, index=2)
|
self.display.buffered_write(0x03, 0x00, index=2)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display.flush()
|
self.display.flush()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.display._write.assert_called_with(bytes.fromhex('01 02 03'), address=80)
|
self.display._write.assert_called_with(bytes.fromhex('01 02 03'), None, address=80)
|
||||||
|
|
||||||
self.assertEqual(self.display.address_counter, 83)
|
self.assertEqual(self.display.address_counter, 83)
|
||||||
self.assertFalse(self.display.dirty)
|
self.assertFalse(self.display.dirty)
|
||||||
@@ -261,7 +367,7 @@ class DisplayLoadAddressCounterTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, dimensions, None)
|
||||||
|
|
||||||
patcher = patch('oec.display.load_address_counter_hi')
|
patcher = patch('oec.display.load_address_counter_hi')
|
||||||
|
|
||||||
@@ -369,7 +475,7 @@ class DisplayWriteTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
dimensions = Dimensions(24, 80)
|
dimensions = Dimensions(24, 80)
|
||||||
|
|
||||||
self.display = Display(self.interface, dimensions)
|
self.display = Display(self.interface, dimensions, None)
|
||||||
|
|
||||||
self.display._load_address_counter = Mock(wraps=self.display._load_address_counter)
|
self.display._load_address_counter = Mock(wraps=self.display._load_address_counter)
|
||||||
|
|
||||||
@@ -385,32 +491,84 @@ class DisplayWriteTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.write_data_mock = patcher.start()
|
self.write_data_mock = patcher.start()
|
||||||
|
|
||||||
|
patcher = patch('oec.display.eab_write_alternate')
|
||||||
|
|
||||||
|
self.eab_write_alternate_mock = patcher.start()
|
||||||
|
|
||||||
self.addCleanup(patch.stopall)
|
self.addCleanup(patch.stopall)
|
||||||
|
|
||||||
def test(self):
|
def test_with_no_eab_data(self):
|
||||||
# Act
|
# Act
|
||||||
self.display._write(bytes.fromhex('01 02 03'))
|
self.display._write(bytes.fromhex('01 02 03'), None)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertIsNone(self.display.address_counter)
|
self.assertIsNone(self.display.address_counter)
|
||||||
|
|
||||||
self.write_data_mock.assert_called_with(self.interface, bytes.fromhex('01 02 03'))
|
self.write_data_mock.assert_called_with(self.interface, bytes.fromhex('01 02 03'), jumbo_write_strategy=None)
|
||||||
|
|
||||||
|
def test_with_eab_data(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
def test_repeat(self):
|
|
||||||
# Act
|
# Act
|
||||||
self.display._write((bytes.fromhex('01 02 03'), 3))
|
self.display._write(bytes.fromhex('01 02 03'), bytes.fromhex('04 05 06'))
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertIsNone(self.display.address_counter)
|
self.assertIsNone(self.display.address_counter)
|
||||||
|
|
||||||
self.write_data_mock.assert_called_with(self.interface, (bytes.fromhex('01 02 03'), 3))
|
self.eab_write_alternate_mock.assert_called_with(self.interface, 7, bytes.fromhex('01 04 02 05 03 06'), jumbo_write_strategy=None)
|
||||||
|
|
||||||
|
def test_repeat_with_no_eab_data(self):
|
||||||
|
# Act
|
||||||
|
self.display._write((bytes.fromhex('01 02 03'), 3), None)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsNone(self.display.address_counter)
|
||||||
|
|
||||||
|
self.write_data_mock.assert_called_with(self.interface, (bytes.fromhex('01 02 03'), 3), jumbo_write_strategy=None)
|
||||||
|
|
||||||
|
def test_repeat_with_eab_data(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
# Act
|
||||||
|
self.display._write((bytes.fromhex('01 02 03'), 3), (bytes.fromhex('04 05 06'), 3))
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
self.assertIsNone(self.display.address_counter)
|
||||||
|
|
||||||
|
self.eab_write_alternate_mock.assert_called_with(self.interface, 7, (bytes.fromhex('01 04 02 05 03 06'), 3), jumbo_write_strategy=None)
|
||||||
|
|
||||||
|
def test_regen_eab_data_mismatch_format(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
# Act and assert
|
||||||
|
with self.assertRaisesRegex(ValueError, 'must be provided in same form'):
|
||||||
|
self.display._write(bytes.fromhex('01 02 03'), (b'\x00', 3))
|
||||||
|
|
||||||
|
def test_regen_eab_data_mismatch_length(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
# Act and assert
|
||||||
|
with self.assertRaisesRegex(ValueError, 'data length must be equal'):
|
||||||
|
self.display._write(bytes.fromhex('01 02 03'), bytes.fromhex('01 02'))
|
||||||
|
|
||||||
|
def test_regen_eab_data_mismatch_length_repeat(self):
|
||||||
|
# Arrange
|
||||||
|
self.display.eab_address = 7
|
||||||
|
|
||||||
|
# Act and assert
|
||||||
|
with self.assertRaisesRegex(ValueError, 'pattern length must be equal'):
|
||||||
|
self.display._write((bytes.fromhex('01 02 03'), 3), (b'\x00', 2))
|
||||||
|
|
||||||
def test_address_if_current_address_unknown(self):
|
def test_address_if_current_address_unknown(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
self.assertIsNone(self.display.address_counter)
|
self.assertIsNone(self.display.address_counter)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display._write(bytes.fromhex('01 02 03'), address=80)
|
self.display._write(bytes.fromhex('01 02 03'), None, address=80)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.address_counter, 83)
|
self.assertEqual(self.display.address_counter, 83)
|
||||||
@@ -420,7 +578,7 @@ class DisplayWriteTestCase(unittest.TestCase):
|
|||||||
self.display.address_counter = 160
|
self.display.address_counter = 160
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display._write(bytes.fromhex('01 02 03'), address=80)
|
self.display._write(bytes.fromhex('01 02 03'), None, address=80)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.address_counter, 83)
|
self.assertEqual(self.display.address_counter, 83)
|
||||||
@@ -432,7 +590,7 @@ class DisplayWriteTestCase(unittest.TestCase):
|
|||||||
self.display.address_counter = 80
|
self.display.address_counter = 80
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display._write(bytes.fromhex('01 02 03'), address=80)
|
self.display._write(bytes.fromhex('01 02 03'), None, address=80)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.address_counter, 83)
|
self.assertEqual(self.display.address_counter, 83)
|
||||||
@@ -446,7 +604,7 @@ class DisplayWriteTestCase(unittest.TestCase):
|
|||||||
self.assertIsNone(self.display.address_counter)
|
self.assertIsNone(self.display.address_counter)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display._write(bytes.fromhex('01 02 03'), restore_original_address=True)
|
self.display._write(bytes.fromhex('01 02 03'), None, restore_original_address=True)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.address_counter, 160)
|
self.assertEqual(self.display.address_counter, 160)
|
||||||
@@ -458,7 +616,7 @@ class DisplayWriteTestCase(unittest.TestCase):
|
|||||||
self.display.address_counter = 160
|
self.display.address_counter = 160
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
self.display._write(bytes.fromhex('01 02 03'), restore_original_address=True)
|
self.display._write(bytes.fromhex('01 02 03'), None, restore_original_address=True)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.assertEqual(self.display.address_counter, 160)
|
self.assertEqual(self.display.address_counter, 160)
|
||||||
|
|||||||
@@ -4,15 +4,26 @@ from unittest.mock import Mock
|
|||||||
import context
|
import context
|
||||||
|
|
||||||
from oec.session import SessionDisconnectedError
|
from oec.session import SessionDisconnectedError
|
||||||
|
from oec.display import Dimensions, Display
|
||||||
from oec.keyboard import Key, KeyboardModifiers
|
from oec.keyboard import Key, KeyboardModifiers
|
||||||
from oec.tn3270 import TN3270Session
|
from oec.tn3270 import TN3270Session
|
||||||
from tn3270 import AttributeCell, CharacterCell, AID, ProtectedCellOperatorError, FieldOverflowOperatorError
|
from tn3270 import AttributeCell, CharacterCell, AID, Color, ProtectedCellOperatorError, FieldOverflowOperatorError
|
||||||
|
from tn3270.attributes import Attribute
|
||||||
|
from tn3270.emulator import CellFormatting
|
||||||
|
|
||||||
class SessionHandleHostTestCase(unittest.TestCase):
|
class SessionHandleHostTestCase(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.interface = Mock()
|
||||||
|
|
||||||
self.terminal = Mock()
|
self.terminal = Mock()
|
||||||
|
|
||||||
self.terminal.display = MockDisplay(24, 80)
|
self.terminal.display = Display(self.interface, Dimensions(24, 80), None)
|
||||||
|
|
||||||
|
self.terminal.display.status_line = Mock()
|
||||||
|
self.terminal.display.move_cursor = Mock()
|
||||||
|
self.terminal.display.flush = Mock()
|
||||||
|
self.terminal.display._load_address_counter = Mock()
|
||||||
|
self.terminal.display._write = Mock()
|
||||||
|
|
||||||
self.session = TN3270Session(self.terminal, 'mainframe', 23)
|
self.session = TN3270Session(self.terminal, 'mainframe', 23)
|
||||||
|
|
||||||
@@ -28,28 +39,36 @@ class SessionHandleHostTestCase(unittest.TestCase):
|
|||||||
# Act and assert
|
# Act and assert
|
||||||
self.assertFalse(self.session.handle_host())
|
self.assertFalse(self.session.handle_host())
|
||||||
|
|
||||||
def test_changes(self):
|
def test_changes_with_no_eab(self):
|
||||||
# Arrange
|
# Arrange
|
||||||
|
self.terminal.display.eab_address = None
|
||||||
|
|
||||||
self.session.emulator.update = Mock(return_value=True)
|
self.session.emulator.update = Mock(return_value=True)
|
||||||
|
|
||||||
cells = _create_screen_cells(24, 80)
|
cells = _create_screen_cells(24, 80)
|
||||||
|
|
||||||
_set_attribute(cells, 0, MockAttribute(protected=True))
|
_set_attribute(cells, 0, protected=True)
|
||||||
_set_characters(cells, 1, 'PROTECTED'.encode('cp500'))
|
_set_characters(cells, 1, 'PROTECTED'.encode('cp500'))
|
||||||
_set_attribute(cells, 10, MockAttribute(protected=True, intensified=True))
|
_set_attribute(cells, 10, protected=True, intensified=True)
|
||||||
_set_characters(cells, 11, 'PROTECTED INTENSIFIED'.encode('cp500'))
|
_set_characters(cells, 11, 'PROTECTED INTENSIFIED'.encode('cp500'))
|
||||||
_set_attribute(cells, 32, MockAttribute(protected=True, hidden=True))
|
_set_attribute(cells, 32, protected=True, hidden=True)
|
||||||
_set_characters(cells, 33, 'PROTECTED HIDDEN'.encode('cp500'))
|
_set_characters(cells, 33, 'PROTECTED HIDDEN'.encode('cp500'))
|
||||||
_set_attribute(cells, 49, MockAttribute(protected=False))
|
_set_attribute(cells, 49, protected=False)
|
||||||
_set_characters(cells, 50, 'UNPROTECTED'.encode('cp500'))
|
_set_characters(cells, 50, 'UNPROTECTED'.encode('cp500'))
|
||||||
_set_attribute(cells, 61, MockAttribute(protected=False, intensified=True))
|
_set_attribute(cells, 61, protected=False, intensified=True)
|
||||||
_set_characters(cells, 62, 'UNPROTECTED INTENSIFIED'.encode('cp500'))
|
_set_characters(cells, 62, 'UNPROTECTED INTENSIFIED'.encode('cp500'))
|
||||||
_set_attribute(cells, 85, MockAttribute(protected=False, hidden=True))
|
_set_attribute(cells, 85, protected=False, hidden=True)
|
||||||
_set_characters(cells, 86, 'UNPROTECTED HIDDEN'.encode('cp500'))
|
_set_characters(cells, 86, 'UNPROTECTED HIDDEN'.encode('cp500'))
|
||||||
_set_attribute(cells, 104, MockAttribute(protected=True))
|
_set_attribute(cells, 104, protected=True)
|
||||||
|
_set_formatting(cells, 104, color=Color.YELLOW)
|
||||||
|
_set_characters(cells, 105, 'EAB'.encode('cp500'))
|
||||||
|
_set_formatting(cells, 105, blink=True)
|
||||||
|
_set_formatting(cells, 106, reverse=True)
|
||||||
|
_set_formatting(cells, 107, underscore=True)
|
||||||
|
_set_attribute(cells, 108, protected=True)
|
||||||
|
|
||||||
self.session.emulator.cells = cells
|
self.session.emulator.cells = cells
|
||||||
self.session.emulator.dirty = set(range(105))
|
self.session.emulator.dirty = set(range(109))
|
||||||
|
|
||||||
self.session.emulator.cursor_address = 8
|
self.session.emulator.cursor_address = 8
|
||||||
|
|
||||||
@@ -58,10 +77,61 @@ class SessionHandleHostTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.terminal.display.flush.assert_called()
|
self.terminal.display.flush.assert_called()
|
||||||
|
|
||||||
self.assertEqual(self.terminal.display.buffer[:105], bytes.fromhex('e0afb1aeb3a4a2b3a4a3e8afb1aeb3a4a2b3a4a300a8adb3a4adb2a8a5a8a4a3ecafb1aeb3a4a2b3a4a300a7a8a3a3a4adc0b4adafb1aeb3a4a2b3a4a3c8b4adafb1aeb3a4a2b3a4a300a8adb3a4adb2a8a5a8a4a3ccb4adafb1aeb3a4a2b3a4a300a7a8a3a3a4ade0'))
|
self.assertEqual(self.terminal.display.regen_buffer[:109], bytes.fromhex('e0afb1aeb3a4a2b3a4a3e8afb1aeb3a4a2b3a4a300a8adb3a4adb2a8a5a8a4a3ecafb1aeb3a4a2b3a4a300a7a8a3a3a4adc0b4adafb1aeb3a4a2b3a4a3c8b4adafb1aeb3a4a2b3a4a300a8adb3a4adb2a8a5a8a4a3ccb4adafb1aeb3a4a2b3a4a300a7a8a3a3a4ade0a4a0a1e0'))
|
||||||
self.assertTrue(all([byte == 0x00 for byte in self.terminal.display.buffer[105:]]))
|
self.assertTrue(all([byte == 0x00 for byte in self.terminal.display.regen_buffer[109:]]))
|
||||||
|
|
||||||
self.assertEqual(self.terminal.display.cursor_index, 8)
|
self.assertTrue(all([byte == 0x00 for byte in self.terminal.display.eab_buffer]))
|
||||||
|
|
||||||
|
self.terminal.display.move_cursor.assert_called_with(index=8)
|
||||||
|
|
||||||
|
self.assertFalse(self.session.emulator.dirty)
|
||||||
|
|
||||||
|
def test_changes_with_eab(self):
|
||||||
|
# Arrange
|
||||||
|
self.terminal.display.eab_address = 7
|
||||||
|
|
||||||
|
self.session.emulator.update = Mock(return_value=True)
|
||||||
|
|
||||||
|
cells = _create_screen_cells(24, 80)
|
||||||
|
|
||||||
|
_set_attribute(cells, 0, protected=True)
|
||||||
|
_set_characters(cells, 1, 'PROTECTED'.encode('cp500'))
|
||||||
|
_set_attribute(cells, 10, protected=True, intensified=True)
|
||||||
|
_set_characters(cells, 11, 'PROTECTED INTENSIFIED'.encode('cp500'))
|
||||||
|
_set_attribute(cells, 32, protected=True, hidden=True)
|
||||||
|
_set_characters(cells, 33, 'PROTECTED HIDDEN'.encode('cp500'))
|
||||||
|
_set_attribute(cells, 49, protected=False)
|
||||||
|
_set_characters(cells, 50, 'UNPROTECTED'.encode('cp500'))
|
||||||
|
_set_attribute(cells, 61, protected=False, intensified=True)
|
||||||
|
_set_characters(cells, 62, 'UNPROTECTED INTENSIFIED'.encode('cp500'))
|
||||||
|
_set_attribute(cells, 85, protected=False, hidden=True)
|
||||||
|
_set_characters(cells, 86, 'UNPROTECTED HIDDEN'.encode('cp500'))
|
||||||
|
_set_attribute(cells, 104, protected=True)
|
||||||
|
_set_formatting(cells, 104, color=Color.YELLOW)
|
||||||
|
_set_characters(cells, 105, 'EAB'.encode('cp500'))
|
||||||
|
_set_formatting(cells, 105, blink=True)
|
||||||
|
_set_formatting(cells, 106, reverse=True)
|
||||||
|
_set_formatting(cells, 107, underscore=True)
|
||||||
|
_set_attribute(cells, 108, protected=True)
|
||||||
|
|
||||||
|
self.session.emulator.cells = cells
|
||||||
|
self.session.emulator.dirty = set(range(109))
|
||||||
|
|
||||||
|
self.session.emulator.cursor_address = 8
|
||||||
|
|
||||||
|
# Act and assert
|
||||||
|
self.assertTrue(self.session.handle_host())
|
||||||
|
|
||||||
|
self.terminal.display.flush.assert_called()
|
||||||
|
|
||||||
|
self.assertEqual(self.terminal.display.regen_buffer[:109], bytes.fromhex('e0afb1aeb3a4a2b3a4a3e8afb1aeb3a4a2b3a4a300a8adb3a4adb2a8a5a8a4a3ecafb1aeb3a4a2b3a4a300a7a8a3a3a4adc0b4adafb1aeb3a4a2b3a4a3c8b4adafb1aeb3a4a2b3a4a300a8adb3a4adb2a8a5a8a4a3ccb4adafb1aeb3a4a2b3a4a300a7a8a3a3a4ade0a4a0a1e0'))
|
||||||
|
self.assertTrue(all([byte == 0x00 for byte in self.terminal.display.regen_buffer[109:]]))
|
||||||
|
|
||||||
|
self.assertTrue(all([byte == 0x00 for byte in self.terminal.display.eab_buffer[:104]]))
|
||||||
|
self.assertEqual(self.terminal.display.eab_buffer[104:109], bytes.fromhex('304080c000'))
|
||||||
|
self.assertTrue(all([byte == 0x00 for byte in self.terminal.display.eab_buffer[109:]]))
|
||||||
|
|
||||||
|
self.terminal.display.move_cursor.assert_called_with(index=8)
|
||||||
|
|
||||||
self.assertFalse(self.session.emulator.dirty)
|
self.assertFalse(self.session.emulator.dirty)
|
||||||
|
|
||||||
@@ -254,41 +324,32 @@ class SessionHandleKeyTestCase(unittest.TestCase):
|
|||||||
# Assert
|
# Assert
|
||||||
self.terminal.display.status_line.write.assert_called_with(8, bytes.fromhex('f600db080000000000'))
|
self.terminal.display.status_line.write.assert_called_with(8, bytes.fromhex('f600db080000000000'))
|
||||||
|
|
||||||
class MockDisplay:
|
|
||||||
def __init__(self, rows, columns):
|
|
||||||
self.buffer = bytearray(rows * columns)
|
|
||||||
self.cursor_index = None
|
|
||||||
|
|
||||||
self.status_line = Mock()
|
|
||||||
|
|
||||||
self.flush = Mock()
|
|
||||||
|
|
||||||
def buffered_write(self, byte, index):
|
|
||||||
self.buffer[index] = byte
|
|
||||||
|
|
||||||
def move_cursor(self, index):
|
|
||||||
self.cursor_index = index
|
|
||||||
|
|
||||||
class MockAttribute:
|
|
||||||
def __init__(self, protected=False, intensified=False, hidden=False):
|
|
||||||
self.protected = protected
|
|
||||||
self.intensified = intensified
|
|
||||||
self.hidden = hidden
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
display = 2 if self.intensified else 3 if self.hidden else 0
|
|
||||||
|
|
||||||
return (0x20 if self.protected else 0) | (display << 2)
|
|
||||||
|
|
||||||
def _create_screen_cells(rows, columns):
|
def _create_screen_cells(rows, columns):
|
||||||
return [CharacterCell(0x00) for address in range(rows * columns)]
|
return [CharacterCell(0x00) for address in range(rows * columns)]
|
||||||
|
|
||||||
def _set_attribute(screen, index, attribute):
|
def _set_attribute(cells, index, protected=False, intensified=False, hidden=False):
|
||||||
screen[index] = AttributeCell(attribute)
|
display = 2 if intensified else 3 if hidden else 0
|
||||||
|
|
||||||
def _set_characters(screen, index, bytes_):
|
attribute = Attribute((0x20 if protected else 0) | (display << 2))
|
||||||
|
|
||||||
|
cells[index] = AttributeCell(attribute)
|
||||||
|
|
||||||
|
def _set_characters(cells, index, bytes_):
|
||||||
for byte in bytes_:
|
for byte in bytes_:
|
||||||
screen[index] = CharacterCell(byte)
|
cells[index] = CharacterCell(byte)
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
|
def _set_formatting(cells, index, color=0x00, blink=False, reverse=False, underscore=False):
|
||||||
|
if color == 0x00 and not blink and not reverse and not underscore:
|
||||||
|
cells[index].formatting = None
|
||||||
|
return
|
||||||
|
|
||||||
|
formatting = CellFormatting()
|
||||||
|
|
||||||
|
formatting.color = color
|
||||||
|
formatting.blink = blink
|
||||||
|
formatting.reverse = reverse
|
||||||
|
formatting.underscore = underscore
|
||||||
|
|
||||||
|
cells[index].formatting = formatting
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class SessionHandleHostTestCase(unittest.TestCase):
|
|||||||
self.session.handle_host()
|
self.session.handle_host()
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
self.terminal.display.buffered_write.assert_any_call(0x80, row=0, column=0)
|
self.terminal.display.buffered_write.assert_any_call(0x80, 0x00, row=0, column=0)
|
||||||
self.terminal.display.buffered_write.assert_any_call(0x81, row=0, column=1)
|
self.terminal.display.buffered_write.assert_any_call(0x81, 0x00, row=0, column=1)
|
||||||
self.terminal.display.buffered_write.assert_any_call(0x82, row=0, column=2)
|
self.terminal.display.buffered_write.assert_any_call(0x82, 0x00, row=0, column=2)
|
||||||
|
|
||||||
self.terminal.display.flush.assert_called()
|
self.terminal.display.flush.assert_called()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user