mirror of
https://github.com/lowobservable/coax.git
synced 2026-02-26 17:13:24 +00:00
Merge branch 'master' into i2
This commit is contained in:
2
.github/workflows/build_pycoax.yml
vendored
2
.github/workflows/build_pycoax.yml
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
A serial attached Arduino interface using the National Semiconductor DP8340 and DP8341.
|
||||
|
||||
## Schematic
|
||||
## Hardware
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
protocol/character-map.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
1
protocol/data.svg
Normal file
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
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
255
protocol/protocol.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||

|
||||
|
||||
#### 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
|
||||
BIN
protocol/status-line-character-map.jpg
Normal file
BIN
protocol/status-line-character-map.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.3.1'
|
||||
__version__ = '0.4.1'
|
||||
|
||||
@@ -7,6 +7,7 @@ from .protocol import (
|
||||
PollResponse,
|
||||
PowerOnResetCompletePollResponse,
|
||||
KeystrokePollResponse,
|
||||
TerminalType,
|
||||
Control,
|
||||
SecondaryControl,
|
||||
poll,
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}...')
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pyserial==3.4
|
||||
sliplib==0.3.0
|
||||
sliplib==0.5.0
|
||||
|
||||
@@ -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=[
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user