diff --git a/.github/workflows/build_pycoax.yml b/.github/workflows/build_pycoax.yml
index 6fdd252..3e9053c 100644
--- a/.github/workflows/build_pycoax.yml
+++ b/.github/workflows/build_pycoax.yml
@@ -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
diff --git a/README.md b/README.md
index 7bf7644..dad40ff 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/interface1/README.md b/interface1/README.md
index 48e850d..95ca69a 100644
--- a/interface1/README.md
+++ b/interface1/README.md
@@ -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
+```
diff --git a/interface1/firmware/platformio.ini b/interface1/firmware/platformio.ini
index a6dd2ab..95622f9 100644
--- a/interface1/firmware/platformio.ini
+++ b/interface1/firmware/platformio.ini
@@ -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
diff --git a/interface1/firmware/src/main.cpp b/interface1/firmware/src/main.cpp
index 260db2a..5d025b7 100644
--- a/interface1/firmware/src/main.cpp
+++ b/interface1/firmware/src/main.cpp
@@ -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;
}
diff --git a/interface1/hardware/schematic.svg b/interface1/hardware/schematic.svg
deleted file mode 100644
index 37312a5..0000000
--- a/interface1/hardware/schematic.svg
+++ /dev/null
@@ -1,4866 +0,0 @@
-
-
diff --git a/protocol/character-map.jpg b/protocol/character-map.jpg
new file mode 100644
index 0000000..b2eab60
Binary files /dev/null and b/protocol/character-map.jpg differ
diff --git a/protocol/data.svg b/protocol/data.svg
new file mode 100644
index 0000000..2c535af
--- /dev/null
+++ b/protocol/data.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/protocol/frame.svg b/protocol/frame.svg
new file mode 100644
index 0000000..a4d51f4
--- /dev/null
+++ b/protocol/frame.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/protocol/protocol.md b/protocol/protocol.md
new file mode 100644
index 0000000..5b0679d
--- /dev/null
+++ b/protocol/protocol.md
@@ -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
`1` - protected |
+| 3-2 (`D`) | `00` - normal, not detectable by selector pen
`01` - normal, detectable by selector pen
`10` - intense, detectable by selector pen
`11` - hidden, not detectable by selector pen |
+
+Bits that store state:
+
+| Bits | Description |
+| --------- | -------------------------------------------------- |
+| 4 (`N`) | `0` - alphanumeric
`1` - numeric |
+| 0 (`M`) | `0` - unmodified
`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
diff --git a/protocol/status-line-character-map.jpg b/protocol/status-line-character-map.jpg
new file mode 100644
index 0000000..e843bbd
Binary files /dev/null and b/protocol/status-line-character-map.jpg differ
diff --git a/pycoax/README.md b/pycoax/README.md
index 77e3649..877259c 100644
--- a/pycoax/README.md
+++ b/pycoax/README.md
@@ -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.
diff --git a/pycoax/coax/__about__.py b/pycoax/coax/__about__.py
index e1424ed..f0ede3d 100644
--- a/pycoax/coax/__about__.py
+++ b/pycoax/coax/__about__.py
@@ -1 +1 @@
-__version__ = '0.3.1'
+__version__ = '0.4.1'
diff --git a/pycoax/coax/__init__.py b/pycoax/coax/__init__.py
index efc9602..2fe13ba 100644
--- a/pycoax/coax/__init__.py
+++ b/pycoax/coax/__init__.py
@@ -7,6 +7,7 @@ from .protocol import (
PollResponse,
PowerOnResetCompletePollResponse,
KeystrokePollResponse,
+ TerminalType,
Control,
SecondaryControl,
poll,
diff --git a/pycoax/coax/protocol.py b/pycoax/coax/protocol.py
index f9e1317..b817ac0 100644
--- a/pycoax/coax/protocol.py
+++ b/pycoax/coax/protocol.py
@@ -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''
+ return (f'')
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}')
diff --git a/pycoax/coax/serial_interface.py b/pycoax/coax/serial_interface.py
index 43602be..e6fb501 100644
--- a/pycoax/coax/serial_interface.py
+++ b/pycoax/coax/serial_interface.py
@@ -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."""
diff --git a/pycoax/examples/20_char_map.py b/pycoax/examples/20_char_map.py
index 83bde17..cf435ca 100755
--- a/pycoax/examples/20_char_map.py
+++ b/pycoax/examples/20_char_map.py
@@ -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)
diff --git a/pycoax/examples/common.py b/pycoax/examples/common.py
index c4b6f40..68d4f6f 100644
--- a/pycoax/examples/common.py
+++ b/pycoax/examples/common.py
@@ -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}...')
diff --git a/pycoax/requirements.txt b/pycoax/requirements.txt
index 0409260..c343611 100644
--- a/pycoax/requirements.txt
+++ b/pycoax/requirements.txt
@@ -1,2 +1,2 @@
pyserial==3.4
-sliplib==0.3.0
+sliplib==0.5.0
diff --git a/pycoax/setup.py b/pycoax/setup.py
index 5b4b9fc..1cc3438 100644
--- a/pycoax/setup.py
+++ b/pycoax/setup.py
@@ -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=[
diff --git a/pycoax/tests/test_protocol.py b/pycoax/tests/test_protocol.py
index ee2088a..2cfe4c1 100644
--- a/pycoax/tests/test_protocol.py
+++ b/pycoax/tests/test_protocol.py
@@ -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)
diff --git a/pycoax/tests/test_serial_interface.py b/pycoax/tests/test_serial_interface.py
index 56ded69..dbb7a87 100644
--- a/pycoax/tests/test_serial_interface.py
+++ b/pycoax/tests/test_serial_interface.py
@@ -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):