Compare commits

...

3 Commits
0.4.1 ... 0.5.0

Author SHA1 Message Date
83051e10c3 Added two utility scripts for working with PMAC motors 2025-01-09 13:26:12 +01:00
08d76d7953 bump sinqMotor version to 0.6.3 2025-01-08 16:04:58 +01:00
1f02001502 Various small improvements to documentation, error messages etc.
Also moved the initialization of some parameters to sinqMotor
2024-12-23 09:32:00 +01:00
11 changed files with 565 additions and 88 deletions

View File

@ -11,7 +11,7 @@ REQUIRED+=asynMotor
REQUIRED+=sinqMotor
# Specify the version of sinqMotor we want to build against
sinqMotor_VERSION=0.6.0
sinqMotor_VERSION=0.6.3
# These headers allow to depend on this library for derived drivers.
HEADERS += src/pmacv3Axis.h

View File

@ -8,6 +8,11 @@ This is a driver for the pmacV3 motion controller with the SINQ communication pr
This driver is a standard sinqMotor-derived driver and does not need any specific configuration. For the general configuration, please see https://git.psi.ch/sinq-epics-modules/sinqmotor/-/blob/main/README.md.
The folder "utils" contains utility scripts for working with pmac motor controllers. To read their manual, run the scripts without any arguments.
- writeRead.py: Allows sending commands to and receiving commands from a pmac controller over an ethernet connection.
- analyzeTcpDump.py: Parse the TCP communication between an IOC and a MCU and format it into a dictionary. "demo.py" shows how this data can be easily visualized for analysis.
## Developer guide
### Usage in IOC shell

View File

@ -50,17 +50,8 @@ pmacv3Axis::pmacv3Axis(pmacv3Controller *pC, int axisNo)
exit(-1);
}
status = pC_->setIntegerParam(axisNo_, pC_->motorEnableRBV_, 0);
if (status != asynSuccess) {
asynPrint(
pC_->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nFATAL ERROR (setting a parameter value failed "
"with %s)\n. Terminating IOC",
__PRETTY_FUNCTION__, __LINE__, pC_->stringifyAsynStatus(status));
exit(-1);
}
status = pC_->setDoubleParam(axisNo_, pC_->motorPosition_, 0.0);
// pmacv3 motors can always be disabled
status = pC_->setIntegerParam(axisNo_, pC_->motorCanDisable_, 1);
if (status != asynSuccess) {
asynPrint(
pC_->pasynUserSelf, ASYN_TRACE_ERROR,
@ -84,21 +75,6 @@ pmacv3Axis::~pmacv3Axis(void) {
Read the configuration at the first poll
*/
asynStatus pmacv3Axis::atFirstPoll() {
asynStatus status = asynSuccess;
// pmacv3 motors can always be disabled
status = pC_->setIntegerParam(axisNo_, pC_->motorCanDisable_, 1);
if (status != asynSuccess) {
return pC_->paramLibAccessFailed(status, "motorCanDisable_",
__PRETTY_FUNCTION__, __LINE__);
}
return readConfig();
}
/*
Read the configuration from the motor control unit and the parameter library.
*/
asynStatus pmacv3Axis::readConfig() {
// Local variable declaration
asynStatus status = asynSuccess;
@ -122,7 +98,10 @@ asynStatus pmacv3Axis::readConfig() {
__PRETTY_FUNCTION__, __LINE__);
}
// Software limits and current position
/*
Read out the axis status, the current position, current and maximum speed,
acceleration and the air cushion delay.
*/
snprintf(command, sizeof(command),
"P%2.2d00 Q%2.2d10 Q%2.2d03 Q%2.2d04 Q%2.2d06 P%2.2d22", axisNo_,
axisNo_, axisNo_, axisNo_, axisNo_, axisNo_);
@ -261,7 +240,7 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
}
// Transform from EPICS to motor coordinates (see comment in
// pmacv3Axis::readConfig())
// pmacv3Axis::atFirstPoll)
previousPosition = previousPosition * motorRecResolution;
// Query the axis status
@ -284,13 +263,13 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
The axis limits are set as: ({[]})
where [] are the positive and negative limits set in EPICS/NICOS, {} are the
software limits set on the MCU and () are the hardware limit switches. In
other words, the EPICS/NICOS limits must be stricter than the software
other words, the EPICS/NICOS limits should be stricter than the software
limits on the MCU which in turn should be stricter than the hardware limit
switches. For example, if the hardware limit switches are at [-10, 10], the
software limits could be at [-9, 9] and the EPICS / NICOS limits could be at
[-8, 8]. Therefore, we cannot use the software limits read from the MCU
directly, but need to shrink them a bit. In this case, we're shrinking them
by 0.1 mm or 0.1 degree (depending on the axis type) on both sides.
by limitsOffset on both sides.
*/
pl_status =
pC_->getDoubleParam(axisNo_, pC_->motorLimitsOffset_, &limitsOffset);
@ -542,7 +521,7 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
snprintf(command, sizeof(command),
"Maximum allowed following error exceeded (P%2.2d01 = %d). "
"Check if movement range is blocked."
"Check if movement range is blocked. "
"Otherwise please call the support.",
axisNo_, error);
pl_status = setStringParam(pC_->motorMessageText_, command);
@ -669,7 +648,7 @@ asynStatus pmacv3Axis::doPoll(bool *moving) {
}
// Transform from motor to EPICS coordinates (see comment in
// pmacv3Axis::readConfig())
// pmacv3Axis::atFirstPoll())
currentPosition = currentPosition / motorRecResolution;
pl_status = setDoubleParam(pC_->motorPosition_, currentPosition);

View File

@ -106,7 +106,6 @@ class pmacv3Axis : public sinqAxis {
protected:
pmacv3Controller *pC_;
asynStatus readConfig();
bool initial_poll_;
bool waitForHandshake_;
time_t timeAtHandshake_;

View File

@ -1,6 +1,3 @@
// Needed to use strcpy_s from string.h
#define __STDC_WANT_LIB_EXT1__ 1
#include "pmacv3Controller.h"
#include "asynMotorController.h"
#include "asynOctetSyncIO.h"
@ -14,21 +11,21 @@
#include <unistd.h>
/**
* @brief Copy src into dst and replace all carriage returns with spaces
* @brief Copy src into dst and replace all carriage returns with spaces. This
* allows to print *dst with asynPrint.
*
*
* @param dst Buffer for the modified string
* @param src Original string
*/
void adjustResponseForPrint(char *dst, const char *src) {
// Needed to use strcpy_s from string.h
#ifdef __STDC_LIB_EXT1__
strcpy_s(dst, src);
for (size_t i = 0; i < strlen(dst); i++) {
if (dst[i] == '\r') {
dst[i] = '_';
void adjustResponseForPrint(char *dst, const char *src, size_t buf_length) {
for (size_t i = 0; i < buf_length; i++) {
if (src[i] == '\r') {
dst[i] = ' ';
} else {
dst[i] = src[i];
}
}
#endif
}
/**
@ -296,14 +293,14 @@ asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
// Second check: If this fails, give up and propagate the error.
if (numExpectedResponses != numReceivedResponses) {
adjustResponseForPrint(modResponse, response);
adjustResponseForPrint(modResponse, response, MAXBUF_);
asynPrint(this->pasynUserSelf, ASYN_TRACE_ERROR,
"%s => line %d:\nUnexpected response %s (_ are "
"carriage returns) for command %s\n",
"%s => line %d:\nUnexpected response '%s' (carriage "
"returns are replaced with spaces) for command %s\n",
__PRETTY_FUNCTION__, __LINE__, modResponse, command);
snprintf(drvMessageText, sizeof(drvMessageText),
"Received unexpected response %s (_ are "
"carriage returns) for command %s. "
"Received unexpected response '%s' (carriage returns "
"are replaced with spaces) for command %s. "
"Please call the support",
modResponse, command);
pl_status = setStringParam(motorMessageText_, drvMessageText);
@ -345,29 +342,6 @@ asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
}
}
if (status != asynSuccess) {
// Check if the axis already is in an error communication mode. If it is
// not, upstream the error. This is done to avoid "flooding" the user
// with different error messages if more than one error ocurred before
// an error-free communication
pl_status =
getIntegerParam(axisNo, motorStatusProblem_, &motorStatusProblem);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusProblem_",
__PRETTY_FUNCTION__, __LINE__);
}
if (motorStatusProblem == 0) {
pl_status =
axis->setStringParam(this->motorMessageText_, drvMessageText);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
}
}
// Log the overall status (communication successfull or not)
if (status == asynSuccess) {
@ -377,21 +351,43 @@ asynStatus pmacv3Controller::writeRead(int axisNo, const char *command,
__PRETTY_FUNCTION__, __LINE__, modResponse);
pl_status = axis->setIntegerParam(this->motorStatusCommsError_, 0);
} else {
if (status == asynSuccess) {
asynPrint(
lowLevelPortUser_, ASYN_TRACE_ERROR,
"%s => line %d:\nCommunication failed for command %s (%s)\n",
__PRETTY_FUNCTION__, __LINE__, fullCommand,
stringifyAsynStatus(status));
pl_status = axis->setIntegerParam(this->motorStatusCommsError_, 1);
}
asynPrint(lowLevelPortUser_, ASYN_TRACE_ERROR,
"%s => line %d:\nCommunication failed for command %s (%s)\n",
__PRETTY_FUNCTION__, __LINE__, fullCommand,
stringifyAsynStatus(status));
// Check if the axis already is in an error communication mode. If it is
// not, upstream the error. This is done to avoid "flooding" the user
// with different error messages if more than one error ocurred before
// an error-free communication
pl_status =
getIntegerParam(axisNo, motorStatusProblem_, &motorStatusProblem);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusCommsError_",
return paramLibAccessFailed(pl_status, "motorStatusProblem_",
__PRETTY_FUNCTION__, __LINE__);
}
if (motorStatusProblem == 0) {
pl_status = axis->setStringParam(motorMessageText_, drvMessageText);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorMessageText_",
__PRETTY_FUNCTION__, __LINE__);
}
pl_status = axis->setIntegerParam(motorStatusProblem_, 1);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusProblem",
__PRETTY_FUNCTION__, __LINE__);
}
pl_status = axis->setIntegerParam(motorStatusProblem_, 1);
if (pl_status != asynSuccess) {
return paramLibAccessFailed(pl_status, "motorStatusCommsError_",
__PRETTY_FUNCTION__, __LINE__);
}
}
}
return asynSuccess;
return status;
}
asynStatus pmacv3Controller::writeInt32(asynUser *pasynUser, epicsInt32 value) {
@ -409,7 +405,7 @@ asynStatus pmacv3Controller::writeInt32(asynUser *pasynUser, epicsInt32 value) {
if (function == rereadEncoderPosition_) {
return axis->rereadEncoder();
} else if (function == readConfig_) {
return axis->readConfig();
return axis->atFirstPoll();
} else {
return sinqController::writeInt32(pasynUser, value);
}
@ -429,7 +425,7 @@ asynStatus pmacv3Controller::errMsgCouldNotParseResponse(
const char *command, const char *response, int axisNo,
const char *functionName, int lineNumber) {
char modifiedResponse[MAXBUF_] = {0};
adjustResponseForPrint(modifiedResponse, response);
adjustResponseForPrint(modifiedResponse, response, MAXBUF_);
return sinqController::errMsgCouldNotParseResponse(
command, modifiedResponse, axisNo, functionName, lineNumber);
}
@ -445,7 +441,7 @@ C wrapper for the controller constructor. Please refer to the pmacv3Controller
constructor documentation.
*/
asynStatus pmacv3CreateController(const char *portName,
const char *lowLevelPortName, int numAxes,
const char *ipPortConfigName, int numAxes,
double movingPollPeriod,
double idlePollPeriod, double comTimeout) {
/*
@ -460,7 +456,7 @@ asynStatus pmacv3CreateController(const char *portName,
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#pragma GCC diagnostic ignored "-Wunused-variable"
pmacv3Controller *pController =
new pmacv3Controller(portName, lowLevelPortName, numAxes,
new pmacv3Controller(portName, ipPortConfigName, numAxes,
movingPollPeriod, idlePollPeriod, comTimeout);
return asynSuccess;

View File

@ -0,0 +1,200 @@
#! venv/bin/python3
"""
This script can be used to format the communication between an EPICS IOC and a
PMAC MCU into JSON files. It does this by parsing PCAP files created by tcpdump
and rearranging the information in a more structured manner.
To read the manual, simply run this script without any arguments.
Stefan Mathis, January 2025
"""
from scapy.all import *
import json
import re
import codecs
import os
from datetime import datetime
def parse(fileAndPath):
try:
scapyCap = PcapReader(fileAndPath)
except:
print(f"Could not read file {fileAndPath} as PCAP file")
requests = []
sent = []
jsonDict = dict()
lastLayer = None
for packet in scapyCap:
layer = packet.getlayer(Raw)
if layer is None:
continue
# Skip the package if it is not a command or a response. A command ends
# with a carriage return (x0d), a response ends with ACKNOWLEDGE (x06)
last = layer.load[-1]
if last == 6:
isResponse = True
elif last == 13:
isResponse = False
else:
continue
# Store the info by the IP adress of the MCU
if isResponse:
ip = packet[IP].src
else:
ip = packet[IP].dst
if ip not in jsonDict:
jsonDict[ip] = dict()
# Convert to ASCII
ascii = layer.load.decode("unicode_escape")
# Convert the time to a float
time = float(packet.time)
if isResponse:
# A response is always a number followed by a carriage return
responses = re.findall("-?\d+\.\d+\r|-?\d+\r", ascii)
# Check if the number of responses matches the number of requests
valid = len(responses) == len(requests)
# Pair up the request-response pairs
for (request, response) in zip(requests, responses):
if request not in jsonDict[ip]:
jsonDict[ip][request] = dict()
if "." in response:
value = float(response)
else:
value = int(response)
lastLayer = lastPacket.getlayer(Raw)
lastTime = int(lastPacket.time)
data = {
'command': {
'hex': [format(value, '02x') for value in lastLayer.load],
'ascii': lastLayer.load.decode("unicode_escape"),
'timestamp': lastTime,
'timeiso': str(datetime.fromtimestamp(lastTime).isoformat()),
},
'response': {
'hex': [format(value, '02x') for value in layer.load],
'ascii': ascii,
'value': value,
'timestamp': time,
'timeiso': str(datetime.fromtimestamp(time).isoformat()),
'valid': valid
}
}
jsonDict[ip][request][time] = data
else:
requests.clear()
sent.clear()
# Store the packet for use in the response iteration
lastPacket = packet
# Parse the ASCII text via regex. A PMAC command usually has the
# format LDDDD(=<Number>), where L is a capital letter, the first
# two digits D are the axis number and the last two digits together
# with the letter form the command.
# Separate the commands into sent data (e.g. setting a position)
# and data requests (e.g. reading the axis status). Sent data always
# has an equal sign.
for command in re.findall("[A-Z]\d+=-?\d+|[A-Z]\d+", ascii):
if "=" in command:
sent.append(command)
else:
requests.append(command)
# Store the sent. The requests yfd stored together with the responses later.
for command in sent:
splitted = command.split("=")
key = splitted[0]
key = key + "="
if key not in jsonDict[ip]:
jsonDict[ip][key] = dict()
if "." in splitted[1]:
value = float(splitted[1])
else:
value = int(splitted[1])
data = {
'command': {
'hex': [format(value, '02x') for value in layer.load],
'ascii': ascii,
'value': value,
'timestamp': time,
'timeiso': str(datetime.fromtimestamp(time).isoformat()),
},
}
jsonDict[ip][key][time] = data
return jsonDict
if __name__ == "__main__":
isInstalled = False
try:
from scapy.all import *
isInstalled = True
except ImportError:
print("This script needs the Scapy package to run. In order to install a "
"suitable virtual environment, use the 'makevenv' script.")
if isInstalled:
from sys import argv
if len(argv) < 2:
print("""
This script can be used to format the communication between an EPICS IOC and a
PMAC MCU into JSON files. It does this by parsing PCAP files created by tcpdump
and rearranging the information in a more structured manner.
After a successfull parse run, the resulting JSON data looks like this:
<IP Adress MCU1>
<Request command type> (e.g. Q0100 to request the position of axis 1)
<Event timestamp>
Command
<Raw ASCII string>
<Actual command> (e.g. P0100)
<Timestamp in Epoch>
Response
<Raw ASCII string>
<Actual response (e.g. -3)
<Timestamp in Epoch>
<Set command type> (e.g. Q0100= to set the position of axis 1)
<Event timestamp>
<Raw ASCII string>
<Actual command (e.g. P0100)
<Set value>
<Timestamp in Epoch>
<IP Adress MCU2>
""")
else:
for fileAndPath in argv[1:]:
jsonDict = parse(fileAndPath)
# Save the dict into a JSON
fileName = os.path.basename(fileAndPath)
jsonfileAndPath = f"{fileName}.json"
with open(jsonfileAndPath, 'w') as fp:
json.dump(jsonDict, fp, indent=4)
print(f"Stored parse result of {fileAndPath} in {fileName}")

Binary file not shown.

83
utils/analyzeTcpDump/demo.py Executable file
View File

@ -0,0 +1,83 @@
#! demovenv/bin/python3
"""
This demo script shows how the "parse" function of "analyzeTcpDump.py" can be
used to easily visualize data from a PCAP file created by the tcpdump tool /
wireshark. A suitable virtual environment can be created with the "makedemovenv"
script.
Stefan Mathis, January 2025
"""
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
from analyzeTcpDump import parse
if __name__ == "__main__":
data = parse("demo.pcap")
plt.figure(figsize=(12, 6))
# Plot the position of axis 5 over time
# Actual position
position_valid = []
dates_valid = []
position_all = []
dates_all = []
for (timestamp, item) in data["172.28.101.24"]["Q0510"].items():
date = datetime.fromtimestamp(timestamp)
value = item["response"]["value"]
dates_all.append(date)
position_all.append(value)
if item["response"]["valid"]:
dates_valid.append(date)
position_valid.append(value)
else:
command = item["command"]["ascii"]
response = item["response"]["ascii"]
# Replace non-renderable characters
command = command.replace("\0", "\\x00")
command = command.replace("\r", "\\x0d")
command = command.replace("\x12", "\\x12")
response = response.replace("\r", "\\x0d")
response = response.replace("\06", "\\x06")
# Shift the text a bit to the right
plt.text(date, value, f"Command: {command}\nResponse: {response}", horizontalalignment="right", verticalalignment="top")
# Target position
position_target = [position_valid[0]]
dates_target = [dates_valid[0]]
for (timestamp, item) in data["172.28.101.24"]["Q0501="].items():
date = datetime.fromtimestamp(timestamp)
value = item["command"]["value"]
dates_target.append(date)
position_target.append(position_target[-1])
dates_target.append(date)
position_target.append(value)
dates_target.append(dates_valid[-1])
position_target.append(position_target[-1])
plt.plot(dates_target, position_target, "k--", label="Target position")
plt.plot(dates_all, position_all, "r", label="All responses")
plt.plot(dates_valid, position_valid, "b", label="Valid responses")
plt.xlabel("Time (ISO 8601)")
plt.ylabel("Axis position in degree")
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%dT%H:%M:%S"))
plt.xticks(rotation=45)
plt.grid(True)
plt.legend(loc="lower left")
plt.title("Position of axis 5")
plt.tight_layout()
plt.show()

View File

@ -0,0 +1,22 @@
#!/bin/bash
#-------------------------------------------------------------------------
# Script which installs a virtual environment for PCAP file parsing
#
# Stefan Mathis, September 2024
#-------------------------------------------------------------------------
# Remove any previous testing environment
if [ -d "demovenv" ]; then
rm -r demovenv
fi
/usr/bin/python3.11 -m venv demovenv
source demovenv/bin/activate
pip install --upgrade pip
pip install "scapy>=2.5,<3.0"
pip install matplotlib
# Exit the virtual environment
exit

21
utils/analyzeTcpDump/makevenv Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
#-------------------------------------------------------------------------
# Script which installs a virtual environment for PCAP file parsing
#
# Stefan Mathis, September 2024
#-------------------------------------------------------------------------
# Remove any previous testing environment
if [ -d "venv" ]; then
rm -r venv
fi
/usr/bin/python3.11 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install "scapy>=2.5,<3.0"
# Exit the virtual environment
exit

172
utils/writeRead.py Normal file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
This script allows direct interaction with a pmac-Controller over an ethernet connection.
To read the manual, simply run this script without any arguments.
Stefan Mathis, December 2024
"""
import struct
import socket
import curses
def packPmacCommand(command):
# 0x40 = VR_DOWNLOAD
# 0xBF = VR_PMAC_GETRESPONSE
buf = struct.pack('BBHHH',0x40,0xBF,0,0,socket.htons(len(command)))
buf = buf + bytes(command,'utf-8')
return buf
def readPmacReply(input):
msg = bytearray()
expectAck = True
while True:
b = input.recv(1)
bint = int.from_bytes(b,byteorder='little')
if bint == 2 or bint == 7: #STX or BELL
expectAck = False
continue
if expectAck and bint == 6: # ACK
return bytes(msg)
else:
if bint == 13 and not expectAck: # CR
return bytes(msg)
else:
msg.append(bint)
if __name__ == "__main__":
from sys import argv
try:
addr = argv[1].split(':')
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((addr[0],int(addr[1])))
if len(argv) == 3:
buf = packPmacCommand(argv[2])
s.send(buf)
reply = readPmacReply(s)
print(reply.decode('utf-8') + '\n')
else:
try:
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
stdscr.keypad(True)
stdscr.scrollok(True)
stdscr.addstr(">> ")
stdscr.refresh()
history = [""]
ptr = len(history) - 1
while True:
c = stdscr.getch()
if c == curses.KEY_RIGHT:
(y, x) = stdscr.getyx()
if x < len(history[ptr]) + 3:
stdscr.move(y, x+1)
stdscr.refresh()
elif c == curses.KEY_LEFT:
(y, x) = stdscr.getyx()
if x > 3:
stdscr.move(y, x-1)
stdscr.refresh()
elif c == curses.KEY_UP:
if ptr > 0:
ptr -= 1
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
elif c == curses.KEY_DOWN:
if ptr < len(history) - 1:
ptr += 1
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
elif c == curses.KEY_ENTER or c == ord('\n') or c == ord('\r'):
if history[ptr] == 'quit':
break
# because of arrow keys move back to the end of the line
(y, x) = stdscr.getyx()
stdscr.move(y, 3+len(history[ptr]))
if history[ptr]:
buf = packPmacCommand(history[ptr])
s.send(buf)
reply = readPmacReply(s)
stdscr.addstr("\n" + reply.decode('utf-8')[0:-1])
if ptr == len(history) - 1 and history[ptr] != "":
history += [""]
else:
history[-1] = ""
ptr = len(history) - 1
stdscr.addstr("\n>> ")
stdscr.refresh()
else:
if ptr < len(history) - 1: # Modifying previous input
if len(history[-1]) == 0:
history[-1] = history[ptr]
ptr = len(history) - 1
else:
history += [history[ptr]]
ptr = len(history) - 1
if c == curses.KEY_BACKSPACE:
if len(history[ptr]) == 0:
continue
(y, x) = stdscr.getyx()
history[ptr] = history[ptr][0:x-4] + history[ptr][x-3:]
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
stdscr.move(y, x-1)
stdscr.refresh()
else:
(y, x) = stdscr.getyx()
history[ptr] = history[ptr][0:x-3] + chr(c) + history[ptr][x-3:]
stdscr.addch("\r")
stdscr.clrtoeol()
stdscr.addstr(">> " + history[ptr])
stdscr.move(y, x+1)
stdscr.refresh()
finally:
# to quit
curses.nocbreak()
stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
print("""
Invalid Arguments
Option 1: Single Command
------------------------
Usage: writeRead.py pmachost:port command
This then returns the response for command.
Option 2: CLI Mode
------------------
Usage: writeRead.py pmachost:port
You can then type in a command, hit enter, and the response will see
the reponse, before being prompted to again enter a command. Type
'quit' to close prompt.
""")