Skip to content

Commit

Permalink
Add machine states for run-time testing.
Browse files Browse the repository at this point in the history
  • Loading branch information
t0mpr1c3 committed Aug 6, 2020
1 parent 6ca81b6 commit 5488b0e
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 86 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 1.0.0 / Unreleased

* #413 Engine: Add run-time test methods and change required API version to 6
* GUI: Fix image alignment and placement of limit lines in graphical scene
* #395 GUI: Check for new release on startup
* #393 Documentation: Add forthcoming features to CHANGELOG
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ The Python module dependencies can be found in *requirements.txt*.
To be able to communicate with your Arduino, it might be necessary to add the
rights for USB communication by adding your user to some groups.

sudo usermod -a -G tty [userName]
sudo usermod -a -G dialout [userName]
sudo usermod -aG tty [userName]
sudo usermod -aG dialout [userName]

To install the development version you can checkout the git repository.

Expand Down
31 changes: 17 additions & 14 deletions src/main/python/ayab/engine/communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,22 @@ class MessageToken(Enum):
reqLine = 0x82
cnfLine = 0x42
indState = 0x84
slipFrameEnd = 0xc0


class AyabCommunication(object):
"""Class Handling the serial communication protocol."""
def __init__(self, serial=None):
"""Creates an AyabCommunication object,
with an optional serial-like object."""
"""Create an AyabCommunication object,
with an optional serial communication object."""
logging.basicConfig(level=logging.DEBUG)
self.__logger = logging.getLogger(type(self).__name__)
self.__ser = serial
self.__driver = sliplib.Driver()
self.__rx_msg_list = list()

def __del__(self):
"""Handles on delete behaviour closing serial port object."""
"""Handle behaviour on deletion by closing the serial port connection."""
self.close_serial()

def is_open(self):
Expand All @@ -69,7 +70,7 @@ def is_open(self):
return False

def open_serial(self, portname=None):
"""Opens serial port communication with a portName."""
"""Open serial port communication."""
if not self.__ser:
self.__portname = portname
try:
Expand All @@ -83,7 +84,7 @@ def open_serial(self, portname=None):
return True

def close_serial(self):
"""Closes serial port."""
"""Close the serial port."""
if self.__ser is not None and self.__ser.is_open is True:
try:
self.__ser.close()
Expand All @@ -96,17 +97,19 @@ def close_serial(self):

# NB this method must be the same for all API versions
def req_info(self):
"""Sends a request for information to controller."""
"""Send a request for information to the device."""
data = self.__driver.send(bytes([MessageToken.reqInfo.value]))
self.__ser.write(data)

def req_test_API6(self):
""""""
data = self.__driver.send(bytes([MessageToken.reqTest.value]))
def req_test_API6(self, machine_val):
"""Send a request for testing to the device."""
data = self.__driver.send(
bytes([MessageToken.reqTest.value, machine_val]))
self.__ser.write(data)

def req_start_API6(self, machine_val, start_needle, stop_needle, continuous_reporting):
"""Sends a start message to the controller."""
def req_start_API6(self, machine_val, start_needle, stop_needle,
continuous_reporting):
"""Send a start message to the device."""
data = bytearray()
data.append(MessageToken.reqStart.value)
data.append(machine_val)
Expand All @@ -120,9 +123,9 @@ def req_start_API6(self, machine_val, start_needle, stop_needle, continuous_repo
self.__ser.write(data)

def cnf_line_API6(self, line_number, color, flags, line_data):
"""Sends a line of data via the serial port.
"""Send a line of data via the serial port.
Sends a line of data to the serial port, all arguments are mandatory.
Send a line of data to the serial port. All arguments are mandatory.
The data sent here is parsed by the Arduino controller which sets the
knitting needles accordingly.
Expand All @@ -146,7 +149,7 @@ def cnf_line_API6(self, line_number, color, flags, line_data):
self.__ser.write(data)

def update_API6(self):
"""Reads data from serial and tries to parse as SLIP packet."""
"""Read data from serial and parse as SLIP packet."""
if self.__ser:
data = self.__ser.read(1000)
if len(data) > 0:
Expand Down
12 changes: 8 additions & 4 deletions src/main/python/ayab/engine/communication_mockup.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,17 @@ def open_serial(self, portname=None) -> bool:
def req_info(self) -> None:
cnfInfo = bytearray([MessageToken.cnfInfo.value, 0x5, 0xFF, 0xFF])
self.__rx_msg_list.append(cnfInfo)
indState = bytearray([MessageToken.indState.value, 0x1, 0xFF, 0xFF, 0xFF, 0xFF, 0x1, 0x7F])
indState = bytearray([
MessageToken.indState.value, 0x1, 0xFF, 0xFF, 0xFF, 0xFF, 0x1, 0x7F
])
self.__rx_msg_list.append(indState)

def req_test_API6(self) -> None:
def req_test_API6(self, machine_val) -> None:
cnfTest = bytearray([MessageToken.cnfTest.value, 0x1])
self.__rx_msg_list.append(cnfTest)

def req_start_API6(self, machine_val, start_needle, stop_needle, continuous_reporting) -> None:
def req_start_API6(self, machine_val, start_needle, stop_needle,
continuous_reporting) -> None:
self.__is_started = True
cnfStart = bytearray([MessageToken.cnfStart.value, 0x1])
self.__rx_msg_list.append(cnfStart)
Expand All @@ -79,7 +82,8 @@ def cnf_line_API6(self, line_number, color, flags, line_data) -> bool:

def update_API6(self) -> tuple:
if self.__is_open and self.__is_started:
reqLine = bytearray([MessageToken.reqLine.value, self.__line_count])
reqLine = bytearray(
[MessageToken.reqLine.value, self.__line_count])
self.__line_count += 1
self.__line_count %= 256
self.__rx_msg_list.append(reqLine)
Expand Down
11 changes: 5 additions & 6 deletions src/main/python/ayab/engine/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def cnf_line_API6(self, line_number):
self.logger.error("Requested line number out of range")
return True # stop knitting
# else
# TODO some better algorithm for block wrapping
# TODO: some better algorithm for block wrapping
if self.former_request == self.BLOCK_LENGTH - 1 and line_number == 0:
# wrap to next block of lines
self.line_block += 1
Expand All @@ -128,7 +128,7 @@ def cnf_line_API6(self, line_number):
self.com.cnf_line_API6(requested_line, color, flag, bits.tobytes())

# screen output
# TODO tidy up this code
# TODO: tidy up this code
msg = str(self.line_block) + " " + str(line_number) + " reqLine: " + \
str(requested_line) + " pat_row: " + str(self.pat_row)
if blank_line:
Expand Down Expand Up @@ -187,12 +187,11 @@ def select_needles_API6(self, color, row_index, blank_line):
self.__last_needle = last_needle
return bits

def knit(self, pattern, options):
def operate(self, pattern, options, operation, API_version=6):
"""Finite State Machine governing serial communication"""
method = "_knit_" + self.state.name.lower()
method = "_API" + str(API_version) + "_" + self.state.name.lower()
if not hasattr(KnitStateMachine, method):
# NONE, FINISHED
return KnitOutput.NONE
dispatch = getattr(KnitStateMachine, method)
result = dispatch(self, pattern, options)
result = dispatch(self, pattern, options, operation)
return result
12 changes: 9 additions & 3 deletions src/main/python/ayab/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ayab import utils
from ayab.observable import Observable
from .control import KnitControl
from .state import KnitOperation
from .pattern import Pattern
from .options import OptionsTab, Alignment, NeedleColor
from .status import Status, StatusTab
Expand Down Expand Up @@ -75,6 +76,7 @@ def setup_ui(self):
# activate UI elements
self.__activate_ui()


# def __activate_status_tab(self):
# self.ui.tab_widget.setTabEnabled(1, True)
# self.ui.tab_widget.setCurrentIndex(1)
Expand Down Expand Up @@ -123,7 +125,8 @@ def knit_config(self, image):

# TODO: detect if previous conf had the same
# image to avoid re-generating.
self.__pattern = Pattern(image, self.config.machine, self.config.num_colors)
self.__pattern = Pattern(image, self.config.machine,
self.config.num_colors)

# validate configuration options
valid, msg = self.validate()
Expand All @@ -133,7 +136,9 @@ def knit_config(self, image):

# update pattern
if self.config.start_needle and self.config.stop_needle:
self.__pattern.set_knit_needles(self.config.start_needle, self.config.stop_needle, self.config.machine)
self.__pattern.set_knit_needles(self.config.start_needle,
self.config.stop_needle,
self.config.machine)
self.__pattern.alignment = self.config.alignment

# update progress bar
Expand Down Expand Up @@ -165,7 +170,8 @@ def knit(self):

# FIXME pattern and config are only used by KnitControl.knit()
# in the KnitState.SETUP step and do not need to be sent otherwise.
result = self.__control.knit(self.__pattern, self.config)
result = self.__control.operate(self.__pattern, self.config,
KnitOperation.KNIT)
self.__feedback.handle(result)
self.__status_handler()
if self.__canceled or result is KnitOutput.FINISHED:
Expand Down
88 changes: 64 additions & 24 deletions src/main/python/ayab/engine/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,20 @@
from .output import KnitOutput


# NB no test states
class KnitOperation(Enum):
KNIT = auto()
TEST = auto()


class KnitState(Enum):
# NONE = auto()
SETUP = auto()
INIT = auto()
WAIT_FOR_INIT = auto()
START = auto()
OPERATE = auto()
# FINISHED = auto()
REQUEST_START = auto()
CONFIRM_START = auto()
RUN_KNIT = auto()
REQUEST_TEST = auto()
CONFIRM_TEST = auto()
RUN_TEST = auto()


class KnitStateMachine(object):
Expand All @@ -46,7 +51,7 @@ class KnitStateMachine(object):
@author Tom Price
@date June 2020
"""
def _knit_setup(control, pattern, options):
def _API6_setup(control, pattern, options, operation):
control.logger.debug("KnitState SETUP")
control.former_request = 0
control.line_block = 0
Expand All @@ -58,7 +63,8 @@ def _knit_setup(control, pattern, options):
control.mode = options.mode
control.inf_repeat = options.inf_repeat
control.len_pat_expanded = control.pat_height * control.num_colors
control.passes_per_row = control.mode.row_multiplier(control.num_colors)
control.passes_per_row = control.mode.row_multiplier(
control.num_colors)
control.reset_status()
if not control.func_selector():
return KnitOutput.ERROR_INVALID_SETTINGS
Expand All @@ -77,48 +83,49 @@ def _knit_setup(control, pattern, options):
control.state = KnitState.INIT
return KnitOutput.NONE

# NB this communication sequence must be the same for every API version
def _knit_init(control, pattern, options):
def _API6_init(control, pattern, options, operation):
control.logger.debug("KnitState INIT")
rcvMsg, rcvParam = control.check_serial()
if rcvMsg == MessageToken.cnfInfo:
if rcvParam >= control.FIRST_SUPPORTED_API_VERSION:
control.api_version = rcvParam
control.state = KnitState.WAIT_FOR_INIT
if operation == KnitOperation.TEST:
control.state = KnitState.REQUEST_TEST
else:
control.state = KnitState.REQUEST_START
return KnitOutput.WAIT_FOR_INIT
else:
control.logger.error("Wrong API version: " + str(rcvParam) +
", expected >= " + str(control.FIRST_SUPPORTED_API_VERSION))
", expected >= " +
str(control.FIRST_SUPPORTED_API_VERSION))
return KnitOutput.ERROR_WRONG_API
# else
control.com.req_info()
return KnitOutput.CONNECTING_TO_MACHINE

# TODO: polymorphic dispatch on control.api_version to ensure backwards compatibility
def _knit_wait_for_init(control, pattern, options):
control.logger.debug("KnitState WAIT_FOR_INIT")
def _API6_request_start(control, pattern, options, operation):
control.logger.debug("KnitState REQUEST_START")
rcvMsg, rcvParam = control.check_serial()
if rcvMsg == MessageToken.indState:
if rcvParam == 1:
control.com.req_start_API6(options.machine.value,
control.pattern.knit_start_needle,
control.pattern.knit_stop_needle,
options.continuous_reporting)
control.state = KnitState.START
control.state = KnitState.CONFIRM_START
else:
# any value of rcvParam other than 1 is some kind of error code
control.logger.debug("Init failed")
control.logger.debug("Knit init failed")
# TODO: more output to describe error
# fallthrough
return KnitOutput.NONE

# TODO: polymorphic dispatch on control.api_version to ensure backwards compatibility
def _knit_start(control, pattern, options):
control.logger.debug("KnitState START")
def _API6_confirm_start(control, pattern, options, operation):
control.logger.debug("KnitState CONFIRM_START")
rcvMsg, rcvParam = control.check_serial()
if rcvMsg == MessageToken.cnfStart:
if rcvParam == 1:
control.state = KnitState.OPERATE
control.state = KnitState.RUN_KNIT
return KnitOutput.PLEASE_KNIT
else:
# any value of rcvParam other than 1 is some kind of error code
Expand All @@ -128,9 +135,8 @@ def _knit_start(control, pattern, options):
# fallthrough
return KnitOutput.NONE

# TODO: polymorphic dispatch on control.api_version to ensure backwards compatibility
def _knit_operate(control, pattern, options):
control.logger.debug("KnitState OPERATE")
def _API6_run_knit(control, pattern, options, operation):
control.logger.debug("KnitState RUN_KNIT")
rcvMsg, rcvParam = control.check_serial()
if rcvMsg == MessageToken.reqLine:
pattern_finished = control.cnf_line_API6(rcvParam)
Expand All @@ -141,3 +147,37 @@ def _knit_operate(control, pattern, options):
return KnitOutput.NEXT_LINE
# fallthrough
return KnitOutput.NONE

def _API6_request_test(control, pattern, options, operation):
control.logger.debug("KnitState REQUEST_TEST")
rcvMsg, rcvParam = control.check_serial()
if rcvMsg == MessageToken.indState:
if rcvParam == 1:
control.com.req_test_API6(options.machine.value)
control.state = KnitState.CONFIRM_TEST
else:
# any value of rcvParam other than 1 is some kind of error code
control.logger.debug("Test init failed")
# TODO: more output to describe error
# fallthrough
return KnitOutput.NONE

def _API6_confirm_test(control, pattern, options, operation):
control.logger.debug("KnitState CONFIRM_TEST")
rcvMsg, rcvParam = control.check_serial()
if rcvMsg == MessageToken.cnfTest:
if rcvParam == 1:
control.state = KnitState.RUN_TEST
return KnitOutput.NONE
else:
# any value of rcvParam other than 1 is some kind of error code
control.logger.error("Device not ready")
# TODO: more output to describe error
return KnitOutput.DEVICE_NOT_READY
# fallthrough
return KnitOutput.NONE

def _API6_run_test(control, pattern, options, operation):
control.logger.debug("KnitState RUN_TEST")
# TODO: open serial monitor
return KnitOutput.NONE
Loading

0 comments on commit 5488b0e

Please sign in to comment.