Skip to content

Commit 5c773e8

Browse files
committed
IBM 3179 EAB support
1 parent b5bea24 commit 5c773e8

7 files changed

Lines changed: 123 additions & 102 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ emulation.
3333

3434
Only CUT (Control Unit Terminal) type terminals are supported. I have tested oec with the following terminals:
3535

36+
* IBM 3179
3637
* IBM 3278-2
3738
* IBM 3472
3839
* IBM 3483-V (InfoWindow II)

oec/__main__.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import os
33
import signal
44
import logging
5-
from coax import open_serial_interface, TerminalType
5+
from coax import open_serial_interface, TerminalType, Feature
66

77
from .args import parse_args
88
from .interface import InterfaceWrapper
99
from .controller import Controller
10-
from .device import get_ids, get_features, get_keyboard_description, UnsupportedDeviceError
11-
from .terminal import Terminal
10+
from .device import get_ids, get_features, UnsupportedDeviceError
11+
from .terminal import Terminal, get_model, get_keyboard_description
1212
from .tn3270 import TN3270Session
1313

1414
# VT100 emulation is not supported on Windows.
@@ -40,34 +40,35 @@ def _get_keymap(_args, keyboard_description):
4040
return KEYMAP_3278_TYPEWRITER
4141

4242
def _create_device(args, interface, device_address, _poll_response):
43-
# Read the terminal identifiers.
4443
(terminal_id, extended_id) = get_ids(interface, device_address)
4544

46-
logger.info(f'Terminal ID = {terminal_id}')
45+
logger.info(f'Terminal ID = {terminal_id}, Extended ID = {extended_id}')
4746

4847
if terminal_id.type != TerminalType.CUT:
4948
raise UnsupportedDeviceError('Only CUT type terminals are supported')
5049

51-
logger.info(f'Extended ID = {extended_id}')
50+
model = get_model(terminal_id, extended_id)
5251

53-
if extended_id is not None:
54-
logger.info(f'Model = IBM {extended_id[2:6]} or equivalent')
52+
if model is not None:
53+
logger.info(f'Model = IBM {model} or equivalent')
5554

56-
keyboard_description = get_keyboard_description(terminal_id, extended_id)
57-
58-
logger.info(f'Keyboard = {keyboard_description}')
59-
60-
# Read the terminal features.
6155
features = get_features(interface, device_address)
6256

57+
# The 3179 includes an EAB but does not respond to the READ_FEATURE_ID
58+
# command.
59+
if model == '3179':
60+
features[Feature.EAB] = 7
61+
6362
logger.info(f'Features = {features}')
6463

65-
# Get the keymap.
64+
keyboard_description = get_keyboard_description(terminal_id, extended_id)
65+
66+
logger.info(f'Keyboard = {keyboard_description}')
67+
6668
keymap = _get_keymap(args, keyboard_description)
6769

6870
logger.info(f'Keymap = {keymap.name}')
6971

70-
# Create the terminal.
7172
terminal = Terminal(interface, device_address, terminal_id, extended_id, features, keymap)
7273

7374
return terminal

oec/device.py

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def execute(self, commands):
3434
"""Execute one or more commands."""
3535
return self.interface.execute(address_commands(self.device_address, commands))
3636

37-
def execute_jumbo_write(self, data, create_first, create_subsequent, first_chunk_max_length_adjustment=-1):
38-
"""Execute a jumbo write command that can be split."""
37+
def prepare_jumbo_write(self, data, create_first, create_subsequent, first_chunk_max_length_adjustment=-1):
38+
"""Prepare a jumbo write command that can be split."""
3939
max_length = None
4040

4141
# The 3299 multiplexer appears to have some frame length limit, after which it will
@@ -55,7 +55,7 @@ def execute_jumbo_write(self, data, create_first, create_subsequent, first_chunk
5555
if len(commands) > 1 and logger.isEnabledFor(logging.DEBUG):
5656
logger.debug(f'Jumbo write split into {len(commands)}')
5757

58-
return self.execute(commands)
58+
return commands
5959

6060
class UnsupportedDeviceError(Exception):
6161
"""Unsupported device."""
@@ -111,65 +111,6 @@ def get_features(interface, device_address):
111111

112112
return parse_features(ids, commands)
113113

114-
def get_keyboard_description(terminal_id, extended_id):
115-
is_3278 = extended_id is None or not int(extended_id[0:2], 16) & 0x80
116-
117-
if is_3278:
118-
description = '3278'
119-
120-
id_map = {
121-
0b0001: 'APL',
122-
0b0010: 'TEXT',
123-
0b0100: 'TYPEWRITER-PSHICO',
124-
0b0101: 'APL',
125-
0b0110: 'TEXT',
126-
0b0111: 'APL-PSHICO',
127-
0b1000: 'DATAENTRY-2',
128-
0b1001: 'DATAENTRY-1',
129-
0b1010: 'TYPEWRITER',
130-
0b1100: 'DATAENTRY-2',
131-
0b1101: 'DATAENTRY-1',
132-
0b1110: 'TYPEWRITER'
133-
}
134-
135-
if terminal_id.keyboard in id_map:
136-
description += '-' + id_map[terminal_id.keyboard]
137-
138-
return description
139-
140-
id_ = int(extended_id[0:2], 16) & 0x1f
141-
142-
is_user = int(extended_id[0:2], 16) & 0x20
143-
144-
if is_user:
145-
description = 'USER'
146-
147-
if id_ in [1, 2, 3, 4]:
148-
description += f'-{id_}'
149-
150-
return description
151-
152-
is_ibm = not int(extended_id[6:8], 16) & 0x80
153-
154-
description = 'IBM' if is_ibm else 'UNKNOWN'
155-
156-
is_enhanced = int(extended_id[6:8], 16) & 0x01
157-
158-
if is_enhanced:
159-
if id_ == 1:
160-
return description + '-ENHANCED'
161-
162-
return None
163-
164-
if id_ == 1:
165-
return description + '-TYPEWRITER'
166-
elif id_ == 2:
167-
return description + '-DATAENTRY'
168-
elif id_ == 3:
169-
return description + '-APL'
170-
171-
return None
172-
173114
def _jumbo_write_split_data(data, max_length, first_chunk_max_length_adjustment=-1):
174115
if max_length is None:
175116
return [data]

oec/display.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,18 @@ def _load_address_counter(self, address, force_load):
175175
return True
176176

177177
def _write_data(self, data):
178-
self.terminal.execute_jumbo_write(data, WriteData, Data, -1)
178+
self.terminal.execute(self.terminal.prepare_jumbo_write(data, WriteData, Data, -1))
179179

180180
def _eab_write_alternate(self, data):
181+
# The EAB mask on a 3179 terminal appears to get reset regularly resulting
182+
# in the EAB buffer not being updated correctly. This does not affect
183+
# later terminals, loading the mask here for all terminals is simpler.
184+
#
181185
# The EAB_WRITE_ALTERNATE command data must be split so that the two bytes
182186
# do not get separated, otherwise the write will be incorrect.
183-
self.terminal.execute_jumbo_write(data, lambda chunk: EABWriteAlternate(self.eab_address, chunk), Data, -2)
187+
commands = [EABLoadMask(self.eab_address, 0xff), *self.terminal.prepare_jumbo_write(data, lambda chunk: EABWriteAlternate(self.eab_address, chunk), Data, -2)]
188+
189+
self.terminal.execute(commands)
184190

185191
def _split_address(address):
186192
if address is None:

oec/terminal.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,75 @@ def sound_alarm(self):
8181
def load_control_register(self):
8282
"""Execute a LOAD_CONTROL_REGISTER command."""
8383
self.execute(LoadControlRegister(self.control))
84+
85+
def get_model(terminal_id, extended_id):
86+
if extended_id is None:
87+
return None
88+
89+
model = extended_id[2:6]
90+
91+
# The 3179 does return an extended ID, but it does not include the model
92+
# like later terminals.
93+
if model == '0000':
94+
model = '3179'
95+
96+
return model
97+
98+
def get_keyboard_description(terminal_id, extended_id):
99+
is_3278 = extended_id is None or not int(extended_id[0:2], 16) & 0x80
100+
101+
if is_3278:
102+
description = '3278'
103+
104+
id_map = {
105+
0b0001: 'APL',
106+
0b0010: 'TEXT',
107+
0b0100: 'TYPEWRITER-PSHICO',
108+
0b0101: 'APL',
109+
0b0110: 'TEXT',
110+
0b0111: 'APL-PSHICO',
111+
0b1000: 'DATAENTRY-2',
112+
0b1001: 'DATAENTRY-1',
113+
0b1010: 'TYPEWRITER',
114+
0b1100: 'DATAENTRY-2',
115+
0b1101: 'DATAENTRY-1',
116+
0b1110: 'TYPEWRITER'
117+
}
118+
119+
if terminal_id.keyboard in id_map:
120+
description += '-' + id_map[terminal_id.keyboard]
121+
122+
return description
123+
124+
id_ = int(extended_id[0:2], 16) & 0x1f
125+
126+
is_user = int(extended_id[0:2], 16) & 0x20
127+
128+
if is_user:
129+
description = 'USER'
130+
131+
if id_ in [1, 2, 3, 4]:
132+
description += f'-{id_}'
133+
134+
return description
135+
136+
is_ibm = not int(extended_id[6:8], 16) & 0x80
137+
138+
description = 'IBM' if is_ibm else 'UNKNOWN'
139+
140+
is_enhanced = int(extended_id[6:8], 16) & 0x01
141+
142+
if is_enhanced:
143+
if id_ == 1:
144+
return description + '-ENHANCED'
145+
146+
return None
147+
148+
if id_ == 1:
149+
return description + '-TYPEWRITER'
150+
elif id_ == 2:
151+
return description + '-DATAENTRY'
152+
elif id_ == 3:
153+
return description + '-APL'
154+
155+
return None

tests/test_device.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import context
99

1010
from oec.interface import InterfaceWrapper
11-
from oec.device import address_commands, format_address, get_ids, get_features, get_keyboard_description, _jumbo_write_split_data
11+
from oec.device import address_commands, format_address, get_ids, get_features, _jumbo_write_split_data
1212

1313
from mock_interface import MockInterface
1414

@@ -161,27 +161,6 @@ def test_eab_feature(self):
161161
# Assert
162162
self.assertEqual(features, { Feature.EAB: 7 })
163163

164-
class GetKeyboardDescriptionTestCase(unittest.TestCase):
165-
def test(self):
166-
CASES = [
167-
(10, None, '3278-TYPEWRITER'),
168-
(0, 'c1347200', 'IBM-TYPEWRITER'),
169-
(10, '41347200', '3278-TYPEWRITER'),
170-
(0, 'c2347200', 'IBM-DATAENTRY'),
171-
(0, 'c3347200', 'IBM-APL'),
172-
(0, 'c1348301', 'IBM-ENHANCED'),
173-
(0, 'e1347200', 'USER-1'),
174-
(0, 'e4347200', 'USER-4')
175-
]
176-
177-
for (keyboard, extended_id, expected_description) in CASES:
178-
with self.subTest(keyboard=keyboard, extended_id=extended_id):
179-
terminal_id = TerminalId(0b0000_0100 | (keyboard << 4))
180-
181-
description = get_keyboard_description(terminal_id, extended_id)
182-
183-
self.assertEqual(description, expected_description)
184-
185164
class JumboWriteSplitDataTestCase(unittest.TestCase):
186165
def test_no_split_strategy(self):
187166
for data in [bytes(range(0, 64)), (bytes.fromhex('00'), 64)]:

tests/test_terminal.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from oec.interface import InterfaceWrapper
99
from oec.device import UnsupportedDeviceError
10-
from oec.terminal import Terminal
10+
from oec.terminal import Terminal, get_keyboard_description
1111
from oec.display import Display, StatusLine
1212
from oec.keymap_3278_typewriter import KEYMAP
1313

@@ -75,6 +75,27 @@ def test_with_enable_keyboard_clicker_queued(self):
7575
# Act and assert
7676
self.assertEqual(self.terminal.get_poll_action(), PollAction.ENABLE_KEYBOARD_CLICKER)
7777

78+
class GetKeyboardDescriptionTestCase(unittest.TestCase):
79+
def test(self):
80+
CASES = [
81+
(10, None, '3278-TYPEWRITER'),
82+
(0, 'c1347200', 'IBM-TYPEWRITER'),
83+
(10, '41347200', '3278-TYPEWRITER'),
84+
(0, 'c2347200', 'IBM-DATAENTRY'),
85+
(0, 'c3347200', 'IBM-APL'),
86+
(0, 'c1348301', 'IBM-ENHANCED'),
87+
(0, 'e1347200', 'USER-1'),
88+
(0, 'e4347200', 'USER-4')
89+
]
90+
91+
for (keyboard, extended_id, expected_description) in CASES:
92+
with self.subTest(keyboard=keyboard, extended_id=extended_id):
93+
terminal_id = TerminalId(0b0000_0100 | (keyboard << 4))
94+
95+
description = get_keyboard_description(terminal_id, extended_id)
96+
97+
self.assertEqual(description, expected_description)
98+
7899
def _create_terminal(interface):
79100
terminal_id = TerminalId(0b11110100)
80101
extended_id = 'c1348300'

0 commit comments

Comments
 (0)