Merge branch 'master' into i2

This commit is contained in:
Andrew Kay
2020-06-14 09:45:39 -05:00
22 changed files with 479 additions and 4936 deletions

View File

@@ -18,7 +18,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt --no-deps
pip install -r requirements.txt
working-directory: ./pycoax
- name: Run unit tests
run: ./run_unit_tests.sh

View File

@@ -1,14 +1,13 @@
# coax
> Hello mainframe!
Tools for interfacing with [IBM 3270](https://en.wikipedia.org/wiki/IBM_3270) type terminals.
## Contents
* [pycoax](pycoax) - Python IBM 3270 coaxial interface library
* [protocol](protocol/protocol.md) - Protocol documentation
* [interface1](interface1) - A serial attached Arduino interface using the National Semiconductor DP8340 and DP8341
* [pycoax](pycoax) - Python interface library
## See Also
* [oec](https://github.com/lowobservable/oec) - An open replacement for the IBM 3174 Establishment Controller
* [oec](https://github.com/lowobservable/oec) - IBM 3270 terminal controller (a replacement for the IBM 3174)

View File

@@ -2,11 +2,9 @@
A serial attached Arduino interface using the National Semiconductor DP8340 and DP8341.
## Schematic
## Hardware
![Schematic](hardware/schematic.svg)
## Bill of Materials
You can find the Gerber files for fabricating a PCB in the [fabrication](hardware/fabrication) directory. I have used JLCPCB to make the PCBs.
This interface requires an [Arduino Mega 2560 R3](https://store.arduino.cc/usa/mega-2560-r3).
@@ -31,3 +29,17 @@ This interface requires an [Arduino Mega 2560 R3](https://store.arduino.cc/usa/m
## Firmware
The firmware currently provides the ability to send commands and receive responses - it is designed to implement a terminal controller, not a terminal.
You will need [PlatformIO](https://platformio.org/) to build and upload the firmware, only Platform IO Core (the CLI) is required.
To build and upload the firmware for an Arduino Mega 2560 R3:
```
platformio run -t upload -e mega2560
```
For an original Arduino Mega:
```
platformio run -t upload -e mega1280
```

View File

@@ -1,6 +1,13 @@
[platformio]
default_envs = mega2560
[env]
framework = arduino
[env:megaatmega2560]
[env:mega1280]
platform = atmelavr
board = megaatmega1280
[env:mega2560]
platform = atmelavr
board = megaatmega2560

View File

@@ -64,11 +64,18 @@ void sendMessage(uint8_t *buffer, int bufferCount)
Serial.flush();
}
void sendErrorMessage(uint8_t code)
void sendErrorMessage(uint8_t code, const char *description)
{
uint8_t message[] = { 0x02, code };
uint8_t message[2 + 62 + 1] = { 0x02, code };
int count = 2;
sendMessage(message, 2);
if (description != NULL) {
strncpy((char *) (message + 2), description, 62);
count += strlen(description);
}
sendMessage(message, count);
}
#define COMMAND_RESET 0x01
@@ -86,7 +93,7 @@ void handleResetCommand(uint8_t *buffer, int bufferCount)
void handleTransmitReceiveCommand(uint8_t *buffer, int bufferCount)
{
if (bufferCount < 6) {
sendErrorMessage(ERROR_INVALID_MESSAGE);
sendErrorMessage(ERROR_INVALID_MESSAGE, "HANDLE_TXRX_BUFFER_COUNT_6");
return;
}
@@ -100,7 +107,7 @@ void handleTransmitReceiveCommand(uint8_t *buffer, int bufferCount)
uint16_t receiveTimeout = (buffer[bufferCount - 2] << 8) | buffer[bufferCount - 1];
if (transmitBufferCount < 1) {
sendErrorMessage(ERROR_INVALID_MESSAGE);
sendErrorMessage(ERROR_INVALID_MESSAGE, "HANDLE_TXRX_TX_BUFFER_COUNT_1");
return;
}
@@ -127,7 +134,7 @@ void handleTransmitReceiveCommand(uint8_t *buffer, int bufferCount)
bufferCount = CoaxTransceiver::transmitReceive(transmitBuffer, transmitBufferCount, receiveBuffer, receiveBufferSize, receiveTimeout);
if (bufferCount < 0) {
sendErrorMessage(100 + ((-1) * bufferCount));
sendErrorMessage(100 + ((-1) * bufferCount), NULL);
return;
}
@@ -142,7 +149,7 @@ void handleTransmitReceiveCommand(uint8_t *buffer, int bufferCount)
void handleMessage(uint8_t *buffer, int bufferCount)
{
if (bufferCount < 1) {
sendErrorMessage(ERROR_INVALID_MESSAGE);
sendErrorMessage(ERROR_INVALID_MESSAGE, "HANDLE_MESSAGE_BUFFER_COUNT_1");
return;
}
@@ -153,21 +160,21 @@ void handleMessage(uint8_t *buffer, int bufferCount)
} else if (command == COMMAND_TRANSMIT_RECEIVE) {
handleTransmitReceiveCommand(buffer + 1, bufferCount - 1);
} else {
sendErrorMessage(ERROR_UNKNOWN_COMMAND);
sendErrorMessage(ERROR_UNKNOWN_COMMAND, NULL);
}
}
void handleFrame(uint8_t *buffer, int bufferCount)
{
if (bufferCount < 4) {
sendErrorMessage(ERROR_INVALID_MESSAGE);
sendErrorMessage(ERROR_INVALID_MESSAGE, "HANDLE_FRAME_BUFFER_COUNT_4");
return;
}
int count = (buffer[0] << 8) | buffer[1];
if (bufferCount - 4 != count) {
sendErrorMessage(ERROR_INVALID_MESSAGE);
sendErrorMessage(ERROR_INVALID_MESSAGE, "HANDLE_FRAME_BUFFER_COUNT_MISMATCH");
return;
}

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 184 KiB

BIN
protocol/character-map.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

1
protocol/data.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

1
protocol/frame.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

255
protocol/protocol.md Normal file
View File

@@ -0,0 +1,255 @@
# IBM 3270 Protocol
This document describes the protocol used to communicate between
[IBM 3270](https://en.m.wikipedia.org/wiki/IBM_3270)
type terminals and IBM 3174 and 3274 controllers.
If you are looking for information on the 3270 data stream (as used by TN3270)
I'd recommend the following resources:
* IBM [3270 Data Stream Programmer's Reference](https://bitsavers.computerhistory.org/pdf/ibm/3270/GA23-0059-4_3270_Data_Stream_Programmers_Reference_Dec88.pdf) (GA23-0059-4)
* Greg Price's [3270 Programming Overview](http://www.prycroft6.com.au/misc/3270.html)
* Tommy Sprinkles' [3270 Data Stream Programming](https://www.tommysprinkle.com/mvs/P3270/start.htm)
For background to the IBM 3270 type terminals and why I am documenting the
protocol, see
_[Building an IBM 3270 terminal controller](https://ajk.me/building-an-ibm-3270-terminal-controller)_.
This document is a work in progress - I appreciate any
[feedback, corrections, or contributions](mailto:projects+3270@ajk.me).
## Devices
The 3270 family consists of the following devices:
* Controller
* Multiplexer
* Display Station - CUT and DFT type terminals
* Printer
## Physical Layer
Devices are connected point-to-point by RG-62 coaxial cable with a
characteristic impedance of 93 Ω. Converters and baluns can convert the coaxial
cable to other media:
* Twisted pair
* Optical fiber
* IBM Cabling System
Differential signaling is used with the coax shield acting as a return.
## Data Link Layer
Data is sent at a bit rate of 2.3587 Mb/s in serial fashion using Machester
encoding. Frames begin and end with unique start and end sequences, both
contain a deliberate violation of the Manchester encoding scheme.
![Diagram showing a 3270 protocol frame containing a single 10-bit word](frame.svg)
A frame contains one or more 10-bit words. Each 10-bit word within a frame
starts with a sync bit and ends with an even parity bit - the parity
calculation includes the sync bit. Words are transmitted most significant bit
(MSB) first.
![Diagram showing two 10-bit words within a 3270 protocol frame](data.svg)
All communication is initiated by the controller when it sends a frame
containing a command word and optional data words. The attached device responds
with a frame containing one or more data words.
### Words
Except for the `POLL` command response, words are either:
* Command - encapsulates a single 8-bit command byte, sent from a controller
to an attached device
* Data - encapsulates a single 8-bit data byte
* Transmission Turnaround (TR/TA) - sent as an acknowledgment of a command
when there is no response data
| Bit | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| ----------------------- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| Command | _C_ | _C_ | _C_ | _C_ | _C_ | _C_ | _C_ | _C_ | `0` | `1` |
| Data | _D_ | _D_ | _D_ | _D_ | _D_ | _D_ | _D_ | _D_ | _P_ | `0` |
| Transmission Turnaround | `0` | `0` | `0` | `0` | `0` | `0` | `0` | `0` | `0` | `0` |
Later devices populate an additional odd parity bit in position 1 (`P`) of
data words; earlier devices leave this `0`.
See the `POLL` command section for a description of the `POLL` response words.
## CUT Terminals
Control Unit Terminal (CUT) type terminals depend on the controller to handle
the 3270 data stream. The controller converts the 3270 data stream commands,
orders, and attributes to commands and data recognized by the terminal.
Distributed Function Terminal (DFT) type terminals, unlike the CUT type, can
handle the 3270 data stream natively.
A CUT type terminal consists of the following components considered part of the
base feature:
* Display
* Keyboard
* Registers
Optional features include an Extended Attribute Buffer (EAB), a selector pen,
and a magnetic stripe reader.
### Display
The display contents, including the status line, are stored in the regen
buffer - named because it is used to regenerate the displayed image.
The size of the buffer depends on the model:
| Model | Rows | Columns | Buffer Size |
|-------|-----:|--------:|------------:|
| 2 | 24 | 80 | 2000 |
| 3 | 32 | 80 | 2640 |
| 4 | 43 | 80 | 3520 |
| 5 | 27 | 132 | 3696 |
_The row count above does not include the status line row, the buffer size does
include the status line._
Each byte in the regen buffer represents a cell on the display - the byte may
be a character or an attribute byte. Attribute bytes control the formatting of
subsequent cells and appear as a blank cell.
The following operations can be performed on the regen buffer:
* Read
* Write - overwrite and insert
* Search
* Clear
The address counter register controls the starting location for these
operations - and also the cursor location. Addresses start at `0`, which
represents the start of the status line. The top-left cell address is equal to
the display width, `80` in the case of 80-column mode.
#### Character Encoding
The character encoding used to represent characters is unique to 3270
terminals; it is not EBCDIC or ASCII. Values below `0xc0` represent displayable
characters.
![Photograph of character map shown on an IBM 3483-V terminal](character-map.jpg)
#### Attribute Bytes
Attribute bytes control the formatting and capabilities of the characters that
follow and store state, such as the modified bit (indicating that the following
field has been modified). The attribute byte formatting wraps-around at the end
of the regen buffer, meaning an attribute byte in the last regen buffer cell
controls the formatting of characters beginning in the top-left cell.
| Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| | `1` | `1` | _P_ | _N_ | _D_ | _D_ | `0` | _M_ |
Bits that control the formatting:
| Bits | Description |
| --------- | -------------------------------------------------- |
| 5 (`P`) | `0` - unprotected<br>`1` - protected |
| 3-2 (`D`) | `00` - normal, not detectable by selector pen<br>`01` - normal, detectable by selector pen<br>`10` - intense, detectable by selector pen<br>`11` - hidden, not detectable by selector pen |
Bits that store state:
| Bits | Description |
| --------- | -------------------------------------------------- |
| 4 (`N`) | `0` - alphanumeric<br>`1` - numeric |
| 0 (`M`) | `0` - unmodified<br>`1` - modified by the operator |
An attribute byte appears as a blank cell on the display. Because the attribute
byte takes up space on the display this imposes limitations on screen design.
#### Status Line
The status line is shown at the bottom of the display. During normal operation,
the status line is managed by the controller, except for the cursor location
area (displayed on the right). The status line begins at address `0`.
The status line does not support formatting. The space in the character
encoding used for attribute bytes (`0xc0` and above) is used to represent
symbols that are unique to the status line.
![Photograph of status line character map shown on an IBM 3483-V terminal](status-line-character-map.jpg)
#### Extended Attribute Buffer (EAB)
The Extended Attribute Buffer (EAB) is an optional feature introduced with the
IBM 3279 terminal that provides more advanced formatting capabilities. It is
implemented as a second buffer that shadows the regen buffer; this buffer only
contains extended attribute bytes that control the formatting of the characters
in the regen buffer - it does not include any characters.
### Keyboard
Keypresses are stored in a FIFO buffer. If there are any keypresses, the scan
code of the first keypress is returned in response to a `POLL` command.
Modifier keys such as _shift_ or _alt_ are treated as regular keypresses but
trigger a keypress on key down and key up (releasing the key). Depending on the
terminal model, the terminal may insert a synthetic release keypress (a unique
scan code) before the modifier scan code when a modifier key is released.
### Registers
Aside from the display and keyboard buffers, the terminal has the following
registers:
* Terminal ID
* Extended ID
* Status
* Address Counter - 16-bit address split into 8-bit high and low registers
* Control
* Secondary Control
* Mask
* Keyboard Clicker State
Registers can be read-only, write-only, or read-write.
### Features
### Commands
| Feature | Command | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | Value |
| ------- | ------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-----:|
| Base | `POLL` | _X_ | _X_ | `0` | `0` | `0` | `0` | `0` | `1` | `0` | `1` | `0x01` |
| Base | `POLL_ACK` | `0` | `0` | `0` | `1` | `0` | `0` | `0` | `1` | `0` | `1` | `0x11` |
| Base | `READ_STATUS` | `0` | `0` | `0` | `0` | `1` | `1` | `0` | `1` | `0` | `1` | `0x0d` |
| Base | `READ_TERMINAL_ID` | `0` | `0` | `0` | `0` | `1` | `0` | `0` | `1` | `0` | `1` | `0x09` |
| Base | `READ_EXTENDED_ID` | `0` | `0` | `0` | `0` | `0` | `1` | `1` | `1` | `0` | `1` | `0x07` |
| Base | `READ_ADDRESS_COUNTER_HI` | `0` | `0` | `0` | `0` | `0` | `1` | `0` | `1` | `0` | `1` | `0x05` |
| Base | `READ_ADDRESS_COUNTER_LO` | `0` | `0` | `0` | `1` | `0` | `1` | `0` | `1` | `0` | `1` | `0x15` |
| Base | `READ_DATA` | `0` | `0` | `0` | `0` | `0` | `0` | `1` | `1` | `0` | `1` | `0x03` |
| Base | `READ_MULTIPLE` | `0` | `0` | `0` | `0` | `1` | `0` | `1` | `1` | `0` | `1` | `0x0b` |
| Base | `RESET` | `0` | `0` | `0` | `0` | `0` | `0` | `1` | `0` | `0` | `1` | `0x02` |
| Base | `LOAD_CONTROL_REGISTER` | `0` | `0` | `0` | `0` | `1` | `0` | `1` | `0` | `0` | `1` | `0x0a` |
| Base | `LOAD_SECONDARY_CONTROL` | `0` | `0` | `0` | `1` | `1` | `0` | `1` | `0` | `0` | `1` | `0x1a` |
| Base | `LOAD_MASK` | `0` | `0` | `0` | `1` | `0` | `1` | `1` | `0` | `0` | `1` | `0x16` |
| Base | `LOAD_ADDRESS_COUNTER_HI` | `0` | `0` | `0` | `0` | `0` | `1` | `0` | `0` | `0` | `1` | `0x04` |
| Base | `LOAD_ADDRESS_COUNTER_LO` | `0` | `0` | `0` | `1` | `0` | `1` | `0` | `0` | `0` | `1` | `0x14` |
| Base | `WRITE_DATA` | `0` | `0` | `0` | `0` | `1` | `1` | `0` | `0` | `0` | `1` | `0x0c` |
| Base | `CLEAR` | `0` | `0` | `0` | `0` | `0` | `1` | `1` | `0` | `0` | `1` | `0x06` |
| Base | `SEARCH_FORWARD` | `0` | `0` | `0` | `1` | `0` | `0` | `0` | `0` | `0` | `1` | `0x10` |
| Base | `SEARCH_BACKWARD` | `0` | `0` | `0` | `1` | `0` | `0` | `1` | `0` | `0` | `1` | `0x12` |
| Base | `INSERT_BYTE` | `0` | `0` | `0` | `0` | `1` | `1` | `1` | `0` | `0` | `1` | `0x0e` |
| Base | `START_OPERATION` | `0` | `0` | `0` | `0` | `1` | `0` | `0` | `0` | `0` | `1` | `0x08` |
| Base | `DIAGNOSTIC_RESET` | `0` | `0` | `0` | `1` | `1` | `1` | `0` | `0` | `0` | `1` | `0x1c` |
_The hexadecimal value above represents the value of the 8-bit command byte; this is
bits 9-2 shifted two bits to the right._
## References
* IBM [3270 Information Display System Introduction](https://bitsavers.computerhistory.org/pdf/ibm/3270/GA27-2739-22_3270_Information_Display_System_Introduction_Oct88.pdf) (GA27-2739-22)
* IBM [3270 Information Display System Component Description](https://bitsavers.computerhistory.org/pdf/ibm/3270/GA27-2749-10_3270_Information_Display_System_Component_Description_Feb80.pdf) (GA27-2749-10)
* CHIPS 82C570
* NS DP8340
* NS DP8341
* NS DP8344

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -2,8 +2,69 @@
Python IBM 3270 coaxial interface library.
## Use
## Usage
You will need to build a [interface](../interface1) and connect it to your computer.
You will need to build an [interface](../interface1) and connect it to your computer.
Install using `pip`:
```
pip install pycoax
```
Assuming your interface is connected to `/dev/ttyACM0` and you have a CUT type terminal connected to the interface, you can do something like this:
```
import time
from serial import Serial
from coax import SerialInterface, poll, poll_ack, load_address_counter_hi, \
load_address_counter_lo, write_data, ReceiveTimeout
with Serial('/dev/ttyACM0', 115200) as serial:
# Give the interface time to wake up...
time.sleep(3)
# Initialize and reset the interface.
interface = SerialInterface(serial)
firmware_version = interface.reset()
print(f'Firmware version is {firmware_version}')
# Wait for a terminal to attach...
poll_response = None
attached = False
while not attached:
try:
poll_response = poll(interface, receive_timeout=1)
if poll_response:
print(poll_response)
poll_ack(interface)
attached = True
except ReceiveTimeout:
print('.')
time.sleep(1)
# Poll the terminal until status is empty.
while poll_response:
poll_response = poll(interface)
if poll_response:
print(poll_response)
poll_ack(interface)
# Move the cursor to top-left cell of a 80 column display.
load_address_counter_hi(interface, 0)
load_address_counter_lo(interface, 80)
# Write a secret message.
write_data(interface, bytes.fromhex('a1 84 00 92 94 91 84 00 93 8e 00 83 91 88 8d 8a 00 98 8e 94 91 00 ae 95 80 8b 93 88 8d 84'))
```
See [examples](examples) for complete examples.

View File

@@ -1 +1 @@
__version__ = '0.3.1'
__version__ = '0.4.1'

View File

@@ -7,6 +7,7 @@ from .protocol import (
PollResponse,
PowerOnResetCompletePollResponse,
KeystrokePollResponse,
TerminalType,
Control,
SecondaryControl,
poll,

View File

@@ -66,7 +66,7 @@ class PowerOnResetCompletePollResponse(PollResponse):
def __init__(self, value):
if not PollResponse.is_power_on_reset_complete(value):
raise ValueError('Invalid POR poll response')
raise ValueError(f'Invalid POR poll response: {value}')
super().__init__(value)
@@ -75,7 +75,7 @@ class KeystrokePollResponse(PollResponse):
def __init__(self, value):
if not PollResponse.is_keystroke(value):
raise ValueError('Invalid keystroke poll response')
raise ValueError(f'Invalid keystroke poll response: {value}')
super().__init__(value)
@@ -97,6 +97,12 @@ class Status:
f'feature_error={self.feature_error}, '
f'operation_complete={self.operation_complete}>')
class TerminalType(Enum):
"""Terminal type."""
CUT = 1
DFT = 2
class TerminalId:
"""Terminal model and keyboard."""
@@ -108,21 +114,28 @@ class TerminalId:
}
def __init__(self, value):
if (value & 0x1) != 0:
raise ValueError('Invalid terminal identifier')
self.value = value
model = (value & 0x0e) >> 1
if (value & 0x1) == 0:
self.type = TerminalType.CUT
if model not in TerminalId._MODEL_MAP:
raise ValueError('Invalid model')
model = (value & 0x0e) >> 1
self.model = TerminalId._MODEL_MAP[model]
self.keyboard = (value & 0xf0) >> 4
if model not in TerminalId._MODEL_MAP:
raise ValueError(f'Invalid model: {model}')
self.model = TerminalId._MODEL_MAP[model]
self.keyboard = (value & 0xf0) >> 4
elif value == 1:
self.type = TerminalType.DFT
self.model = None
self.keyboard = None
else:
raise ValueError(f'Invalid terminal identifier: {value}')
def __repr__(self):
return f'<TerminalId model={self.model}, keyboard={self.keyboard}>'
return (f'<TerminalId type={self.type.name}, model={self.model}, '
f'keyboard={self.keyboard}>')
class Control:
"""Terminal control register."""
@@ -324,7 +337,7 @@ def is_command_word(word):
def unpack_command_word(word):
"""Unpack a 10-bit command word."""
if not is_command_word(word):
raise ProtocolError('Word does not have command bit set')
raise ProtocolError(f'Word does not have command bit set: {word}')
command = (word >> 2) & 0x1f
@@ -343,13 +356,13 @@ def is_data_word(word):
def unpack_data_word(word, check_parity=False):
"""Unpack the data byte from a 10-bit data word."""
if not is_data_word(word):
raise ProtocolError('Word does not have data bit set')
raise ProtocolError(f'Word does not have data bit set: {word}')
byte = (word >> 2) & 0xff
parity = (word >> 1) & 0x1
if check_parity and parity != odd_parity(byte):
raise ProtocolError('Parity error')
raise ProtocolError(f'Parity error: {word}')
return byte
@@ -374,7 +387,8 @@ def _execute_read_command(interface, command_word, response_length=1,
if validate_response_length and len(response) != response_length:
command = unpack_command_word(command_word)
raise ProtocolError(f'Expected {response_length} word {command.name} response')
raise ProtocolError((f'Expected {response_length} word {command.name} '
f'response: {response}'))
return unpack_data_words(response) if unpack else response
@@ -396,7 +410,7 @@ def _execute_write_command(interface, command_word, data=None, **kwargs):
if len(response) != 1:
command = unpack_command_word(command_word)
raise ProtocolError(f'Expected 1 word {command.name} response')
raise ProtocolError(f'Expected 1 word {command.name} response: {response}')
if response[0] != 0:
raise ProtocolError('Expected TR/TA response')
raise ProtocolError(f'Expected TR/TA response: {response}')

View File

@@ -36,7 +36,7 @@ class SerialInterface(Interface):
raise _convert_error(message)
if len(message) != 4:
raise InterfaceError('Invalid reset response')
raise InterfaceError(f'Invalid reset response: {message}')
(major, minor, patch) = struct.unpack('BBB', message[1:])
@@ -109,7 +109,7 @@ class SerialInterface(Interface):
raise InterfaceError('SLIP protocol error')
if len(message) < 4:
raise InterfaceError('Invalid response message')
raise InterfaceError(f'Invalid response message: {message}')
(length,) = struct.unpack('>H', message[:2])
@@ -156,15 +156,26 @@ ERROR_MAP = {
def _convert_error(message):
if message[0] != 0x02:
return InterfaceError('Invalid response')
return InterfaceError(f'Invalid response: {message}')
if len(message) < 2:
return InterfaceError('Invalid error response')
return InterfaceError(f'Invalid error response: {message}')
if message[1] in ERROR_MAP:
return ERROR_MAP[message[1]]
error = ERROR_MAP[message[1]]
return InterfaceError('Unknown error')
# Append description if included.
if len(message) > 2:
description = message[2:].decode('ascii')
if error.args:
error.args = (f'{error.args[0]}: {description}', *error.args[1:])
else:
error.args = (description,)
return error
return InterfaceError(f'Unknown error: {message[1]}')
class SlipSerial(SlipWrapper):
"""sliplib wrapper for pySerial."""

View File

@@ -2,40 +2,62 @@
from common import create_serial, create_interface
from coax import read_address_counter_hi, read_address_counter_lo, load_address_counter_hi, load_address_counter_lo, write_data
from coax import Control, read_address_counter_hi, read_address_counter_lo, load_address_counter_hi, load_address_counter_lo, write_data, load_control_register
DIGIT_MAP = [0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85]
with create_serial() as serial:
interface = create_interface(serial)
print('LOAD_ADDRESS_COUNTER_HI...')
load_control_register(interface, Control(cursor_inhibit=True))
load_address_counter_hi(interface, 0)
print('LOAD_ADDRESS_COUNTER_LO...')
load_address_counter_lo(interface, 80)
print('WRITE_DATA...')
buffer = b'\x00' * 3
buffer = b'\x00' * 4
# Header Row
for lo in range(16):
buffer += bytes([DIGIT_MAP[lo]]) + (b'\x00' * 3)
buffer += bytes([0x2f, DIGIT_MAP[lo]]) + (b'\x00' * 2)
buffer += b'\x00' * 13
buffer += b'\x00' * 12
# Rows
for hi in range(16):
buffer += bytes([DIGIT_MAP[hi]]) + (b'\x00' * 2)
buffer += bytes([DIGIT_MAP[hi], 0x2f]) + (b'\x00' * 2)
for lo in range(16):
buffer += bytes([(hi << 4) | lo]) + b'\x32\xc0\x00'
if hi < 12:
buffer += bytes([0x00, (hi << 4) | lo, 0xc0, 0x00])
else:
buffer += bytes([(hi << 4) | lo, 0x32, 0xc0, 0x00])
buffer += b'\x00' * 13
buffer += b'\x00' * 12
buffer += b'\x00' * 560
write_data(interface, buffer)
# Status Line
load_address_counter_hi(interface, 7)
load_address_counter_lo(interface, 48)
buffer = b''
for hi in range(12, 16):
buffer += bytes([DIGIT_MAP[hi]]) + (b'\x00' * 15)
buffer += b'\x00' * 16
for hi in range(12, 16):
for lo in range(0, 16):
buffer += bytes([DIGIT_MAP[lo]])
write_data(interface, buffer)
load_address_counter_hi(interface, 0)
load_address_counter_lo(interface, 0)
buffer = bytes(range(0xc0, 0xff + 1))
write_data(interface, buffer)

View File

@@ -1,15 +1,16 @@
import sys
import time
import os
from serial import Serial
sys.path.append('..')
from coax import SerialInterface, poll, poll_ack
SERIAL_PORT = '/dev/ttyACM0'
DEFAULT_SERIAL_PORT = '/dev/ttyACM0'
def create_serial():
port = SERIAL_PORT
port = os.environ.get('COAX_PORT', DEFAULT_SERIAL_PORT)
print(f'Opening {port}...')

View File

@@ -1,2 +1,2 @@
pyserial==3.4
sliplib==0.3.0
sliplib==0.5.0

View File

@@ -21,7 +21,7 @@ setup(
author='Andrew Kay',
author_email='projects@ajk.me',
packages=['coax'],
install_requires=['pyserial==3.4', 'sliplib==0.3.0'],
install_requires=['pyserial==3.4', 'sliplib==0.5.0'],
long_description=LONG_DESCRIPTION,
long_description_content_type='text/markdown',
classifiers=[

View File

@@ -4,7 +4,7 @@ from unittest.mock import Mock
import context
from coax import PollResponse, KeystrokePollResponse, ProtocolError
from coax.protocol import Command, Status, TerminalId, Control, SecondaryControl, pack_command_word, unpack_command_word, pack_data_word, unpack_data_word, pack_data_words, unpack_data_words, _execute_read_command, _execute_write_command
from coax.protocol import Command, Status, TerminalType, TerminalId, Control, SecondaryControl, pack_command_word, unpack_command_word, pack_data_word, unpack_data_word, pack_data_words, unpack_data_words, _execute_read_command, _execute_write_command
class PollResponseTestCase(unittest.TestCase):
def test_is_power_on_reset_complete(self):
@@ -35,31 +35,40 @@ class StatusTestCase(unittest.TestCase):
self.assertTrue(status.operation_complete)
class TerminalIdTestCase(unittest.TestCase):
def test_model_2(self):
def test_cut_model_2(self):
terminal_id = TerminalId(0b00000100)
self.assertEqual(terminal_id.type, TerminalType.CUT)
self.assertEqual(terminal_id.model, 2)
def test_model_3(self):
def test_cut_model_3(self):
terminal_id = TerminalId(0b00000110)
self.assertEqual(terminal_id.type, TerminalType.CUT)
self.assertEqual(terminal_id.model, 3)
def test_model_4(self):
def test_cut_model_4(self):
terminal_id = TerminalId(0b00001110)
self.assertEqual(terminal_id.type, TerminalType.CUT)
self.assertEqual(terminal_id.model, 4)
def test_model_5(self):
def test_cut_model_5(self):
terminal_id = TerminalId(0b00001100)
self.assertEqual(terminal_id.type, TerminalType.CUT)
self.assertEqual(terminal_id.model, 5)
def test_dft(self):
terminal_id = TerminalId(0b00000001)
self.assertEqual(terminal_id.type, TerminalType.DFT)
def test_invalid_identifier(self):
with self.assertRaisesRegex(ValueError, 'Invalid terminal identifier'):
terminal_id = TerminalId(0b00000001)
terminal_id = TerminalId(0b00000011)
def test_invalid_model(self):
def test_invalid_cut_model(self):
with self.assertRaisesRegex(ValueError, 'Invalid model'):
terminal_id = TerminalId(0b00000000)

View File

@@ -53,6 +53,14 @@ class SerialInterfaceResetTestCase(unittest.TestCase):
with self.assertRaisesRegex(InterfaceError, 'Invalid request message'):
self.interface.reset()
def test_error_with_description_is_handled_correctly(self):
# Arrange
self.interface._read_message = Mock(return_value=bytes.fromhex('02 01 45 72 72 6f 72 20 64 65 73 63 72 69 70 74 69 6f 6e'))
# Act and assert
with self.assertRaisesRegex(InterfaceError, 'Invalid request message: Error description'):
self.interface.reset()
# TODO...
class SerialInterfaceReadMessageTestCase(unittest.TestCase):