From 8eb60784c61e702ce64e0a5160809a8bf5b9310a Mon Sep 17 00:00:00 2001 From: drunsinn Date: Sun, 15 Jan 2023 16:14:05 +0100 Subject: [PATCH] release v1.0 (#43) --- .codespellrc | 5 + .pylintrc | 78 +- LICENSE | 2 +- README.md | 203 +-- docs/conf.py | 13 + docs/doc_requirements.txt | 1 + docs/faq.rst | 6 +- docs/lsv2-toolbox.rst | 4 +- docs/package.rst | 35 +- docs/protocol.rst | 2 +- pyLSV2/__init__.py | 9 +- pyLSV2/client.py | 2190 +++++++++++++++------------- pyLSV2/const.py | 42 +- pyLSV2/dat_cls.py | 926 ++++++++++++ pyLSV2/err.py | 19 + pyLSV2/low_level_com.py | 286 +++- pyLSV2/misc.py | 373 +++-- pyLSV2/table_reader.py | 620 +++++--- pyLSV2/translate_messages.py | 192 +-- pytest.ini | 5 + scripts/check_for_LSV2_commands.py | 66 - scripts/lsv2_demo.py | 315 ++-- scripts/lsv2cmd.py | 90 +- scripts/ssh_tunnel_demo.py | 16 +- scripts/tab2csv.py | 69 + scripts/table_reader_demo.py | 31 - setup.py | 24 +- tests/conftest.py | 2 +- tests/test_connection.py | 46 +- tests/test_file_functions.py | 105 +- tests/test_keys.py | 12 +- tests/test_machine_parameters.py | 12 +- tests/test_misc.py | 2 +- tests/test_plc_read.py | 75 +- tests/test_read_info.py | 47 +- tests/test_transfer.py | 20 +- tests/test_translation.py | 10 +- 37 files changed, 3848 insertions(+), 2105 deletions(-) create mode 100644 .codespellrc create mode 100644 pyLSV2/dat_cls.py create mode 100644 pyLSV2/err.py create mode 100644 pytest.ini delete mode 100644 scripts/check_for_LSV2_commands.py create mode 100644 scripts/tab2csv.py delete mode 100644 scripts/table_reader_demo.py diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..44f905f --- /dev/null +++ b/.codespellrc @@ -0,0 +1,5 @@ +[codespell] +skip = *.po,*.ts,./docs/_build,./docs/_static,./.git +count = +quiet-level = 3 +ignore-words-list = spindel diff --git a/.pylintrc b/.pylintrc index ef3266e..653cb92 100644 --- a/.pylintrc +++ b/.pylintrc @@ -60,17 +60,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, +disable=raw-checker-failed, bad-inline-option, locally-disabled, file-ignored, @@ -78,67 +68,6 @@ disable=print-statement, useless-suppression, deprecated-pragma, use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, consider-using-f-string # Enable the message, report, category or checker with the given id(s). You can @@ -251,7 +180,8 @@ good-names=i, k, ex, Run, - _ + _, + pyLSV2 # Good variable names regexes, separated by a comma. If names match any regex, # they will always be accepted @@ -318,7 +248,7 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 diff --git a/LICENSE b/LICENSE index 6285737..b354fff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 - 2022 drunsinn +Copyright (c) 2020 - 2023 drunsinn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b3847ae..d7f9507 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ as collect information about said files. Over time more and more functions where added which support gathering information from the control. - Most of this library is based on the work of tfischer73 and his Eclipse plugin found at [his GitHub page](https://github.com/tfischer73/Eclipse-Plugin-Heidenhain). Since there is no free documentation beside the plugin, some parts are based purely on reverse engineering and might therefore be not correct. + Most of this library is based on the work of tfischer73 and his Eclipse plugin found at [his GitHub page](https://github.com/tfischer73/Eclipse-Plugin-Heidenhain). Since there is no free documentation beside the plugin, some parts are based purely on reverse engineering and might therefore be not correct. - As long as no encrypted communication is necessary, no additional librarys are necessary. + As long as no encrypted communication is necessary and you are using Python 3.5 or newer, no additional librarys are necessary. Please consider the dangers of using this library on a production machine! This library is by no means complete and could damage the control or cause injuries! Everything beyond simple file manipulation is blocked by a lockout parameter. Use at your own risk! @@ -40,105 +40,70 @@ SOFTWARE. ## News and Releases -check [the github release page](https://github.com/drunsinn/pyLSV2/releases) for information on the latest updates + check [the github release page](https://github.com/drunsinn/pyLSV2/releases) for information on the latest updates ## Contributors -In chronological order: -- tfischer73 -- drunsinn -- WouterElfrink -- kekec14 -- Michal Navrátil -- PandaRoux8 -- sazima -- manusolve -- NeeXoo -- Baptou88 - -## Compatibility -Since there are a lot of different software versions and machine configurations out there -it is hard to say if this library is compatible with all of them. Most testing has been done -on programming stations but also with real hardware. Here is a list of versions that have -been tested: - -### Programming Stations -| Control | Software | -|-------------|----------------| -| TNC640 | 340595 08 SP1 | -| TNC640 | 340595 10 SP2 | -| TNC640 | 340595 11 SP1 | -| TNC640 | 340595 11 SP4 | -| iTNC530 | 606425 04 SP20 | -| iTNC530 | 340494 08 SP2 | -| CNCpilot640 | 1230521 03 SP1 | - -### Machines -| Control | Software | -|-------------|----------------| -| TNC620 | 817605 04 SP1 | -| TNC640 | 340595 08 SP1 | -| iTNC530 | 340480 14 SP4 | -| iTNC530 | 606420 02 SP14 | -| iTNC530 | 606420 02 SP3 | - -If you have tested it on one of your machines with a different software version, please let us know! - -Take a look at [protocol.rst](https://github.com/drunsinn/pyLSV2/blob/master/docs/package.rst) for a more in depth explanation on the detials of LSV2. + In chronological order: + - tfischer73 + - drunsinn + - WouterElfrink + - kekec14 + - Michal Navrátil + - PandaRoux8 + - sazima + - manusolve + - NeeXoo + - Baptou88 ## Usage -See [lsv2_demo.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/lsv2_demo.py) for a demonstration of some of the functions. - -Notice that the definitionns of constant values will be moved from pyLSV2.LSV2 to pyLSV2 directly! - -### Basic example + See [lsv2_demo.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/lsv2_demo.py) for a demonstration of some of the functions. + + Since the whole protocol isn't documented there can always be problems with certain corner cases. Especially during file transfer a lot of stuff can go wrong. + In case the control doesn't accept a command it returns an error. Some of these errors are checked internally but not everything is covered as of now. It is therefore + best to check the last error with `con.last_error()`. Every error consists of type and a status code. The enum `LSV2StatusCode` contains all known status codes. + +### Notes for upgrade to v1.xx +Notice: The change from 0.xx to 1.xx brought some major incompatible changes in regards to the API: + - raise the minimal required python version to 3.4 (for [IronPython 3.4](https://ironpython.net/)), future releases (1.1.x) will target 3.7 or higher! + - change of function names and parameters to better reflect their use + - change of return types from dict to data classes to reduce the dependency on magic strings +These changes where made intentionally to make further development easier. See the demo script for an overview of the new API. + +#### exemplary overview of the changes from v0.x to v1.x +| Functionality | Version 0.x | Version 1.x | +|------------------------------|-------------------------------------------------------|---------------------------------------------------------| +| read nc software version | `con.get_versions()["NC_Version"]` | `con.versions.nc_sw` | +| check if control is a iTNC | `con.is_itnc()` | `con.version.is_itnc()` | +| get execution status via | `con.get_execution_status()` returns `int` value | `con.execution_status()` returns enum `ExecState` | +| read override values via | `con.get_override_info()` returns `dict` or `False` | `con.override_info()` returns `OverrideState` or `None` | +| read axes position via | `con.get_axes_location()` returns `dict` or `False` | `con.axes_location()` returns `dict` or `None` | +| move a file on the control | `con.move_local_file()` | `con.move_file()` | + +### Basic example without context manager ``` import pyLSV2 - con = pyLSV2.LSV2('192.168.56.101') + con = pyLSV2.LSV2("192.168.56.101") con.connect() - print(con.get_versions()) + print(con.versions.control) con.disconnect() ``` -### Reading Information from the control +### Basic example with context manager ``` - import logging import pyLSV2 - - logging.basicConfig(level=logging.DEBUG) - con = pyLSV2.LSV2('192.168.56.101', safe_mode=False) - con.connect() - print(con.get_program_status_text(con.get_program_status())) - print(con.get_execution_status_text(con.get_execution_status())) - print(con.get_axes_location()) - con.disconnect() -``` - -### File transfer -``` - import logging - import pyLSV2 - - logging.basicConfig(level=logging.DEBUG) - con = pyLSV2.LSV2('192.168.56.101', safe_mode=True) - con.connect() - - con.send_file(local_path='./test.H', remote_path='TNC:/', - override_file=True, binary_mode=True) - - con.recive_file(local_path='./', remote_path='TNC:/nc_prog/$mdi.h', - override_file=True, binary_mode=True) - - con.disconnect() + with pyLSV2.LSV2("192.168.56.101") as con: + ... con.connect() + ... print(con.versions.control) ``` ### Accessing PLC data To read values from the PLC memory you need to know the memory area/type and the memory address. There are two ways to read these values. #### Reading via memory address - The following command reads 15 marker (bits) starting at address + The following command reads 15 marker (bits) starting at address 32. This returns a list with 15 boolean values. ``` - con.read_plc_memory(address=32, mem_type=pyLSV2.PLC_MEM_TYPE_MARKER, count=15) + con.read_plc_memory(32, pyLSV2.MemoryType.MARKER, 15) ``` See [lsv2_demo.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/lsv2_demo.py) for more examples. @@ -169,23 +134,79 @@ Notice that the definitionns of constant values will be moved from pyLSV2.LSV2 t See [lsv2_demo.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/lsv2_demo.py) for more examples. ### SSH Tunnel -Newer controls allow the use of ssh to encrypt the communication via LSV2. See scripts/ssh_tunnel_demo.py for an example on how to use the python library [sshtunnel](https://github.com/pahaz/sshtunnel) to achieve a secure connection. + Newer controls allow the use of ssh to encrypt the communication via LSV2. + See [ssh_tunnel_demo.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/ssh_tunnel_demo.py) for an example on + how to use the python library [sshtunnel](https://github.com/pahaz/sshtunnel) to achieve a secure connection. + +## Compatibility + Since there are a lot of different software versions and machine configurations out there + it is hard to say if this library is compatible with all of them. Most testing has been done + on programming stations but also with real hardware. Here is a list of versions that have + been tested: + +### Programming Stations +| Control | Software | +|-------------|----------------| +| TNC640 | 340595 08 SP1 | +| TNC640 | 340595 10 SP2 | +| TNC640 | 340595 11 SP1 | +| TNC640 | 340595 11 SP4 | +| iTNC530 | 606425 04 SP20 | +| iTNC530 | 340494 08 SP2 | +| CNCpilot640 | 1230521 03 SP1 | + +### Machines +| Control | Software | +|-------------|----------------| +| TNC620 | 817605 04 SP1 | +| TNC640 | 340595 08 SP1 | +| iTNC530 | 340480 14 SP4 | +| iTNC530 | 606420 02 SP14 | +| iTNC530 | 606420 02 SP3 | + + If you have tested it on one of your machines with a different software version, please let us know! # Tables -Included in this library is also fuctionality to work with Tables used by different NC Controls. This includes for example TNC controls as well as Anilam 6000i CNC. As these controls and there software versions use different table formats, it is also possible to dreive the format form an existing table and export the format to a json file. + Included in this library is also functionality to work with Tables used by different NC Controls. This includes for example TNC controls as well as Anilam 6000i CNC. As these controls and there software versions use different table formats, it is also possible to dreive the format form an existing table and export the format to a json file. - See [table_reader_demo.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/table_reader_demo.py) for a demonstration on how to read a table and convert it to a different format. + See [tab2csv.py](https://github.com/drunsinn/pyLSV2/blob/master/scripts/tab2csv.py) for a demonstration on how to read a table and convert it to a csv file. + + This script can also be used as a command line tool +``` + usage: tab2csv.py [-h] [--decimal_char DECIMAL_CHAR] [-d | -v] source + + command line script parsing table files + + positional arguments: + source table file to parse + + options: + -h, --help show this help message and exit + --decimal_char DECIMAL_CHAR + override local decimal char + -d, --debug enable log level DEBUG + -v, --verbose enable log level INFO +``` # Testing - To run the test you either need a machine or a programming station. The controls has to be on and the - PLC program has to be running. You can add the IP-Address and timeout as a parameter + To run the test you either need a machine or a programming station. The control has to be on and the + PLC program has to be running. The IP address and timeout are set via command line parameters. ``` pytest --address=192.168.56.103 --timeout=5 ``` -# Resources -https://www.inventcom.net/support/heidenhain/read-tnc-plc-data - -https://www.inventcom.net/s1/_pdf/Heidenhain_TNC_Machine_Data.pdf +# Minimum required Python version + The minimum required python version was checked with [vermin](https://github.com/netromdk/vermin). +``` + vermin --no-parse-comments . +``` + The results indicate that pyLSV2 should work with python 3.5 and even with 3.4 if you install + the packported modules argparse, enum and typing. While argpares is only used in the demo script + the other two are necessary. Therefore it should be possible to use pyLSV2 with the curretn version + of [IronPython](https://ironpython.net/) if you install these two modules. -https://de.industryarena.com/heidenhain/forum +# Resources + - [protocol.rst](https://github.com/drunsinn/pyLSV2/blob/master/docs/package.rst) + - https://www.inventcom.net/support/heidenhain/read-tnc-plc-data + - https://www.inventcom.net/s1/_pdf/Heidenhain_TNC_Machine_Data.pdf + - https://www.yumpu.com/en/document/read/18882603/-f-heidenhain diff --git a/docs/conf.py b/docs/conf.py index 0216132..21cfe11 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,6 +34,7 @@ "sphinx.ext.autodoc", "sphinx.ext.githubpages", "myst_parser", + "sphinx_autodoc_typehints" ] # Add any paths that contain templates here, relative to this directory. @@ -44,6 +45,8 @@ # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +autoclass_content = "both" + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -55,3 +58,13 @@ # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] + + +# -- Options for sphinx_autodoc_typehints ------------------------------------ +typehints_fully_qualified = False +always_document_param_types = True +typehints_document_rtype = True +typehints_use_rtype = True +typehints_defaults = 'comma' +simplify_optional_unions = True +typehints_formatter = None diff --git a/docs/doc_requirements.txt b/docs/doc_requirements.txt index 7cd0eed..76e4ef3 100644 --- a/docs/doc_requirements.txt +++ b/docs/doc_requirements.txt @@ -1 +1,2 @@ myst-parser>=0.14.0 +sphinx_autodoc_typehints>=1.18.3 diff --git a/docs/faq.rst b/docs/faq.rst index 8bbe4da..0b7f4e5 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -7,16 +7,16 @@ Check [inventcom.net](https://www.inventcom.net/s1/_pdf/Heidenhain_TNC_Machine_D some steps on how to check some common problems. For iTNC 530: -- open the MOD dialog and enable the external access by toggeling the soft key "External access" +- open the MOD dialog and enable the external access by toggling the soft key "External access" - restart control For TNC 320, 620, 640 - open the MOD dialog - navigate to Machine settings/External access -- enable the external access by toggeling the soft key "External access" +- enable the external access by toggling the soft key "External access" - restart control -If the soft key "External access" is not visible you have to change some patameters that require +If the soft key "External access" is not visible you have to change some parameters that require the PLC password: - iTNC: uncommented the line "REMOTE.LOCKSOFTKEYVISIBLE = YES" in PLC:/oem.sys - TNC 320, 620, 640: add the parameter "denyAllConnections" in "CfgAccessControl" and/or set the value to False diff --git a/docs/lsv2-toolbox.rst b/docs/lsv2-toolbox.rst index bb43780..7644786 100644 --- a/docs/lsv2-toolbox.rst +++ b/docs/lsv2-toolbox.rst @@ -70,12 +70,12 @@ Important constants Overview of commands ordered by function ---------------------------------------- +---------------------------------------- TBD Overview of commands ordered by login ------------------------------------- +------------------------------------- +-------------+----------------------+-----------------------------------+ | Login | Command | Description | +=============+======================+===================================+ diff --git a/docs/package.rst b/docs/package.rst index be0bcf8..ed3aaf6 100644 --- a/docs/package.rst +++ b/docs/package.rst @@ -14,9 +14,40 @@ pyLSV2 Base Table reader ------------ -.. automodule:: pyLSV2.table_reader +.. autoclass:: pyLSV2.NCTable + :members: + +Dataclasses +----------- + +.. autoclass:: pyLSV2.dat_cls.VersionInfo + :members: + +.. autoclass:: pyLSV2.dat_cls.SystemParameters + :members: + +.. autoclass:: pyLSV2.dat_cls.ToolInformation + :members: + +.. autoclass:: pyLSV2.dat_cls.OverrideState + :members: + +.. autoclass:: pyLSV2.dat_cls.NCErrorMessage + :members: + +.. autoclass:: pyLSV2.dat_cls.StackState + :members: + +.. autoclass:: pyLSV2.dat_cls.FileEntry + :members: + +.. autoclass:: pyLSV2.dat_cls.DirectoryEntry + :members: + +.. autoclass:: pyLSV2.dat_cls.DriveEntry + :members: -.. autoclass:: pyLSV2.TableReader +.. autoclass:: pyLSV2.dat_cls.LSV2Error :members: Constants diff --git a/docs/protocol.rst b/docs/protocol.rst index 5827594..e417dc6 100644 --- a/docs/protocol.rst +++ b/docs/protocol.rst @@ -103,4 +103,4 @@ Afterwards a regular file copy takes place. File transfer ------------- Transfer of files can happen in binary or ASCII mode. To enable binary mode, add 0x01 after the filename. In TNCremo you can find a list of file types for which binary mode is recommended. -The functions :py:meth:`pyLSV2.LSV2.recive_file` and :py:meth:`pyLSV2.LSV2.send_file` can be configured with the parameter binary_mode. +The functions :py:meth:`pyLSV2.LSV2.recive_file` and :py:meth:`pyLSV2.LSV2.send_file` can be configured with the parameter `binary_mode`. diff --git a/pyLSV2/__init__.py b/pyLSV2/__init__.py index ace68d5..57c2397 100644 --- a/pyLSV2/__init__.py +++ b/pyLSV2/__init__.py @@ -1,9 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """A pure Python3 implementation of the LSV2 protocol""" -from .client import LSV2 +from .client import * from .const import * +from .dat_cls import * from .table_reader import * from .translate_messages import * +from .err import * -__version__ = "0.7.7" +__version__ = "1.0.0" +__author__ = "drunsinn" +__license__ = "MIT" +__email__ = "dr.unsinn@googlemail.com" diff --git a/pyLSV2/client.py b/pyLSV2/client.py index f9afbef..0d50b35 100644 --- a/pyLSV2/client.py +++ b/pyLSV2/client.py @@ -1,98 +1,79 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""This library is an attempt to implement the LSV2 communication protocol used by certain - CNC controls. - Please consider the dangers of using this library on a production machine! This library is - by no means complete and could damage the control or cause injuries! Everything beyond simple - file manipulation is blocked by a lockout parameter. Use at your own risk! +""" +This library is an attempt to implement the LSV2 communication protocol used by certain +CNC controls. +Please consider the dangers of using this library on a production machine! This library is +by no means complete and could damage the control or cause injuries! Everything beyond simple +file manipulation is blocked by a lockout parameter. Use at your own risk! """ import logging import math +import pathlib import re -import os import struct from datetime import datetime -from pathlib import Path - -from .const import ( - ControlType, - DriveName, - Login, - MemoryType, - LSV2Err, - CMD, - RSP, - ParCCC, - ParRVR, - ParRRI, - ParRDR, - BIN_FILES, - PATH_SEP, - MODE_BINARY, -) - -from .low_level_com import LLLSV2Com -from .misc import ( - decode_directory_info, - decode_error_message, - decode_file_system_info, - decode_override_information, - decode_system_parameters, - decode_tool_information, - is_file_binary, -) -from .translate_messages import ( - get_error_text, - get_execution_status_text, - get_program_status_text, +from types import TracebackType +from typing import List, Union, Optional, Type, Dict + +from . import const as lc +from . import dat_cls as ld +from . import misc as lm +from . import translate_messages as lt +from .low_level_com import LSV2TCP +from .err import ( + LSV2DataException, + LSV2InputException, + LSV2ProtocolException, + LSV2StateException, ) class LSV2: - """Implementation of the LSV2 protocol used to communicate with certain CNC controls""" + """implements functions for communicationg with CNC controls via LSV2""" def __init__( - self, hostname, port=0, timeout=15.0, safe_mode=True, locale_path=None + self, + hostname: str, + port: int = 0, + timeout: float = 15.0, + safe_mode: bool = True, ): - """init object variables and create socket""" - logging.getLogger(__name__).addHandler(logging.NullHandler()) + """ + Implementation of the LSV2 protocol used to communicate with certain CNC controls - self._llcom = LLLSV2Com(hostname, port, timeout) + :param hostname: hostname or IP address of the controls + :param port: port number to connect to + :param timeout: number of seconds waited for a response + :param safe_mode: switch to disable safety functions that might influence the control + """ + self._logger = logging.getLogger("LSV2 Client") - self._buffer_size = LLLSV2Com.DEFAULT_BUFFER_SIZE - self._active_logins = list() + self._llcom = LSV2TCP(hostname, port, timeout) - if safe_mode: - logging.info( - "safe mode is active, login and system commands are restricted" - ) - self._known_logins = (Login.INSPECT, Login.FILETRANSFER, Login.MONITOR) - self._known_sys_cmd = ( - ParCCC.SET_BUF1024, - ParCCC.SET_BUF512, - ParCCC.SET_BUF2048, - ParCCC.SET_BUF3072, - ParCCC.SET_BUF4096, - ParCCC.SECURE_FILE_SEND, - ParCCC.SCREENDUMP, - ) - else: - logging.info( - "safe mode is off, login and system commands are not restricted. Use with caution!" - ) - self._known_logins = [e.value for e in Login] - self._known_sys_cmd = [e.value for e in ParCCC] + self._active_logins = [] + + self.switch_safe_mode(safe_mode) + + self._versions = ld.VersionInfo() + self._sys_par = ld.SystemParameters() - self._versions = None - self._sys_par = None self._secure_file_send = False - self._control_type = ControlType.UNKNOWN - self._last_error_code = None - if locale_path is None: - self._locale_path = os.path.join(os.path.dirname(__file__), "locales") - else: - self._locale_path = locale_path + @property + def versions(self) -> ld.VersionInfo: + """version information of the connected control""" + return self._versions + + @property + def parameters(self) -> ld.SystemParameters: + """system parameters of the connected control""" + return self._sys_par + + @property + def last_error(self) -> ld.LSV2Error: + """type and code of the last transmission error""" + return self._llcom.last_error def connect(self): """connect to control""" @@ -102,937 +83,1139 @@ def connect(self): def disconnect(self): """logout of all open logins and close connection""" self.logout(login=None) + + self._versions = ld.VersionInfo() + self._sys_par = ld.SystemParameters() + self._llcom.disconnect() - logging.debug("Connection to host closed") - - def is_itnc(self): - """return true if control is a iTNC""" - return self._control_type == ControlType.MILL_OLD - - def is_tnc(self): - """return true if control is a TNC""" - return self._control_type == ControlType.MILL_NEW - - def is_pilot(self): - """return true if control is a CNCPILOT640""" - return self._control_type == ControlType.LATHE_NEW - - @staticmethod - def _decode_error(content, locale_path=None): - """decode error codes to text""" - if locale_path is None: - locale_path = os.path.join(os.path.dirname(__file__), "locales") - ( - byte_1, - byte_2, - ) = struct.unpack("!BB", content) - error_text = get_error_text(byte_1, byte_2, locale_path=locale_path) - logging.warning( - "T_ER or T_BD received, an error occurred during the execution of the last command: %s", - error_text, + self._logger.debug("connection to host closed") + + def __enter__(self): + """enter context""" + self.connect() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_tb: Optional[TracebackType], + ): + """exit context""" + self._logger.debug( + "close context with exception type '%s', value '%s' and traceback '%s'", + exc_type, + exc_value, + exc_tb, ) - return error_text - - def _send_recive(self, command, expected_response, payload=None): - """takes a command and payload, sends it to the control and checks - if the response is as expected. Returns content if not an error""" - if expected_response is None: - self._llcom.telegram( - command, payload, buffer_size=self._buffer_size, wait_for_response=False - ) - logging.info( - "command %s sent successfully, did not check for response", command + self.disconnect() + + def switch_safe_mode(self, enable_safe_mode: bool = True): + """switch between safe mode and unrestricted mode""" + if enable_safe_mode is False: + self._logger.info( + "disabling safe mode. login and system commands are not restricted. Use with caution!" ) - return True + self._known_logins = tuple(e.value for e in lc.Login) + self._known_sys_cmd = tuple(e.value for e in lc.ParCCC) else: - response, content = self._llcom.telegram( - command, payload, buffer_size=self._buffer_size, wait_for_response=True + self._logger.info("enabling safe mode. restricting functionality") + self._known_logins = ( + lc.Login.INSPECT, + lc.Login.FILETRANSFER, + lc.Login.MONITOR, + ) + self._known_sys_cmd = ( + lc.ParCCC.SET_BUF1024, + lc.ParCCC.SET_BUF512, + lc.ParCCC.SET_BUF2048, + lc.ParCCC.SET_BUF3072, + lc.ParCCC.SET_BUF4096, + lc.ParCCC.SECURE_FILE_SEND, + lc.ParCCC.SCREENDUMP, ) - if response in expected_response: - if content is not None and len(content) > 0: - logging.debug( - "command %s executed successfully, received %s with %d bytes payload", - command, - response, - len(content), - ) - return content - logging.debug( - "command %s executed successfully, received %s without any payload", - command, - response, + def _send_recive( + self, + command: Union[lc.CMD, lc.RSP], + payload: Union[bytes, bytearray, None] = None, + expected_response: lc.RSP = lc.RSP.NONE, + ) -> Union[bool, bytearray]: + """ + Takes a command and optional payload, sends it to the control and checks if the next telegram contains the + expected response. If the correct response is received, returns response content if available, or ``True`` if + no content was received. Otherwiese returns ``False`` on error. + + Use :py:attr:`~pyLSV2.LSV2.last_error` to check the cause of the last error. + + :param command: valid LSV2 command to send + :param payload: data to send along with the command + :param expected_response: expected response telegram from the control to signal success + + :raises LSV2ProtocolException: if an unknown/unexpected response was received + """ + + if payload is None: + bytes_to_send = bytearray() + elif isinstance(payload, (bytearray,)): + bytes_to_send = payload + else: + bytes_to_send = bytearray(payload) + + if command is lc.CMD.C_CC: + if len(bytes_to_send) < 2: + self._logger.warning( + "system command requires a payload of at exactly 2 bytes" ) - return True + return False - if response in RSP.T_ER: - self._decode_error(content) - self._last_error_code = struct.unpack("!BB", content) - else: - logging.error( - "received unexpected response %s to command %s. response code %s", - response, - command, - content, + c_cc_command = struct.unpack("!H", bytes_to_send[0:2])[0] + if c_cc_command not in self._known_sys_cmd: + self._logger.debug( + "unknown or unsupported system command %s", bytes_to_send ) - self._last_error_code = None + return False - return False + lsv_content = self._llcom.telegram(command, bytes_to_send) - def _send_recive_block(self, command, expected_response, payload=None): - """takes a command and payload, sends it to the control and continues reading - until the expected response is received.""" - response_buffer = list() - response, content = self._llcom.telegram( - command, payload, buffer_size=self._buffer_size - ) + if self._llcom.last_response is lc.RSP.UNKNOWN: + self._logger.error("unknown response received") + raise LSV2ProtocolException("unknown response received") - if response in RSP.T_ER: - self._decode_error(content) - elif response in RSP.T_FD: - logging.debug("Transfer is finished with no content") - elif response not in expected_response: - logging.error( - "received unexpected response %s block read for command %s. response code %s", - response, - command, - content, + if self._llcom.last_response is lc.RSP.T_ER: + self._logger.info( + "an error was received after the last transmission, %s '%s'", + self.last_error, + lt.get_error_text(self.last_error), ) - raise Exception("received unexpected response {}".format(response)) - else: - while response in expected_response: - response_buffer.append(content) - response, content = self._llcom.telegram( - RSP.T_OK, buffer_size=self._buffer_size - ) - return response_buffer + return False - def _send_recive_ack(self, command, payload=None): - """sends command and pyload to control, returns True on T_OK""" - response, content = self._llcom.telegram( - command, payload, buffer_size=self._buffer_size - ) - if response in RSP.T_OK: + if self._llcom.last_response is expected_response: + # expected response received + self._logger.debug( + "expected response received: %s", self._llcom.last_response + ) + if len(lsv_content) > 0: + return lsv_content return True - if response in RSP.T_ER: - self._decode_error(content) - else: - logging.error( - "received unexpected response %s to command %s. response code %s", - response, - command, - content, + if expected_response is lc.RSP.NONE: + self._logger.debug("no response expected") + return False + + self._logger.warning( + "received unexpected response %s", self._llcom.last_response + ) + return False + + def _send_recive_block( + self, + command: Union[lc.CMD, lc.RSP], + payload: bytearray, + expected_response: lc.RSP = lc.RSP.NONE, + ) -> Union[bool, List[bytearray]]: + """ + Takes a command and optional payload, sends it to the control and continues reading telegrams until a + telegram contains the expected response or an error response. If the correct response is received, returns + the accumulated response content. Otherwiese returns ``False`` on error. + + Use :py:attr:`~pyLSV2.LSV2.last_error` to check the cause of the last error. + + :param command: valid LSV2 command to send + :param payload: data to send along with the command + :param expected_response: expected response telegram from the control to signal success + """ + + bytes_to_send = payload + + lsv_content = self._llcom.telegram(command, bytes_to_send) + + if self._llcom.last_response is lc.RSP.UNKNOWN: + self._logger.info("unknown response received, abort") + return False + + if self._llcom.last_response is lc.RSP.T_ER: + self._logger.warning( + "error received, %s '%s'", + self.last_error, + lt.get_error_text(self.last_error), + ) + return False + + if self._llcom.last_response in lc.RSP.T_FD: + if len(lsv_content) > 0: + self._logger.error( + "transfer should have finished without content but data received: %s", + lsv_content, + ) + else: + self._logger.debug("transfer finished without content") + return False + + response_buffer = [] + if self._llcom.last_response is expected_response: + # expected response received + self._logger.debug( + "expected response received: %s", self._llcom.last_response ) + while self._llcom.last_response is expected_response: + response_buffer.append(lsv_content) + lsv_content = self._llcom.telegram(command=lc.RSP.T_OK) + return response_buffer + + self._logger.warning( + "received unexpected response %s, with data %s", + self._llcom.last_response, + lsv_content, + ) return False def _configure_connection(self): - """Set up the communication parameters for file transfer. Buffer size and secure file - transfere are enabled based on the capabilitys of the control. + """ + Set up the communication parameters for file transfer. + Buffer size and secure file transfere are enabled based on the capabilitys of the control. + Automatically enables Login ``INSPECT`` and ``FILETRANSFER`` - :rtype: None + :raises LSV2ProtocolException: if buffer size could not be negotiated or setting of buffer + size did not work """ - self.login(login=Login.INSPECT) - control_type = self.get_versions()["Control"] - max_block_length = self.get_system_parameter()["Max_Block_Length"] - logging.info( + self.login(login=lc.Login.INSPECT) + + self._read_version() + + self._read_parameters() + + self._logger.debug( "setting connection settings for %s and block length %s", - control_type, - max_block_length, + self._versions.type, + self._sys_par.max_block_length, ) - if control_type in ("TNC640", "TNC620", "TNC320", "TNC128"): - self._control_type = ControlType.MILL_NEW - elif control_type in ("iTNC530", "iTNC530 Programm"): - self._control_type = ControlType.MILL_OLD - elif control_type in ("CNCPILOT640",): - self._control_type = ControlType.LATHE_NEW - else: - logging.warning("Unknown control type, treat machine as new style mill") - self._control_type = ControlType.MILL_NEW - selected_size = -1 selected_command = None - if max_block_length >= 4096: + if self._sys_par.max_block_length >= 4096: selected_size = 4096 - selected_command = ParCCC.SET_BUF4096 - elif 3072 <= max_block_length < 4096: + selected_command = lc.ParCCC.SET_BUF4096 + elif 3072 <= self._sys_par.max_block_length < 4096: selected_size = 3072 - selected_command = ParCCC.SET_BUF3072 - elif 2048 <= max_block_length < 3072: + selected_command = lc.ParCCC.SET_BUF3072 + elif 2048 <= self._sys_par.max_block_length < 3072: selected_size = 2048 - selected_command = ParCCC.SET_BUF2048 - elif 1024 <= max_block_length < 2048: + selected_command = lc.ParCCC.SET_BUF2048 + elif 1024 <= self._sys_par.max_block_length < 2048: selected_size = 1024 - selected_command = ParCCC.SET_BUF1024 - elif 512 <= max_block_length < 1024: + selected_command = lc.ParCCC.SET_BUF1024 + elif 512 <= self._sys_par.max_block_length < 1024: selected_size = 512 - selected_command = ParCCC.SET_BUF512 - elif 256 <= max_block_length < 512: + selected_command = lc.ParCCC.SET_BUF512 + elif 256 <= self._sys_par.max_block_length < 512: selected_size = 256 else: - logging.error( + self._logger.error( "could not decide on a buffer size for maximum message length of %d", - max_block_length, + self._sys_par.max_block_length, + ) + raise LSV2ProtocolException( + "could not negotiate buffer site, unknown buffer size of %d" + % self._sys_par.max_block_length ) - raise Exception("unknown buffer size") if selected_command is None: - logging.debug("use smallest buffer size of 256") - self._buffer_size = selected_size + self._logger.debug("use smallest buffer size of 256") + self._llcom.buffer_size = selected_size else: - logging.debug("use buffer size of %d", selected_size) - if self.set_system_command(selected_command): - self._buffer_size = selected_size + self._logger.debug("use buffer size of %d", selected_size) + if self._send_recive( + lc.CMD.C_CC, struct.pack("!H", selected_command), lc.RSP.T_OK + ): + self._llcom.buffer_size = selected_size else: - raise Exception( + raise LSV2ProtocolException( "error in communication while setting buffer size to %d" % selected_size ) - if not self.set_system_command(ParCCC.SECURE_FILE_SEND): - logging.warning("secure file transfer not supported? use fallback") + if not self._send_recive( + lc.CMD.C_CC, struct.pack("!H", lc.ParCCC.SECURE_FILE_SEND), lc.RSP.T_OK + ): + self._logger.debug("secure file transfer not supported? use fallback") self._secure_file_send = False else: + self._logger.debug("secure file send is enabled") self._secure_file_send = True - self.login(login=Login.FILETRANSFER) - logging.info( - "successfully configured connection parameters and basic logins. selected buffer size is %d, use secure file send: %s", - self._buffer_size, - self._secure_file_send, + self.login(login=lc.Login.FILETRANSFER) + + self._logger.info( + "successfully configured connection parameters and basic logins" ) - def login(self, login, password=None): - """Request additional access rights. To elevate this level a logon has to be performed. Some levels require a password. + def login(self, login: lc.Login, password: str = "") -> bool: + """ + Request additional access rights. To elevate this level a logon has to be performed. + Some levels require a password. + Returns ``True`` if execution was successful. - :param str login: One of the known login strings - :param str password: optional. Password for login - :returns: True if execution was successful - :rtype: bool + :param login: One of the known login strings + :param password: optional. Password for login """ + if login in self._active_logins: - logging.debug("login already active") + self._logger.debug("login already active") return True if login not in self._known_logins: - logging.error("unknown or unsupported login") + self._logger.warning("unknown or unsupported login") return False - payload = bytearray() - payload.extend(map(ord, login)) - payload.append(0x00) - if password is not None: - payload.extend(map(ord, password)) - payload.append(0x00) - - if not self._send_recive_ack(CMD.A_LG, payload): - logging.error("an error occurred during login for login %s", login) - return False + payload = lm.ustr_to_ba(login.value) - self._active_logins.append(login) + if password is not None and len(password) > 0: + payload.extend(lm.ustr_to_ba(password)) - logging.info("login executed successfully for login %s", login) - return True + if self._send_recive(lc.CMD.A_LG, payload, lc.RSP.T_OK): + self._logger.debug("login executed successfully for login %s", login.value) + self._active_logins.append(login) + return True - def logout(self, login=None): - """Drop one or all access right. If no login is supplied all active access rights are dropped. + self._logger.warning("error logging in as %s", login.value) + return False - :param str login: optional. One of the known login strings - :returns: True if execution was successful - :rtype: bool + def logout(self, login: Union[lc.Login, None] = None) -> bool: """ - if login in self._known_logins or login is None: - logging.debug("logout for login %s", login) - if login in self._active_logins or login is None: - payload = bytearray() - if login is not None: - payload.extend(map(ord, login)) - payload.append(0x00) - - if self._send_recive_ack(CMD.A_LO, payload): - logging.info("logout executed successfully for login %s", login) - if login is not None: - self._active_logins.remove(login) - else: - self._active_logins = list() + Drop one or all access right. If no login is supplied all active access rights are dropped. + Returns ``True`` if execution was successful. + + :param login: optional. One of the known login strings + """ + payload = bytearray() + + if login is not None: + if isinstance(login, (lc.Login,)): + if login in self._active_logins: + payload.extend(lm.ustr_to_ba(login.value)) + else: + # login is not active return True else: - logging.info("login %s was not active, logout not necessary", login) - return True - else: - logging.warning("unknown or unsupported user") - return False - - def set_system_command(self, command, parameter=None): - """Execute a system command on the control if command is one a known value. If safe mode is active, some of the - commands are disabled. If necessary additinal parameters can be supplied. + # unknown login + return False - :param int command: system command - :param str parameter: optional. parameter payload for system command - :returns: True if execution was successful - :rtype: bool - """ - if command in self._known_sys_cmd: - payload = bytearray() - payload.extend(struct.pack("!H", command)) - if parameter is not None: - payload.extend(map(ord, parameter)) - payload.append(0x00) - if self._send_recive_ack(CMD.C_CC, payload): - return True - logging.debug("unknown or unsupported system command") + if self._send_recive(lc.CMD.A_LO, payload, lc.RSP.T_OK): + self._logger.info("logout executed successfully for login %s", login) + if login is None: + self._active_logins = [] + else: + self._active_logins.remove(login) + return True return False - def get_system_parameter(self, force=False): - """Get all version information, result is bufferd since it is also used internally. With parameter force it is - possible to manually re-read the information form the control + def _read_parameters(self, force: bool = False) -> ld.SystemParameters: + """ + Read all available system parameter entries. The results are buffered since it is also used internally. + This means additional calls dont cause communication with the control. - :param bool force: if True the information is read even if it is already buffered - :returns: dictionary with system parameters like number of plc variables, supported lsv2 version etc. - :rtype: dict + :param force: if ``True`` the information is re-read even if it is already buffered """ - if self._sys_par is not None and force is False: - logging.debug("version info already in memory, return previous values") - return self._sys_par + if self._sys_par.lsv2_version != -1 and force is False: + self._logger.debug( + "system parameters already in memory, return previous values" + ) + else: + result = self._send_recive(lc.CMD.R_PR, None, lc.RSP.S_PR) + if isinstance(result, (bytearray,)): + self._sys_par = lm.decode_system_parameters(result) + self._logger.debug("got system parameters: %s", self._sys_par) + else: + self._logger.warning( + "an error occurred while querying system parameters" + ) + return self._sys_par - result = self._send_recive(command=CMD.R_PR, expected_response=RSP.S_PR) - if result: - sys_par = decode_system_parameters(result) - logging.debug("got system parameters: %s", sys_par) - self._sys_par = sys_par - return self._sys_par - logging.error("an error occurred while querying system parameters") - return False + def _read_version(self, force=False) -> ld.VersionInfo: + """ + Read all available version information entries. The results are buffered since it is also used internally. + This means additional calls dont cause communication with the control. - def get_versions(self, force=False): - """Get all version information, result is bufferd since it is also used internally. With parameter force it is - possible to manually re-read the information form the control + :param force: if ``True`` the information is re-read even if it is already buffered - :param bool force: if True the information is read even if it is already buffered - :returns: dictionary with version text for control type, nc software, plc software, software options etc. - :rtype: dict + :raises LSV2DataException: if basic information could not be read from control """ - if self._versions is not None and force is False: - logging.debug("version info already in memory, return previous values") + if len(self._versions.control) > 0 and force is False: + self._logger.debug("version info already in memory, return previous values") else: - info_data = dict() + info_data = ld.VersionInfo() result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.CONTROL) + lc.CMD.R_VR, struct.pack("!B", lc.ParRVR.CONTROL), lc.RSP.S_VR ) - if result: - info_data["Control"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.control = lm.ba_to_ustr(result) else: - raise Exception("Could not read version information from control") + raise LSV2DataException( + "Could not read version information from control" + ) result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.NC_VERSION) + lc.CMD.R_VR, + struct.pack("!B", lc.ParRVR.NC_VERSION), + lc.RSP.S_VR, ) - if result: - info_data["NC_Version"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.nc_sw = lm.ba_to_ustr(result) result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.PLC_VERSION) + lc.CMD.R_VR, + struct.pack("!B", lc.ParRVR.PLC_VERSION), + lc.RSP.S_VR, ) - if result: - info_data["PLC_Version"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.plc = lm.ba_to_ustr(result) result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.OPTIONS) + lc.CMD.R_VR, + struct.pack("!B", lc.ParRVR.OPTIONS), + lc.RSP.S_VR, ) - if result: - info_data["Options"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.option_bits = lm.ba_to_ustr(result) result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.ID) + lc.CMD.R_VR, + struct.pack("!B", lc.ParRVR.ID), + lc.RSP.S_VR, ) - if result: - info_data["ID"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.id_number = lm.ba_to_ustr(result) - if self.is_itnc(): - info_data["Release_Type"] = "not supported" + if "itnc" in info_data.control.lower(): + info_data.release = "not supported" else: result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.RELEASE_TYPE) + lc.CMD.R_VR, + struct.pack("!B", lc.ParRVR.RELEASE_TYPE), + lc.RSP.S_VR, ) - if result: - info_data["Release_Type"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.release = lm.ba_to_ustr(result) result = self._send_recive( - CMD.R_VR, RSP.S_VR, payload=struct.pack("!B", ParRVR.SPLC_VERSION) + lc.CMD.R_VR, + struct.pack("!B", lc.ParRVR.SPLC_VERSION), + lc.RSP.S_VR, ) - if result: - info_data["SPLC_Version"] = result.strip(b"\x00").decode("utf-8") + if isinstance(result, (bytearray,)) and len(result) > 0: + info_data.splc = lm.ba_to_ustr(result) else: - info_data["SPLC_Version"] = "not supported" + info_data.splc = "not supported" - logging.debug("got version info: %s", info_data) + self._logger.debug("got version info: %s", info_data) self._versions = info_data return self._versions - def get_program_status(self): - """Get status code of currently active program + def program_status(self) -> lc.PgmState: + """ + Ret status code of currently active program. + Requires access level ``DNC`` to work. See https://github.com/tfischer73/Eclipse-Plugin-Heidenhain/issues/1 - - :returns: status code or False if something went wrong - :rtype: int """ - self.login(login=Login.DNC) - - payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.PGM_STATE)) - - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - if result: - pgm_state = struct.unpack("!H", result)[0] - logging.debug( + if not self.login(login=lc.Login.DNC): + self._logger.warning("could not log in as user DNC") + return lc.PgmState.UNDEFINED + + payload = struct.pack("!H", lc.ParRRI.PGM_STATE) + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)): + self._logger.debug( "successfully read state of active program: %s", - get_program_status_text(pgm_state, locale_path=self._locale_path), + struct.unpack("!H", result)[0], ) - return pgm_state + return lc.PgmState(struct.unpack("!H", result)[0]) + self._logger.warning("an error occurred while querying program state") + return lc.PgmState.UNDEFINED - logging.error("an error occurred while querying program state") - return False - - def get_program_stack(self): - """Get path of currently active nc program(s) and current line number + def program_stack(self) -> Union[ld.StackState, None]: + """ + Get path of currently active nc program(s) and current line number. + Requires access level ``DNC`` to work. See https://github.com/tfischer73/Eclipse-Plugin-Heidenhain/issues/1 - - :returns: dictionary with line number, main program and current program or False if something went wrong - :rtype: dict """ - self.login(login=Login.DNC) - - payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.SELECTED_PGM)) - - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload=payload) - if result: - stack_info = dict() - stack_info["Line"] = struct.unpack("!L", result[:4])[0] - stack_info["Main_PGM"] = ( - result[4:] - .split(b"\x00")[0] - .decode() - .strip("\x00") # .replace("\\", "/") - ) - stack_info["Current_PGM"] = ( - result[4:] - .split(b"\x00")[1] - .decode() - .strip("\x00") # .replace("\\", "/") - ) - logging.debug( - "successfully read active program stack and line number: %s", stack_info - ) + if not self.login(login=lc.Login.DNC): + self._logger.warning("could not log in as user DNC") + return None + + payload = struct.pack("!H", lc.ParRRI.SELECTED_PGM) + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)) and len(result) > 0: + stack_info = lm.decode_stack_info(result) + self._logger.debug("successfully read active program stack: %s", stack_info) return stack_info + self._logger.warning("an error occurred while querying active program state") - logging.error("an error occurred while querying active program state") - return False + return None - def get_execution_status(self): - """Get status code of program state to text + def execution_state(self) -> lc.ExecState: + """ + Get status code of program state + Requires access level ``DNC`` to work. See https://github.com/drunsinn/pyLSV2/issues/1 - - :returns: status code or False if something went wrong - :rtype: int """ - self.login(login=Login.DNC) + if not self.login(login=lc.Login.DNC): + self._logger.warning("could not log in as user DNC") + return lc.ExecState.UNDEFINED - payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.EXEC_STATE)) + payload = struct.pack("!H", lc.ParRRI.EXEC_STATE) - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - if result: - exec_state = struct.unpack("!H", result)[0] - logging.debug( - "read execution state %d : %s", - exec_state, - get_execution_status_text(exec_state, locale_path=self._locale_path), + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)): + self._logger.debug( + "read execution state %d", struct.unpack("!H", result)[0] ) - return exec_state + return lc.ExecState(struct.unpack("!H", result)[0]) + self._logger.warning("an error occurred while querying execution state") + return lc.ExecState.UNDEFINED - logging.error("an error occurred while querying execution state") - return False - - def get_directory_info(self, remote_directory=None): - """Query information a the current working directory on the control + def directory_info(self, remote_directory: str = "") -> ld.DirectoryEntry: + """ + Read information about the currenct working directory on the control. + Requires access level ``FILETRANSFER`` to work. - :param str remote_directory: optional. If set, working directory will be changed - :returns: dictionary with info about the directory or False if an error occurred - :rtype: dict + :param remote_directory: optional. change working directory before reading info """ - if remote_directory is not None and not self.change_directory(remote_directory): - logging.error( + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return ld.DirectoryEntry() + + if ( + len(remote_directory) > 0 + and self.change_directory(remote_directory) is False + ): + self._logger.warning( "could not change current directory to read directory info for %s", remote_directory, ) - - result = self._send_recive(CMD.R_DI, RSP.S_DI) - if result: - dir_info = decode_directory_info(result) - logging.debug("successfully received directory information %s", dir_info) - + return ld.DirectoryEntry() + result = self._send_recive(lc.CMD.R_DI, None, lc.RSP.S_DI) + if isinstance(result, (bytearray,)) and len(result) > 0: + dir_info = lm.decode_directory_info(result) + self._logger.debug( + "successfully received directory information for %s", dir_info.path + ) return dir_info + self._logger.warning("an error occurred while querying directory info") - logging.error("an error occurred while querying directory info") - return False + return ld.DirectoryEntry() - def change_directory(self, remote_directory): - """Change the current working directoyon the control + def change_directory(self, remote_directory: str) -> bool: + """ + change the current working directory on the control. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. - :param str remote_directory: path of directory on the control - :returns: True if changing of directory succeeded - :rtype: bool + :param remote_directory: path of directory on the control """ - dir_path = remote_directory.replace("/", PATH_SEP) - payload = bytearray() - payload.extend(map(ord, dir_path)) - payload.append(0x00) - if self._send_recive_ack(CMD.C_DC, payload=payload): - logging.debug("changed working directory to %s", dir_path) - return True + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False + + dir_path = remote_directory.replace("/", lc.PATH_SEP) + payload = lm.ustr_to_ba(dir_path) - logging.error("an error occurred while changing directory") + result = self._send_recive(lc.CMD.C_DC, payload, lc.RSP.T_OK) + if isinstance(result, (bool,)) and result is True: + self._logger.debug("changed working directory to %s", dir_path) + return True + self._logger.warning("an error occurred while changing directory") return False - def get_file_info(self, remote_file_path): - """Query information about a file + def file_info(self, remote_file_path: str) -> Union[ld.FileEntry, None]: + """ + Query information about a file. + Requires access level ``FILETRANSFER`` to work. + Returns ``None`` of file doesn't exist or missing access rights + + :param remote_file_path: path of file on the control - :param str remote_file_path: path of file on the control - :returns: dictionary with info about file of False if remote path does not exist - :rtype: dict + :raises LSV2ProtocolException: if an error occurred during reading of file info """ - file_path = remote_file_path.replace("/", PATH_SEP) - payload = bytearray() - payload.extend(map(ord, file_path)) - payload.append(0x00) + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return None - result = self._send_recive(CMD.R_FI, RSP.S_FI, payload=payload) - if result: - file_info = decode_file_system_info(result, self._control_type) - logging.debug("successfully received file information %s", file_info) + file_path = remote_file_path.replace("/", lc.PATH_SEP) + payload = lm.ustr_to_ba(file_path) + + result = self._send_recive(lc.CMD.R_FI, payload, lc.RSP.S_FI) + if isinstance(result, (bytearray,)) and len(result) > 0: + file_info = lm.decode_file_system_info(result, self._versions.type) + self._logger.debug("received file information for %s", file_info.name) return file_info - logging.warning( - "an error occurred while querying file info this might also indicate that it does not exist %s", + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_NO_FILE: + self._logger.debug("file does not exist") + return None + + self._logger.error( + "an error occurred while querying file info for %s : '%s'", remote_file_path, + lt.get_error_text(self.last_error), ) - return False - - def get_directory_content(self): - """Query content of current working directory from the control. - In some situations it is necessary to fist call get_directory_info() or else - the attributes won't be correct. + return None - :returns: list of dict with info about directory entries - :rtype: list + def directory_content(self) -> List[ld.FileEntry]: + """ + Query content of current working directory from the control. In some situations it is necessary to + fist call :py:func:`~pyLSV2.LSV2.directory_info` or else the attributes won't be correct. + Requires access level ``FILETRANSFER`` to work. """ - dir_content = list() - payload = bytearray() - payload.append(ParRDR.SINGLE) - result = self._send_recive_block(CMD.R_DR, RSP.S_DR, payload) - logging.debug( - "received %d entries for directory content information", len(result) - ) - for entry in result: - dir_content.append(decode_file_system_info(entry, self._control_type)) + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return [] - logging.debug("successfully received directory information %s", dir_content) - return dir_content + dir_content = [] + payload = bytearray(struct.pack("!B", lc.ParRDR.SINGLE)) - def get_drive_info(self): - """Query info all drives and partitions from the control + result = self._send_recive_block(lc.CMD.R_DR, payload, lc.RSP.S_DR) + if isinstance(result, (list,)): + for entry in result: + dir_content.append( + lm.decode_file_system_info(entry, self._versions.type) + ) - :returns: list of dict with with info about drive entries - :rtype: list - """ - drives_list = list() - payload = bytearray() - payload.append(ParRDR.DRIVES) + self._logger.debug( + "received %d packages for directory content", len(dir_content) + ) + else: + self._logger.warning( + "an error occurred while directory content info: '%s'", + lt.get_error_text(self.last_error), + ) + return dir_content - result = self._send_recive_block(CMD.R_DR, RSP.S_DR, payload) - logging.debug("received %d packet of for drive information", len(result)) - for entry in result: - drives_list.append(entry) + def drive_info(self) -> List[ld.DriveEntry]: + """ + Read info all drives and partitions from the control. + Requires access level ``FILETRANSFER`` to work. + """ - logging.debug("successfully received drive information %s", drives_list) + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return [] + + drives_list = [] + payload = bytearray(struct.pack("!B", lc.ParRDR.DRIVES)) + result = self._send_recive_block(lc.CMD.R_DR, payload, lc.RSP.S_DR) + if isinstance(result, (list,)): + for entry in result: + drives_list.extend(lm.decode_drive_info(entry)) + + self._logger.debug( + "successfully received %d packages for drive information %s", + len(result), + drives_list, + ) + else: + self._logger.warning( + "an error occurred while reading drive info: '%s'", + lt.get_error_text(self.last_error), + ) return drives_list - def make_directory(self, dir_path): - """Create a directory on control. If necessary also creates parent directories + def make_directory(self, dir_path: str) -> bool: + """ + Create a directory on control. If necessary also creates parent directories. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. - :param str dir_path: path of directory on the control - :returns: True if creating of directory completed successfully - :rtype: bool + :param dir_path: path of directory on the control """ - path_parts = dir_path.replace("/", PATH_SEP).split(PATH_SEP) # convert path + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False + + path_parts = dir_path.replace("/", lc.PATH_SEP).split( + lc.PATH_SEP + ) # convert path path_to_check = "" + for part in path_parts: - path_to_check += part + PATH_SEP + path_to_check += part + lc.PATH_SEP # no file info -> does not exist and has to be created - if self.get_file_info(path_to_check) is False: - payload = bytearray() - payload.extend(map(ord, path_to_check)) - payload.append(0x00) # terminate string - if self._send_recive_ack(command=CMD.C_DM, payload=payload): - logging.debug("Directory created successfully") + if self.file_info(path_to_check) is None: + payload = lm.ustr_to_ba(path_to_check) + + result = self._send_recive(lc.CMD.C_DM, payload, lc.RSP.T_OK) + if isinstance(result, (bool,)) and result is True: + self._logger.debug("Directory created successfully") else: - raise Exception( - "an error occurred while creating directory {}".format(dir_path) + self._logger.warning( + "an error occurred while creating directory %s: '%s'", + dir_path, + lt.get_error_text(self.last_error), ) + return False else: - logging.debug("nothing to do as this segment already exists") + self._logger.debug("nothing to do as this segment already exists") return True - def delete_empty_directory(self, dir_path): - """Delete empty directory on control + def delete_empty_directory(self, dir_path: str) -> bool: + """ + Delete empty directory on control. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. - :param str file_path: path of directory on the control - :returns: True if deleting of directory completed successfully - :rtype: bool + :param file_path: path of directory on the control """ - dir_path = dir_path.replace("/", PATH_SEP) - payload = bytearray() - payload.extend(map(ord, dir_path)) - payload.append(0x00) - if not self._send_recive_ack(command=CMD.C_DD, payload=payload): - logging.warning( - "an error occurred while deleting directory %s, this might also indicate that it it does not exist", - dir_path, + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False + + dir_path = dir_path.replace("/", lc.PATH_SEP) + payload = lm.ustr_to_ba(dir_path) + + result = self._send_recive(lc.CMD.C_DD, payload, lc.RSP.T_OK) + if isinstance(result, (bool)) and result is True: + self._logger.debug("successfully deleted directory %s", dir_path) + return True + + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_NO_DIR: + self._logger.debug("noting to do, directory %s didn't exist", dir_path) + return True + + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_DEL_DIR: + self._logger.debug( + "could not delete directory %s since it is not empty", dir_path ) return False - logging.debug("successfully deleted directory %s", dir_path) - return True - def delete_file(self, file_path): - """Delete file on control + self._logger.warning( + "an error occurred while deleting directory %s", + dir_path, + ) + return False - :param str file_path: path of file on the control - :returns: True if deleting of file completed successfully - :rtype: bool + def delete_file(self, file_path: str) -> bool: """ - file_path = file_path.replace("/", PATH_SEP) - payload = bytearray() - payload.extend(map(ord, file_path)) - payload.append(0x00) - if not self._send_recive_ack(command=CMD.C_FD, payload=payload): - logging.warning( - "an error occurred while deleting file %s, this might also indicate that it it does not exist", - file_path, - ) + Delete file on control. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. + + :param file_path: path of file on the control + """ + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") return False - logging.debug("successfully deleted file %s", file_path) - return True - def copy_local_file(self, source_path, target_path): - """Copy file on control from one place to another + file_path = file_path.replace("/", lc.PATH_SEP) + payload = lm.ustr_to_ba(file_path) + + if self._send_recive(lc.CMD.C_FD, payload, lc.RSP.T_OK): + self._logger.debug("successfully deleted file %s", file_path) + return True - :param str source_path: path of file on the control - :param str target_path: path of target location - :returns: True if copying of file completed successfully - :rtype: bool + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_NO_FILE: + self._logger.debug("noting to do, file %s didn't exist", file_path) + return True + + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_NO_DELETE: + self._logger.info("could not delete file %s since it is in use", file_path) + return False + + self._logger.warning( + "an error occurred while deleting file %s", + file_path, + ) + return False + + def copy_remote_file(self, source_path: str, target_path: str) -> bool: """ - source_path = source_path.replace("/", PATH_SEP) - target_path = target_path.replace("/", PATH_SEP) + Copy file on control from one place to another. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. + + :param source_path: path of file on the control + :param target_path: path of target location - if PATH_SEP in source_path: + :raises LSV2StateException: if the selected path could not be found or + the path is not accessible + """ + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False + + source_path = source_path.replace("/", lc.PATH_SEP) + target_path = target_path.replace("/", lc.PATH_SEP) + + if lc.PATH_SEP in source_path: # change directory - source_file_name = source_path.split(PATH_SEP)[-1] + source_file_name = source_path.split(lc.PATH_SEP)[-1] source_directory = source_path.rstrip(source_file_name) if not self.change_directory(remote_directory=source_directory): - raise Exception("could not open the source directory") + raise LSV2StateException("could not open the source directory") else: source_file_name = source_path source_directory = "." - if target_path.endswith(PATH_SEP): + if target_path.endswith(lc.PATH_SEP): target_path += source_file_name - payload = bytearray() - payload.extend(map(ord, source_file_name)) - payload.append(0x00) - payload.extend(map(ord, target_path)) - payload.append(0x00) - logging.debug( + payload = lm.ustr_to_ba(source_file_name) + payload.extend(lm.ustr_to_ba(target_path)) + self._logger.debug( "prepare to copy file %s from %s to %s", source_file_name, source_directory, target_path, ) - if not self._send_recive_ack(command=CMD.C_FC, payload=payload): - logging.warning( - "an error occurred copying file %s to %s", source_path, target_path - ) - return False - logging.debug("successfully copied file %s", source_path) - return True + if self._send_recive(lc.CMD.C_FC, payload, lc.RSP.T_OK): + self._logger.debug("successfully copied file %s", source_path) + return True - def move_local_file(self, source_path, target_path): - """Move file on control from one place to another + self._logger.warning( + "an error occurred copying file %s to %s", source_path, target_path + ) + return False - :param str source_path: path of file on the control - :param str target_path: path of target location - :returns: True if moving of file completed successfully - :rtype: bool + def move_file(self, source_path: str, target_path: str) -> bool: """ - source_path = source_path.replace("/", PATH_SEP) - target_path = target_path.replace("/", PATH_SEP) + Move file on control from one place to another. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if creating directory was successful. - if PATH_SEP in source_path: - source_file_name = source_path.split(PATH_SEP)[-1] + :param source_path: path of file on the control + :param target_path: path of target location with or without filename + + :raises LSV2StateException: if the selected path could not be found or + the path is not accessible + """ + source_path = source_path.replace("/", lc.PATH_SEP) + target_path = target_path.replace("/", lc.PATH_SEP) + + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False + + if lc.PATH_SEP in source_path: + source_file_name = source_path.split(lc.PATH_SEP)[-1] source_directory = source_path.rstrip(source_file_name) if not self.change_directory(remote_directory=source_directory): - raise Exception("could not open the source directory") + raise LSV2StateException("could not open the source directory") else: source_file_name = source_path source_directory = "." - if target_path.endswith(PATH_SEP): + if target_path.endswith(lc.PATH_SEP): target_path += source_file_name - payload = bytearray() - payload.extend(map(ord, source_file_name)) - payload.append(0x00) - payload.extend(map(ord, target_path)) - payload.append(0x00) - logging.debug( + payload = lm.ustr_to_ba(source_file_name) + payload.extend(lm.ustr_to_ba(target_path)) + + self._logger.debug( "prepare to move file %s from %s to %s", source_file_name, source_directory, target_path, ) - if not self._send_recive_ack(command=CMD.C_FR, payload=payload): - logging.warning( - "an error occurred moving file %s to %s", source_path, target_path + if self._send_recive(lc.CMD.C_FR, payload, lc.RSP.T_OK): + self._logger.debug("successfully moved file %s", source_path) + return True + + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_FILE_EXISTS: + self._logger.info( + "could not move file %s to %s since already exists", + source_path, + target_path, ) return False - logging.debug("successfully moved file %s", source_path) - return True - def send_file( - self, local_path, remote_path, override_file=False, binary_mode=False - ): - """Upload a file to control + if self.last_error.e_code == lc.LSV2StatusCode.T_ER_NO_FILE: + self._logger.info( + "could not move file since either source or target path does not exist", + ) + return False + + self._logger.warning( + "an error occurred moving file %s to %s", source_path, target_path + ) + return False - :param str remote_path: path of file on the control - :param str local_path: local path of destination with or without file name - :param bool override_file: flag if file should be replaced if it already exists - :param bool binary_mode: flag if binary transfer mode should be used, if not set the - file name is checked for known binary file type - :returns: True if transfer completed successfully - :rtype: bool + def send_file( + self, + local_path: Union[str, pathlib.Path], + remote_path: str, + override_file: bool = False, + binary_mode: bool = False, + ) -> bool: """ - local_file = Path(local_path) + Upload a file to control + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. + + :param local_path: path of file to be sent to the control + :param remote_path: path with or without the file name on the control + :param override_file: flag if file should be replaced if it already exists + :param binary_mode: flag if binary transfer mode should be used, if not set the + file name is checked for known binary file type + + :raises LSV2StateException: if local file could not be opened, + destination directory could not be accessed or + destination file could not be deleted + """ + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False + + if isinstance(local_path, (str,)): + local_file = pathlib.Path(local_path) + else: + local_file = local_path if not local_file.is_file(): - logging.error("the supplied path %s did not resolve to a file", local_file) - raise Exception("local file does not exist! {}".format(local_file)) + self._logger.warning( + "the supplied path %s did not resolve to a file", local_file + ) + raise LSV2StateException("local file does not exist! {}".format(local_file)) - remote_path = remote_path.replace("/", PATH_SEP) + remote_path = remote_path.replace("/", lc.PATH_SEP) - if PATH_SEP in remote_path: - if remote_path.endswith(PATH_SEP): # no filename given + if lc.PATH_SEP in remote_path: + if remote_path.endswith(lc.PATH_SEP): # no filename given remote_file_name = local_file.name remote_directory = remote_path else: - remote_file_name = remote_path.split(PATH_SEP)[-1] + remote_file_name = remote_path.split(lc.PATH_SEP)[-1] remote_directory = remote_path.rstrip(remote_file_name) if not self.change_directory(remote_directory=remote_directory): - raise Exception( - "could not open the source directory {}".format( + raise LSV2StateException( + "could not open the destination directory {}".format( remote_directory ) ) else: remote_file_name = remote_path - remote_directory = self.get_directory_info()["Path"] # get pwd - remote_directory = remote_directory.rstrip(PATH_SEP) + remote_directory = self.directory_info().path # get pwd + remote_directory = remote_directory.rstrip(lc.PATH_SEP) - if not self.get_directory_info(remote_directory): - logging.debug("remote path does not exist, create directory(s)") + if not self.directory_info(remote_directory): + self._logger.debug("remote path does not exist, create directory(s)") self.make_directory(remote_directory) - remote_info = self.get_file_info(remote_directory + PATH_SEP + remote_file_name) + remote_info = self.file_info(remote_directory + lc.PATH_SEP + remote_file_name) if remote_info: - logging.debug("remote path exists and points to file's") + self._logger.debug("remote path exists and points to file's") if override_file: - if not self.delete_file(remote_directory + PATH_SEP + remote_file_name): - raise Exception( + if not self.delete_file( + remote_directory + lc.PATH_SEP + remote_file_name + ): + raise LSV2StateException( "something went wrong while deleting file {}".format( - remote_directory + PATH_SEP + remote_file_name + remote_directory + lc.PATH_SEP + remote_file_name ) ) else: - logging.warning("remote file already exists, override was not set") + self._logger.warning("remote file already exists, override was not set") return False - logging.debug( + self._logger.debug( "ready to send file from %s to %s", local_file, - remote_directory + PATH_SEP + remote_file_name, + remote_directory + lc.PATH_SEP + remote_file_name, ) - payload = bytearray() - payload.extend(map(ord, remote_directory + PATH_SEP + remote_file_name)) - payload.append(0x00) - if binary_mode or is_file_binary(local_path): - payload.append(MODE_BINARY) - logging.info("selecting binary transfer mode for this file type") + payload = lm.ustr_to_ba(remote_directory + lc.PATH_SEP + remote_file_name) + if binary_mode or lm.is_file_binary(local_path): + payload.append(lc.MODE_BINARY) + self._logger.debug("selecting binary transfer mode") else: - payload.append(0x00) - logging.info("selecting non binary transfer mode") + payload.append(lc.MODE_NON_BIN) + self._logger.debug("selecting non binary transfer mode") - response, content = self._llcom.telegram( - CMD.C_FL, payload, buffer_size=self._buffer_size + self._llcom.telegram( + lc.CMD.C_FL, + payload, ) - if response in RSP.T_OK: + if self._llcom.last_response in lc.RSP.T_OK: with local_file.open("rb") as input_buffer: while True: # use current buffer size but reduce by 10 to make sure it fits together with command and size - buffer = input_buffer.read(self._buffer_size - 10) + buffer = bytearray( + input_buffer.read(self._llcom.buffer_size - 8 - 2) + ) if not buffer: # finished reading file break - response, content = self._llcom.telegram( - RSP.S_FL, buffer, buffer_size=self._buffer_size + self._llcom.telegram( + lc.RSP.S_FL, + buffer, ) - if response in RSP.T_OK: + if self._llcom.last_response in lc.RSP.T_OK: pass else: - if response in RSP.T_ER: - self._decode_error(content) + if self._llcom.last_response == lc.RSP.T_ER: + self._logger.info( + "control returned error '%s' which translates to '%s'", + self.last_error, + lt.get_error_text(self.last_error), + ) else: - logging.error("could not send data with error %s", response) + self._logger.warning( + "could not send data, received unexpected response '%s'", + self._llcom.last_response, + ) return False # signal that no more data is being sent if self._secure_file_send: - if not self._send_recive( - command=RSP.T_FD, expected_response=RSP.T_OK, payload=None - ): - logging.error("could not send end of file with error") + if not self._send_recive(lc.RSP.T_FD, None, lc.RSP.T_OK): + self._logger.warning( + "could not send end of transmission telegram, got response '%s'", + self._llcom.last_response, + ) return False else: - if not self._send_recive( - command=RSP.T_FD, expected_response=None, payload=None - ): - logging.error("could not send end of file with error") + if not self._send_recive(lc.RSP.T_FD, None, lc.RSP.NONE): + self._logger.warning( + "could not send end of transmission telegram, got response '%s'", + self._llcom.last_response, + ) return False else: - if response in RSP.T_ER: - self._decode_error(content) + if self._llcom.last_response is lc.RSP.T_ER: + self._logger.warning( + "error received, %s '%s'", + self.last_error, + lt.get_error_text(self.last_error), + ) else: - logging.error("could not send file with error %s", response) + self._logger.warning( + "could not send file with error %s", self._llcom.last_response + ) return False return True def recive_file( - self, remote_path, local_path, override_file=False, binary_mode=False - ): - """Download a file from control - - :param str remote_path: path of file on the control - :param str local_path: local path of destination with or without file name - :param bool override_file: flag if file should be replaced if it already exists - :param bool binary_mode: flag if binary transfer mode should be used, if not set the file name is - checked for known binary file type - :returns: True if transfer completed successfully - :rtype: bool + self, + remote_path: str, + local_path: Union[str, pathlib.Path], + override_file: bool = False, + binary_mode: bool = False, + ) -> bool: """ + Download a file from control. + Requires access level ``FILETRANSFER`` to work. + Returns ``True`` if completed successfully. + + :param remote_path: path of file on the control + :param local_path: local path of destination with or without file name + :param override_file: flag if file should be replaced if it already exists + :param binary_mode: flag if binary transfer mode should be used, if not set the + file name is checked for known binary file type + """ + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("could not log in as user FILE") + return False - remote_path = remote_path.replace("/", PATH_SEP) - remote_file_info = self.get_file_info(remote_path) + if isinstance(local_path, (str,)): + local_file = pathlib.Path(local_path) + else: + local_file = local_path + + remote_path = remote_path.replace("/", lc.PATH_SEP) + remote_file_info = self.file_info(remote_path) if not remote_file_info: - logging.error("remote file does not exist: %s", remote_path) + self._logger.warning("remote file does not exist: %s", remote_path) return False - local_file = Path(local_path) if local_file.is_dir(): local_file.joinpath(remote_path.split("/")[-1]) - - if local_file.is_file(): - logging.debug("local path exists and points to file") - if override_file: - local_file.unlink() - else: - logging.warning( - "local file already exists and override was not set. doing nothing" + elif local_file.is_file(): + # self._logger.debug("local path exists and points to file") + if not override_file: + self._logger.warning( + "local file already exists and override was not set. nothing to do" ) return False + local_file.unlink() - logging.debug("loading file from %s to %s", remote_path, local_file) + self._logger.debug("loading file from %s to %s", remote_path, local_file) - payload = bytearray() - payload.extend(map(ord, remote_path)) - payload.append(0x00) - if binary_mode or is_file_binary(remote_path): - payload.append(MODE_BINARY) # force binary transfer - logging.info("using binary transfer mode") + payload = lm.ustr_to_ba(remote_path) + + if binary_mode or lm.is_file_binary(remote_path): + payload.append(lc.MODE_BINARY) # force binary transfer + self._logger.debug("using binary transfer mode") else: - payload.append(0x00) - logging.info("using non binary transfer mode") + payload.append(lc.MODE_NON_BIN) + self._logger.debug("using non binary transfer mode") - response, content = self._llcom.telegram( - CMD.R_FL, payload, buffer_size=self._buffer_size + content = self._llcom.telegram( + lc.CMD.R_FL, + payload, ) with local_file.open("wb") as out_file: - if response in RSP.S_FL: + if self._llcom.last_response in lc.RSP.S_FL: if binary_mode: out_file.write(content) else: out_file.write(content.replace(b"\x00", b"\r\n")) - logging.debug("received first block of file file %s", remote_path) + self._logger.debug("received first block of file file %s", remote_path) while True: - response, content = self._llcom.telegram( - RSP.T_OK, payload=None, buffer_size=self._buffer_size + content = self._llcom.telegram( + lc.RSP.T_OK, ) - if response in RSP.S_FL: + if self._llcom.last_response in lc.RSP.S_FL: if binary_mode: out_file.write(content) else: out_file.write(content.replace(b"\x00", b"\r\n")) - logging.debug("received %d more bytes for file", len(content)) - elif response in RSP.T_FD: - logging.info("finished loading file") + self._logger.debug( + "received %d more bytes for file", len(content) + ) + elif self._llcom.last_response in lc.RSP.T_FD: + self._logger.info("finished loading file") break else: - if response in RSP.T_ER or response in RSP.T_BD: - logging.error( - "an error occurred while loading the first block of data for file %s : %s", - remote_path, - self._decode_error(content), - ) - else: - logging.error( - "something went wrong while receiving file data %s", - remote_path, + self._logger.warning( + "something went wrong while receiving file data %s", + remote_path, + ) + if ( + self._llcom.last_response is lc.RSP.T_ER + or self._llcom.last_response is lc.RSP.T_BD + ): + self._logger.warning( + "an error occurred while loading the first block of data %s '%s'", + self.last_error, + lt.get_error_text(self.last_error), ) return False else: - if response in RSP.T_ER or response in RSP.T_BD: - logging.error( - "an error occurred while loading the first block of data for file %s : %s", + if ( + self._llcom.last_response is lc.RSP.T_ER + or self._llcom.last_response is lc.RSP.T_BD + ): + self._logger.warning( + "an error occurred while loading the first block of data for file %s, %s '%s'", remote_path, - self._decode_error(content), + self.last_error, + lt.get_error_text(self.last_error), ) - self._last_error_code = struct.unpack("!BB", content) else: - logging.error("could not load file with error %s", response) - self._last_error_code = None + self._logger.warning( + "could not load file with error %s", self._llcom.last_response + ) return False - logging.info( + self._logger.info( "received %d bytes transfer complete for file %s to %s", local_file.stat().st_size, remote_path, @@ -1041,102 +1224,108 @@ def recive_file( return True - def read_plc_memory(self, address, mem_type, count=1): - """Read data from plc memory. + def read_plc_memory( + self, first_element: int, mem_type: lc.MemoryType, number_of_elements: int = 1 + ) -> list: + """ + Read data from plc memory. + Requires access level ``PLCDEBUG`` to work. - :param address: which memory location should be read, starts at 0 up to the max number for each type + :param first_element: which memory location should be read, starts at 0 up to the max number for each type :param mem_type: what datatype to read - :param count: how many elements should be read at a time, from 1 (default) up to 255 or max number - :returns: a list with the data values - :raises Exception: raises an Exception + :param number_of_elements: how many elements should be read + + :raises LSV2InputException: if unknowns memory type is requested or if the to many elements are requested + :raises LSV2DataException: if number of received values does not match the number of expected """ - if self._sys_par is None: - self.get_system_parameter() - self.login(login=Login.PLCDEBUG) + if self._sys_par.lsv2_version != -1: + self._read_parameters() - first_element = address - number_of_elements = count # keep old parameter name for compatibility + if not self.login(login=lc.Login.PLCDEBUG): + self._logger.warning("could not log in as user PLCDEBUG") + return [] - if mem_type is MemoryType.MARKER: - mem_start_address = self._sys_par["Marker_Start"] - max_elemens = self._sys_par["Markers"] - mem_bytes_per_element = 1 + if mem_type is lc.MemoryType.MARKER: + start_address = self._sys_par.markers_start_address + max_elemens = self._sys_par.number_of_markers + mem_byte_count = 1 unpack_string = "!?" - elif mem_type is MemoryType.INPUT: - mem_start_address = self._sys_par["Input_Start"] - max_elemens = self._sys_par["Inputs"] - mem_bytes_per_element = 1 + elif mem_type is lc.MemoryType.INPUT: + start_address = self._sys_par.inputs_start_address + max_elemens = self._sys_par.number_of_inputs + mem_byte_count = 1 unpack_string = "!?" - elif mem_type is MemoryType.OUTPUT: - mem_start_address = self._sys_par["Output_Start"] - max_elemens = self._sys_par["Outputs"] - mem_bytes_per_element = 1 + elif mem_type is lc.MemoryType.OUTPUT: + start_address = self._sys_par.outputs_start_address + max_elemens = self._sys_par.number_of_outputs + mem_byte_count = 1 unpack_string = "!?" - elif mem_type is MemoryType.COUNTER: - mem_start_address = self._sys_par["Counter_Start"] - max_elemens = self._sys_par["Counters"] - mem_bytes_per_element = 1 + elif mem_type is lc.MemoryType.COUNTER: + start_address = self._sys_par.counters_start_address + max_elemens = self._sys_par.number_of_counters + mem_byte_count = 1 unpack_string = "!?" - elif mem_type is MemoryType.TIMER: - mem_start_address = self._sys_par["Timer_Start"] - max_elemens = self._sys_par["Timers"] - mem_bytes_per_element = 1 + elif mem_type is lc.MemoryType.TIMER: + start_address = self._sys_par.timers_start_address + max_elemens = self._sys_par.number_of_timers + mem_byte_count = 1 unpack_string = "!?" - elif mem_type is MemoryType.BYTE: - mem_start_address = self._sys_par["Word_Start"] - max_elemens = self._sys_par["Words"] * 2 - mem_bytes_per_element = 1 + elif mem_type is lc.MemoryType.BYTE: + start_address = self._sys_par.words_start_address + max_elemens = self._sys_par.number_of_words * 2 + mem_byte_count = 1 unpack_string = "!B" - elif mem_type is MemoryType.WORD: - mem_start_address = self._sys_par["Word_Start"] - max_elemens = self._sys_par["Words"] - mem_bytes_per_element = 2 + elif mem_type is lc.MemoryType.WORD: + start_address = self._sys_par.words_start_address + max_elemens = self._sys_par.number_of_words + mem_byte_count = 2 unpack_string = " max_elemens: - raise ValueError("maximum number of values is %d" % max_elemens) + if (first_element + number_of_elements) > max_elemens: + raise LSV2InputException( + "highest address is %d but address of last requested element is %d" + % (max_elemens, (first_element + number_of_elements)) + ) - plc_values = list() + plc_values = [] - if mem_type is MemoryType.STRING: + if mem_type is lc.MemoryType.STRING: for i in range(number_of_elements): address = ( - mem_start_address - + first_element * mem_bytes_per_element - + i * mem_bytes_per_element + start_address + first_element * mem_byte_count + i * mem_byte_count ) payload = bytearray() payload.extend(struct.pack("!L", address)) - payload.extend(struct.pack("!B", mem_bytes_per_element)) - result = self._send_recive(CMD.R_MB, RSP.S_MB, payload=payload) + payload.extend(struct.pack("!B", mem_byte_count)) + result = self._send_recive(lc.CMD.R_MB, payload, lc.RSP.S_MB) if isinstance(result, (bytearray,)): logging.debug( - "read string %d with lenght %d", + "read string %d with length %d", (first_element + i), len(result), ) @@ -1144,9 +1333,7 @@ def read_plc_memory(self, address, mem_type, count=1): unpack_string = "{}s".format(len(result)) plc_values.append( - struct.unpack(unpack_string, result)[0] - .rstrip(b"\x00") - .decode("latin1") + lm.ba_to_ustr(struct.unpack(unpack_string, result)[0]) ) else: logging.error( @@ -1154,10 +1341,10 @@ def read_plc_memory(self, address, mem_type, count=1): (first_element + i), address, ) - return False + return [] else: - max_elements_per_transfer = math.floor(255 / mem_bytes_per_element) + max_elements_per_transfer = math.floor(255 / mem_byte_count) num_groups = math.ceil(number_of_elements / max_elements_per_transfer) logging.debug( "memory type allows %d elements per telegram, split request into %d group(s)", @@ -1173,58 +1360,54 @@ def read_plc_memory(self, address, mem_type, count=1): else: elements_in_group = remaining_elements address = ( - mem_start_address - + first_element * mem_bytes_per_element - + i * elements_in_group * mem_bytes_per_element + start_address + + first_element * mem_byte_count + + i * elements_in_group * mem_byte_count ) logging.debug( "current transfer group %d has %d elements", i, elements_in_group ) - # address = mem_start_address + first_element * mem_bytes_per_element - payload = bytearray() payload.extend(struct.pack("!L", address)) - payload.extend( - struct.pack("!B", elements_in_group * mem_bytes_per_element) - ) - result = self._send_recive(CMD.R_MB, RSP.S_MB, payload=payload) + payload.extend(struct.pack("!B", elements_in_group * mem_byte_count)) + result = self._send_recive(lc.CMD.R_MB, payload, lc.RSP.S_MB) if isinstance(result, (bytearray,)): logging.debug( "read %d value(s) from address %d", elements_in_group, first_element, ) - for i in range(0, len(result), mem_bytes_per_element): + for j in range(0, len(result), mem_byte_count): plc_values.append( struct.unpack( - unpack_string, result[i : i + mem_bytes_per_element] + unpack_string, result[j : j + mem_byte_count] )[0] ) else: logging.error( "failed to read value from address %d", - mem_start_address + first_element, + start_address + first_element, ) - return False + return [] logging.debug("read a total of %d value(s)", len(plc_values)) if len(plc_values) != number_of_elements: - raise Exception( - "number of recived values %d is not equal to number of requested %d", - len(plc_values), - number_of_elements, + raise LSV2DataException( + "number of received values %d is not equal to number of requested %d" + % (len(plc_values), number_of_elements) ) return plc_values - def set_keyboard_access(self, unlocked): - """Enable or disable the keyboard on the control. Requires access level MONITOR to work. + def set_keyboard_access(self, unlocked: bool) -> bool: + """ + Enable or disable the keyboard on the control. + Requires access level ``MONITOR`` to work. + Returns ``True`` if completed successfully. - :param bool unlocked: if True unlocks the keyboard. if false, input is set to locked - :returns: True or False if command was executed successfully - :rtype: bool + :param unlocked: if ``True`` unlocks the keyboard so it can be used. If ``False``, input is set to locked """ - if not self.login(Login.MONITOR): - logging.error("clould not log in as user MONITOR") + if not self.login(lc.Login.MONITOR): + self._logger.warning("clould not log in as user MONITOR") return False payload = bytearray() @@ -1233,201 +1416,230 @@ def set_keyboard_access(self, unlocked): else: payload.extend(struct.pack("!B", 0x01)) - result = self._send_recive(CMD.C_LK, RSP.T_OK, payload=payload) + result = self._send_recive(lc.CMD.C_LK, payload, lc.RSP.T_OK) if result: if unlocked: - logging.debug("command to unlock keyboard was successful") + self._logger.debug("command to unlock keyboard was successful") else: - logging.debug("command to lock keyboard was successful") + self._logger.debug("command to lock keyboard was successful") return True - else: - logging.warning("an error occurred changing the state of the keyboard lock") + self._logger.warning( + "an error occurred changing the state of the keyboard lock" + ) return False - def get_machine_parameter(self, name): - """Read machine parameter from control. Requires access INSPECT level to work. + def get_machine_parameter(self, name: str) -> str: + """ + Read machine parameter from control. + Requires access level ``INSPECT`` level to work. - :param str name: name of the machine parameter. For iTNC the parameter number hase to be converted to string - :returns: value of parameter or False if command not successful - :rtype: str or bool + :param name: name of the machine parameter. """ - payload = bytearray() - payload.extend(map(ord, name)) - payload.append(0x00) - result = self._send_recive(CMD.R_MC, RSP.S_MC, payload=payload) - if result: - value = result.rstrip(b"\x00").decode("utf8") - logging.debug("machine parameter %s has value %s", name, value) - return value + if not self.login(lc.Login.INSPECT): + self._logger.warning("clould not log in as user INSPECT") + return "" - logging.warning("an error occurred while reading machine parameter %s", name) - return False + if isinstance(name, (int,)): + name = str(name) - def set_machine_parameter(self, name, value, safe_to_disk=False): - """Set machine parameter on control. Requires access PLCDEBUG level to work. - Writing a parameter takes some time, make sure to set timeout sufficiently high! + payload = lm.ustr_to_ba(name) - :param str name: name of the machine parameter. For iTNC the parameter number hase to be converted to string - :param str value: new value of the machine parameter. There is no type checking, if the value can not be converted by the control an error will be sent. - :param bool safe_to_disk: If True the new value will be written to the harddisk and stay permanent. If False (default) the value will only be available until the next reboot. + result = self._send_recive(lc.CMD.R_MC, payload, lc.RSP.S_MC) + if isinstance(result, (bytearray,)) and len(result) > 0: + value = lm.ba_to_ustr(result) + self._logger.debug("machine parameter %s has value %s", name, value) + return value - :returns: True or False if command was executed successfully - :rtype: bool + self._logger.warning( + "an error occurred while reading machine parameter %s", name + ) + return "" + + def set_machine_parameter( + self, name: str, value: str, safe_to_disk: bool = False + ) -> bool: + """ + Set machine parameter on control. Writing a parameter takes some time, make sure to set timeout + sufficiently high! + Requires access ``PLCDEBUG`` level to work. + Returns ``True`` if completed successfully. + + :param name: name of the machine parameter. For iTNC the parameter number hase to be converted to string + :param value: new value of the machine parameter. There is no type checking, if the value can not be + converted by the control an error will be sent. + :param safe_to_disk: If True the new value will be written to the harddisk and stay permanent. + If False (default) the value will only be available until the next reboot. """ + if not self.login(lc.Login.PLCDEBUG): + self._logger.warning("clould not log in as user PLCDEBUG") + return False + payload = bytearray() if safe_to_disk: payload.extend(struct.pack("!L", 0x00)) else: payload.extend(struct.pack("!L", 0x01)) - payload.extend(map(ord, name)) - payload.append(0x00) - payload.extend(map(ord, value)) - payload.append(0x00) + payload.extend(lm.ustr_to_ba(name)) + payload.extend(lm.ustr_to_ba(value)) - result = self._send_recive(CMD.C_MC, RSP.T_OK, payload=payload) + result = self._send_recive(lc.CMD.C_MC, payload, lc.RSP.T_OK) if result: - logging.debug( + self._logger.debug( "setting of machine parameter %s to value %s was successful", name, value, ) return True - logging.warning( + self._logger.warning( "an error occurred while setting machine parameter %s to value %s", name, value, ) return False - def send_key_code(self, key_code): - """Send key code to control. Behaves as if the associated key was pressed on the - keyboard. Requires access MONITOR level to work. To work correctly you first - have to lock the keyboard and unlock it afterwards!: + def send_key_code(self, key_code: Union[lc.KeyCode, lc.OldKeyCode]) -> bool: + """ + Send key code to control. Behaves as if the associated key was pressed on the keyboard. + Requires access ``MONITOR`` level to work. + To work correctly you first have to lock the keyboard and unlock it afterwards!: + + .. code-block:: python - set_keyboard_access(False) - send_key_code(KeyCode.CE) - set_keyboard_access(True) + set_keyboard_access(False) + send_key_code(KeyCode.CE) + set_keyboard_access(True) - :param int key_code: code number of the keyboard key + Returns ``True`` if completed successfully. - :returns: True or False if command was executed successfully - :rtype: bool + :param key_code: code number of the keyboard key """ - if not self.login(Login.MONITOR): - logging.error("clould not log in as user MONITOR") + if not self.login(lc.Login.MONITOR): + self._logger.warning("clould not log in as user MONITOR") return False payload = bytearray() payload.extend(struct.pack("!H", key_code)) - result = self._send_recive(CMD.C_EK, RSP.T_OK, payload=payload) + result = self._send_recive(lc.CMD.C_EK, payload, lc.RSP.T_OK) if result: - logging.debug("sending the key code %d was successful", key_code) + self._logger.debug("sending the key code %d was successful", key_code) return True - logging.warning("an error occurred while sending the key code %d", key_code) + self._logger.warning( + "an error occurred while sending the key code %d", key_code + ) return False - def get_spindle_tool_status(self): - """Get information about the tool currently in the spindle - - :returns: tool information or False if something went wrong - :rtype: dict + def spindle_tool_status(self) -> Union[ld.ToolInformation, None]: + """ + Get information about the tool currently in the spindle + Requires access level ``DNC`` to work. """ - self.login(login=Login.DNC) + if not self.login(lc.Login.DNC): + self._logger.warning("clould not log in as user DNC") + return None + payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.CURRENT_TOOL)) - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - if result: - tool_info = decode_tool_information(result) - logging.debug("successfully read info on current tool: %s", tool_info) + payload.extend(struct.pack("!H", lc.ParRRI.CURRENT_TOOL)) + + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)) and len(result) > 0: + tool_info = lm.decode_tool_info(result) + self._logger.debug("successfully read info on current tool: %s", tool_info) return tool_info - logging.warning( + self._logger.warning( "an error occurred while querying current tool information. This does not work for all control types" ) - return False - - def get_override_info(self): - """Get information about the override info + return None - :returns: override information or False if something went wrong - :rtype: dict + def override_state(self) -> Union[ld.OverrideState, None]: """ - self.login(login=Login.DNC) + Get information about the override info. + Requires access level ``DNC`` to work. + """ + if not self.login(lc.Login.DNC): + self._logger.warning("clould not log in as user DNC") + return None + payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.OVERRIDE)) - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - if result: - override_info = decode_override_information(result) - logging.debug("successfully read override info: %s", override_info) + payload.extend(struct.pack("!H", lc.ParRRI.OVERRIDE)) + + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)) and len(result) > 0: + override_info = lm.decode_override_state(result) + self._logger.debug("successfully read override info: %s", override_info) return override_info - logging.warning( + self._logger.warning( "an error occurred while querying current override information. This does not work for all control types" ) - return False - - def get_error_messages(self): - """Get information about the first or next error displayed on the control + return None - :param bool next_error: if True check if any further error messages are available - :returns: error information or False if something went wrong - :rtype: dict + def get_error_messages(self) -> List[ld.NCErrorMessage]: """ - messages = list() - self.login(login=Login.DNC) + Get information about the first or next error displayed on the control + Requires access level ``DNC`` to work. + Returns error list of error messages + """ + messages = [] + if not self.login(lc.Login.DNC): + self._logger.warning("clould not log in as user DNC") + return [] payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.FIRST_ERROR)) - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - if result: - messages.append(decode_error_message(result)) + payload.extend(struct.pack("!H", lc.ParRRI.FIRST_ERROR)) + + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)) and len(result) > 0: + messages.append(lm.decode_error_message(result)) payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.NEXT_ERROR)) - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - logging.debug("successfully read first error but further errors") + payload.extend(struct.pack("!H", lc.ParRRI.NEXT_ERROR)) + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + self._logger.debug("successfully read first error but further errors") - while result: - messages.append(decode_error_message(result)) - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) + while isinstance(result, (bytearray,)): + messages.append(lm.decode_error_message(result)) + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) - if self._last_error_code[1] == LSV2Err.T_ER_NO_NEXT_ERROR: - logging.debug("successfully read all errors") + if self.last_error is lc.LSV2StatusCode.T_ER_NO_NEXT_ERROR: + self._logger.debug("successfully read all errors") else: - logging.warning("an error occurred while querying error information.") + self._logger.warning( + "an error occurred while querying error information." + ) return messages - elif self._last_error_code[1] == LSV2Err.T_ER_NO_NEXT_ERROR: - logging.debug("successfully read first error but no error active") + if self.last_error is lc.LSV2StatusCode.T_ER_NO_NEXT_ERROR: + self._logger.debug("successfully read first error but no error active") return messages - logging.warning( + self._logger.warning( "an error occurred while querying error information. This does not work for all control types" ) - return False + return [] - def _walk_dir(self, descend=True): - """helber function to recursively search in directories for files + def _walk_dir(self, descend: bool = True) -> List[str]: + """ + helper function to recursively search in directories for files. + Requires access level ``FILETRANSFER`` to work. - :param bool descend: control if search should run recursively - :returns: list of files found in directory - :rtype: list + :param descend: control if search should run recursively """ - current_path = self.get_directory_info()["Path"] - content = list() - for entry in self.get_directory_content(): - if ( - entry["Name"] == "." - or entry["Name"] == ".." - or entry["Name"].endswith(":") - ): + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("clould not log in as user FILE") + return [] + + current_path = self.directory_info().path + content = [] + for entry in self.directory_content(): + if entry.name == "." or entry.name == ".." or entry.name.endswith(":"): continue - current_fs_element = str(current_path + entry["Name"]).replace( - "/", PATH_SEP + current_fs_element = str(current_path + entry.name).replace( + "/", lc.PATH_SEP ) - if entry["is_directory"] is True and descend is True: + if entry.is_directory is True and descend is True: if self.change_directory(current_fs_element): content.extend(self._walk_dir()) else: @@ -1435,57 +1647,70 @@ def _walk_dir(self, descend=True): self.change_directory(current_path) return content - def get_file_list(self, path=None, descend=True, pattern=None): - """Get list of files in directory structure. + def get_file_list( + self, path: str = "", descend: bool = True, pattern: str = "" + ) -> List[str]: + """ + Get list of files in directory structure. + Requires access level ``FILETRANSFER`` to work. - :param str path: path of the directory where files should be searched. if None than the current directory is used - :param bool descend: control if search should run recursiv - :param str pattern: regex string to filter the file names - :returns: list of files found in directory - :rtype: list + :param path: path of the directory where files should be searched. if None than the current directory is used + :param descend: control if search should run recursively + :param pattern: regex string to filter the file names """ + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("clould not log in as user FILE") + return [] + if path is not None: if self.change_directory(path) is False: - logging.warning("could not change to directory") - return None + self._logger.warning("could not change to directory") + return [] - if pattern is None: + if len(pattern) == 0: file_list = self._walk_dir(descend) else: - file_list = list() + file_list = [] for entry in self._walk_dir(descend): - file_name = entry.split(PATH_SEP)[-1] + file_name = entry.split(lc.PATH_SEP)[-1] if re.match(pattern, file_name): file_list.append(entry) return file_list - def read_data_path(self, path): - """Read values from control via data path. Only works on iTNC controls. + def read_data_path(self, path: str) -> Union[bool, int, float, str, None]: + """ + Read values from control via data path. Only works on iTNC controls. For ease of use, the path is formatted by replacing / by \\ and " by '. + Returns data value read from control formatted in nativ data type or None if reading + was not successful. + Requires access level ``DATA`` to work. + + :param path: data path from which to read the value. - :param str path: data path from which to read the value. - :returns: data value read from control formatted in nativ data type or None if reading was not successful + :raises LSV2ProtocolException: if data type could not be determiend """ - if not self.is_itnc(): - logging.warning( + if not self.versions.is_itnc(): + self._logger.warning( "Reading values from data path does not work on non iTNC controls!" ) + return None - path = path.replace("/", PATH_SEP).replace('"', "'") + path = path.replace("/", lc.PATH_SEP).replace('"', "'") - self.login(login=Login.DATA) + if not self.login(lc.Login.DATA): + self._logger.warning("clould not log in as user DATA") + return None payload = bytearray() payload.extend(b"\x00") # <- ??? payload.extend(b"\x00") # <- ??? payload.extend(b"\x00") # <- ??? payload.extend(b"\x00") # <- ??? - payload.extend(map(ord, path)) - payload.append(0x00) # escape string + payload.extend(lm.ustr_to_ba(path)) - result = self._send_recive(CMD.R_DP, RSP.S_DP, payload) + result = self._send_recive(lc.CMD.R_DP, payload, lc.RSP.S_DP) - if result: + if isinstance(result, (bytearray,)) and len(result) > 0: value_type = struct.unpack("!L", result[0:4])[0] if value_type == 2: data_value = struct.unpack("!h", result[4:6])[0] @@ -1494,7 +1719,7 @@ def read_data_path(self, path): elif value_type == 5: data_value = struct.unpack(" Union[Dict[str, float], None]: + """ + Read axes location from control. Not fully documented, value of first byte unknown. + Requires access level ``DNC`` to work. + Returns ``None`` if no data was received or dictionary with key = axis name, value = position - :returns: dictionary of axis label and current value + :raises LSV2DataException: Error during parsing of data values """ - self.login(login=Login.DNC) + if not self.login(lc.Login.DNC): + self._logger.warning("clould not log in as user DNC") + return None payload = bytearray() - payload.extend(struct.pack("!H", ParRRI.AXIS_LOCATION)) - - result = self._send_recive(CMD.R_RI, RSP.S_RI, payload) - if result: - # unknown = result[0:1] # <- ??? - number_of_axes = struct.unpack("!b", result[1:2])[0] - - split_list = list() - beginning = 2 - for i, byte in enumerate(result[beginning:]): - if byte == 0x00: - value = result[beginning : i + 3] - split_list.append(value.strip(b"\x00").decode("utf-8")) - beginning = i + 3 - - if len(split_list) != (2 * number_of_axes): - raise Exception("error parsing axis values") - - axes_values = dict() - for i in range(number_of_axes): - axes_values[split_list[i + number_of_axes]] = float(split_list[i]) - - logging.info("successfully read axes values: %s", axes_values) + payload.extend(struct.pack("!H", lc.ParRRI.AXIS_LOCATION)) + result = self._send_recive(lc.CMD.R_RI, payload, lc.RSP.S_RI) + if isinstance(result, (bytearray,)) and len(result) > 0: + axes_values = lm.decode_axis_location(result) + self._logger.info("successfully read axes values: %s", axes_values) return axes_values - logging.error("an error occurred while querying axes position") - return False + self._logger.warning("an error occurred while querying axes position") + return None - def grab_screen_dump(self, image_path: Path): - """create screen_dump of current control screen and save it as bitmap""" - if not self.login(Login.FILETRANSFER): - logging.error("clould not log in as user FILE") + def grab_screen_dump(self, image_path: pathlib.Path) -> bool: + """ + Create screen_dump of current control screen and save it as bitmap. + Requires access level ``DNC`` to work. + Returns ``True`` if completed successfully. + + :param image_path: path of bmp file for screendump + """ + if not self.login(lc.Login.FILETRANSFER): + self._logger.warning("clould not log in as user FILE") return False temp_file_path = ( - DriveName.TNC - + PATH_SEP + lc.DriveName.TNC + + lc.PATH_SEP + "screendump_" + datetime.now().strftime("%Y%m%d_%H%M%S") + ".bmp" ) - if not self.set_system_command(ParCCC.SCREENDUMP, temp_file_path): - logging.error("screen dump was not created") + payload = bytearray(struct.pack("!H", lc.ParCCC.SCREENDUMP)) + payload.extend(lm.ustr_to_ba(temp_file_path)) + + result = self._send_recive(lc.CMD.C_CC, payload, lc.RSP.T_OK) + + if not (isinstance(result, (bool,)) and result is True): + self._logger.warning("screen dump was not created") return False if not self.recive_file( remote_path=temp_file_path, local_path=image_path, binary_mode=True ): - logging.error("could not download screen dump from control") + self._logger.warning("could not download screen dump from control") return False if not self.delete_file(temp_file_path): - logging.error("clould not delete temporary file on control") + self._logger.warning("clould not delete temporary file on control") return False - logging.debug("successfully recived screen dump") + self._logger.debug("successfully received screen dump") return True diff --git a/pyLSV2/const.py b/pyLSV2/const.py index 29ed27b..3c28b91 100644 --- a/pyLSV2/const.py +++ b/pyLSV2/const.py @@ -3,15 +3,9 @@ """Constant values used in LSV2""" from enum import Enum, IntEnum -# #: files system attributes -# FS_ENTRY_IS_HIDDEN = 0x08 -# FS_ENTRY_IS_DRIVE = 0x10 -# FS_ENTRY_IS_DIRECTORY = 0x20 -# FS_ENTRY_IS_PROTCTED = 0x40 -# FS_ENTRY_IS_IN_USE = 0x80 - -#: enable binary file transfer for C_FL and R_FL +#: enable/disable binary file transfer for C_FL and R_FL MODE_BINARY = 0x01 +MODE_NON_BIN = 0x00 PATH_SEP = "\\" @@ -194,7 +188,7 @@ class MemoryType(IntEnum): OUTPUT_WORD = 11 -class LSV2Err(IntEnum): +class LSV2StatusCode(IntEnum): """Enum for LSV2 protocol error numbers range 0 - 19: protocol or transmission errors range 20 - 99: telegram errors @@ -203,7 +197,7 @@ class LSV2Err(IntEnum): LSV2_OK = 0 - # reciving + # receiving LSV2_TIMEOUT = 1 LSV2_NO_ENQ = 2 LSV2_TIMEOUT2 = 3 @@ -316,6 +310,9 @@ class LSV2Err(IntEnum): # plain error message (TODO see global var usererrortext) T_USER_ERROR = 255 + T_ER_NON = -1 + """not an valid error code. devault value if no error occurred""" + class KeyCode(IntEnum): """Keycodes""" @@ -454,7 +451,7 @@ class KeyCode(IntEnum): PROG_TOOL_CALL = 0x01D2 PROG_CYC_DEF = 0x01D3 PROG_CYC_CALL = 0x01D4 - PROG_CYC_CAL = PROG_CYC_CALL # TODO typo + # PROG_CYC_CAL = PROG_CYC_CALL PROG_LBL = 0x01D5 PROG_LBL_CALL = 0x01D6 PROG_L = 0x01D7 @@ -593,7 +590,7 @@ class OldKeyCode(IntEnum): AXIS_X = 0x006D TOGGEL_POLAR = 0x0043 - PROG_TOUCH_PROBE = 0x004E # TODO + PROG_TOUCH_PROBE = 0x004E PROG_RR = 0x0057 PROG_RL = 0x0056 PROG_LBL_CALL = 0x005E @@ -618,8 +615,8 @@ class OldKeyCode(IntEnum): PGMMGT = 0x0061 TOGGEL_INC = 0x0044 - Cl_Pgm = 0x0062 # TODO - Pgm_Nr = 0x003B # TODO + CL_PGM = 0x0062 + PGM_NR = 0x003B ARROW_LEFT = 0x0059 ARROW_DOWN = 0x0067 @@ -653,7 +650,7 @@ class CMD(str, Enum): requires no login priviliege""" A_LO = "A_LO" - """A_LO: used to drop access to certain parts of the control, followed by an optional logon name. + """A_LO: used to drop access to certain parts of the control, followed by an optional logon name. requires any login priviliege""" C_CC = "C_CC" @@ -711,7 +708,7 @@ class CMD(str, Enum): C_MC = "C_MC" """C_MC: set machine parameter, followed by flags, name and value - """ + requires PLCDEBUG login privilege""" # C_OP = 'C_OP' # found via bruteforce test, purpose unknown! -> Timeout @@ -780,7 +777,7 @@ class CMD(str, Enum): # R_OI = "R_OI" # found via bruteforce test, purpose unknown! R_PD = "R_PD" - """request palet definiton. + """request palet definition. requires FILE or MONITOR login priviliege""" R_PR = "R_PR" @@ -823,9 +820,12 @@ class CMD(str, Enum): requires INSPECT login priviliege""" R_WD = "R_WD" - """request window definiton. + """request window definition. requires MONITOR login priviliege""" + NONE = "NONE" + """not a valid command but used internally""" + class RSP(str, Enum): """Enum of all known response telegrams""" @@ -881,6 +881,12 @@ class RSP(str, Enum): S_VR = "S_VR" """S_VR: signals that the command R_VR was accepted, it is followed by more data""" + NONE = "NONE" + """not a valid response but used internally to signal that no response was received or should be sent""" + + UNKNOWN = "UNKN" + """not a valid response but used internally to signal an unknown response was received""" + class ParCCC(IntEnum): """enum for telegram C_CC / SetSysCmd""" diff --git a/pyLSV2/dat_cls.py b/pyLSV2/dat_cls.py new file mode 100644 index 0000000..692f3e5 --- /dev/null +++ b/pyLSV2/dat_cls.py @@ -0,0 +1,926 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +data classes for pyLSV2 + +after migration to python 3.7+ these will be changed to @dataclass! +""" + +from datetime import datetime +import struct + +from .const import ControlType, LSV2StatusCode + + +class VersionInfo: + """data class for version information""" + + def __init__(self): + """init with default values""" + self.control = "" + self.type = ControlType.UNKNOWN + self.nc_sw = "" + self.plc = "" + self.splc = "" + self.option_bits = "" + self.id_number = "" + self.release = "" + + def __str__(self) -> str: + return "%s / %s" % (self.control, self.nc_sw) + + @property + def control(self) -> str: + """ + version identifyer of the control + + :getter: returns the version string + :setter: sets the version string + """ + return self._control_version + + @control.setter + def control(self, value: str): + value = value.replace(" ", "").strip() + self._control_version = value + + value = value.upper() + + if value.startswith("ITNC530"): + self.type = ControlType.MILL_OLD + elif value.startswith("TNC640"): + self.type = ControlType.MILL_NEW + elif value.startswith("TNC620"): + self.type = ControlType.MILL_NEW + elif value.startswith("TNC320"): + self.type = ControlType.MILL_NEW + elif value.startswith("TNC128"): + self.type = ControlType.MILL_NEW + elif value.startswith("CNCPILOT640"): + self.type = ControlType.LATHE_NEW + else: + self.type = ControlType.UNKNOWN + + @property + def type(self) -> ControlType: + """ + control type identifyer of the control + + :getter: returns the control type + :setter: sets the control type + """ + return self._control_type + + @type.setter + def type(self, value: ControlType): + self._control_type = value + + @property + def nc_sw(self) -> str: + """version identifier of the nc software""" + return self._nc_version + + @nc_sw.setter + def nc_sw(self, value: str): + self._nc_version = value + + @property + def plc(self) -> str: + """version identifier of the plc software""" + return self._plc_version + + @plc.setter + def plc(self, value: str): + self._plc_version = value + + @property + def splc(self) -> str: + """version identifier of the splc software""" + return self._splc_version + + @splc.setter + def splc(self, value: str): + self._splc_version = value + + @property + def option_bits(self) -> str: + """available options in the control""" + return self._option_bits + + @option_bits.setter + def option_bits(self, value: str): + self._option_bits = value + + @property + def id_number(self) -> str: + """id of the control""" + return self._id_number + + @id_number.setter + def id_number(self, value: str): + self._id_number = value + + @property + def release(self) -> str: + """release type ???""" + return self._release_type + + @release.setter + def release(self, value: str): + self._release_type = value + + def is_itnc(self) -> bool: + """return ``True`` if control is a iTNC""" + return self._control_type == ControlType.MILL_OLD + + def is_tnc(self) -> bool: + """return ``True`` if control is a TNC""" + return self._control_type == ControlType.MILL_NEW + + def is_pilot(self) -> bool: + """return ``True`` if control is a CNCPILOT640""" + return self._control_type == ControlType.LATHE_NEW + + +class SystemParameters: + """data class for system parameters""" + + def __init__(self): + """init with default values""" + self.inputs_start_address = -1 + self.number_of_inputs = -1 + self.outputs_start_address = -1 + self.number_of_outputs = -1 + + self.counters_start_address = -1 + self.number_of_counters = -1 + + self.timers_start_address = -1 + self.number_of_timers = -1 + + self.words_start_address = -1 + self.umber_of_words = -1 + + self.strings_start_address = -1 + self.number_of_strings = -1 + self.max_string_lenght = -1 + + self.input_words_start_address = -1 + self.number_of_input_words = -1 + + self.output_words_start_address = -1 + self.number_of_output_words = -1 + + self.lsv2_version = -1 + self.lsv2_version_flags = -1 + self.sv2_version_flags_ex = -1 + + self.max_block_length = -1 + + self.bin_version = -1 + self.bin_revision = -1 + self.iso_version = -1 + self.iso_revision = -1 + + self.hardware_version = -1 + + self.max_trace_line = -1 + self.number_of_scope_channels = -1 + + self.password_encryption_key = -1 + + @property + def markers_start_address(self) -> int: + """memory start address for markers""" + return self._markers_start_address + + @markers_start_address.setter + def markers_start_address(self, value: int): + self._markers_start_address = value + + @property + def number_of_markers(self) -> int: + """total number of markers""" + return self._number_of_markers + + @number_of_markers.setter + def number_of_markers(self, value: int): + self._number_of_markers = value + + @property + def inputs_start_address(self) -> int: + """memory start address for inputs""" + return self._inputs_start_address + + @inputs_start_address.setter + def inputs_start_address(self, value: int): + self._inputs_start_address = value + + @property + def number_of_inputs(self) -> int: + """total number of inputs""" + return self._number_of_inputs + + @number_of_inputs.setter + def number_of_inputs(self, value: int): + self._number_of_inputs = value + + @property + def outputs_start_address(self) -> int: + """memory start address for outputs""" + return self._outputs_start_address + + @outputs_start_address.setter + def outputs_start_address(self, value: int): + self._outputs_start_address = value + + @property + def number_of_outputs(self) -> int: + """total number of outputs""" + return self._number_of_outputs + + @number_of_outputs.setter + def number_of_outputs(self, value: int): + self._number_of_outputs = value + + @property + def counters_start_address(self) -> int: + """memory start address for counters""" + return self._counters_start_address + + @counters_start_address.setter + def counters_start_address(self, value: int): + self._counters_start_address = value + + @property + def number_of_counters(self) -> int: + """total number of counters""" + return self._number_of_counters + + @number_of_counters.setter + def number_of_counters(self, value: int): + self._number_of_counters = value + + @property + def timers_start_address(self) -> int: + """memory start address for timers""" + return self._timers_start_address + + @timers_start_address.setter + def timers_start_address(self, value: int): + self._timers_start_address = value + + @property + def number_of_timers(self) -> int: + """total number of timers""" + return self._number_of_timers + + @number_of_timers.setter + def number_of_timers(self, value: int): + self._number_of_timers = value + + @property + def words_start_address(self) -> int: + """memory start address for words""" + return self._words_start_address + + @words_start_address.setter + def words_start_address(self, value: int): + self._words_start_address = value + + @property + def number_of_words(self) -> int: + """total number of words""" + return self._number_of_words + + @number_of_words.setter + def number_of_words(self, value: int): + self._number_of_words = value + + @property + def strings_start_address(self) -> int: + """memory start address for strings""" + return self._strings_start_address + + @strings_start_address.setter + def strings_start_address(self, value: int): + self._strings_start_address = value + + @property + def number_of_strings(self) -> int: + """total number of strings""" + return self._number_of_strings + + @number_of_strings.setter + def number_of_strings(self, value: int): + self._number_of_strings = value + + @property + def max_string_lenght(self) -> int: + """maximum number of bytes in a string""" + return self._max_string_lenght + + @max_string_lenght.setter + def max_string_lenght(self, value: int): + self._max_string_lenght = value + + @property + def input_words_start_address(self) -> int: + """memory start address for input words""" + return self._input_words_start_address + + @input_words_start_address.setter + def input_words_start_address(self, value: int): + self._input_words_start_address = value + + @property + def number_of_input_words(self) -> int: + """total number of input words""" + return self._number_of_input_words + + @number_of_input_words.setter + def number_of_input_words(self, value: int): + self._number_of_input_words = value + + @property + def output_words_start_address(self) -> int: + """memory start address for output words""" + return self._output_words_start_address + + @output_words_start_address.setter + def output_words_start_address(self, value: int): + self._output_words_start_address = value + + @property + def number_of_output_words(self) -> int: + """total number of outpu words""" + return self._number_of_output_words + + @number_of_output_words.setter + def number_of_output_words(self, value: int): + self._number_of_output_words = value + + @property + def lsv2_version(self) -> int: + """version of the LSV2 protocol used""" + return self._lsv2_version + + @lsv2_version.setter + def lsv2_version(self, value: int): + self._lsv2_version = value + + @property + def lsv2_version_flags(self) -> int: + """feature flags used by this version of LSV2""" + return self._lsv2_version_flags + + @lsv2_version_flags.setter + def lsv2_version_flags(self, value: int): + self._lsv2_version_flags = value + + @property + def lsv2_version_flags_ex(self) -> int: + """additional feature flags used by this version of LSV2""" + return self._lsv2_version_flags_ex + + @lsv2_version_flags_ex.setter + def lsv2_version_flags_ex(self, value: int): + self._lsv2_version_flags_ex = value + + @property + def max_block_length(self) -> int: + """maximal number of bytes that can be sent and received by the control""" + return self._max_block_length + + @max_block_length.setter + def max_block_length(self, value: int): + self._max_block_length = value + + @property + def bin_version(self) -> int: + """bin version ???""" + return self._bin_version + + @bin_version.setter + def bin_version(self, value: int): + self._bin_version = value + + @property + def bin_revision(self) -> int: + """bin revisiion ???""" + return self._bin_revision + + @bin_revision.setter + def bin_revision(self, value: int): + self._bin_revision = value + + @property + def iso_version(self) -> int: + """iso revisiion ???""" + return self._iso_version + + @iso_version.setter + def iso_version(self, value: int): + self._iso_version = value + + @property + def iso_revision(self) -> int: + """iso revisiion ???""" + return self._iso_revision + + @iso_revision.setter + def iso_revision(self, value: int): + self._iso_revision = value + + @property + def hardware_version(self) -> int: + """hardware revisiion ???""" + return self._hardware_version + + @hardware_version.setter + def hardware_version(self, value: int): + self._hardware_version = value + + @property + def max_trace_line(self) -> int: + """maximum number of trace lines??""" + return self._max_trace_line + + @max_trace_line.setter + def max_trace_line(self, value: int): + self._max_trace_line = value + + @property + def number_of_scope_channels(self) -> int: + """number of channels used by the scope""" + return self._number_of_scope_channels + + @number_of_scope_channels.setter + def number_of_scope_channels(self, value: int): + self._number_of_scope_channels = value + + @property + def password_encryption_key(self) -> int: + """???""" + return self._password_encryption_key + + @password_encryption_key.setter + def password_encryption_key(self, value: int): + self._password_encryption_key = value + + +class ToolInformation: + """data class for information about a tool""" + + def __init__(self): + """init with default values""" + self.number = -1 + self.index = -1 + self.axis = "" + self.length = -1 + self.radius = -1 + self.name = "" + + @property + def number(self) -> int: + """tool number""" + return self._number + + @number.setter + def number(self, value: int): + self._number = value + + @property + def index(self) -> int: + """index number""" + return self._index + + @index.setter + def index(self, value: int): + self._index = value + + @property + def axis(self) -> str: + """tool/spindle axis""" + return self._axis + + @axis.setter + def axis(self, value: str): + self._axis = value + + @property + def length(self) -> float: + """tool length""" + return self._length + + @length.setter + def length(self, value: float): + self._length = value + + @property + def radius(self) -> float: + """tool radius""" + return self._radius + + @radius.setter + def radius(self, value: float): + self._radius = value + + @property + def name(self) -> str: + """tool name""" + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + +class OverrideState: + """data class for the override states""" + + def __init__(self): + """init with default values""" + self.feed = -1.0 + self.rapid = -1.0 + self.spindle = -1.0 + + def __str__(self) -> str: + return "F:%3.1f%% FMAX:%3.1f%% S:%3.1f%%" % ( + self.feed, + self.rapid, + self.spindle, + ) + + @property + def feed(self) -> float: + """override value for feed rate""" + return self._feed + + @feed.setter + def feed(self, value: float): + self._feed = value + + @property + def rapid(self) -> float: + """override value for rapid feed""" + return self._rapid + + @rapid.setter + def rapid(self, value: float): + self._rapid = value + + @property + def spindle(self) -> float: + """override value for spindle speed""" + return self._spindel + + @spindle.setter + def spindle(self, value: float): + self._spindel = value + + +class NCErrorMessage: + """data class for nc error messages + LSV2 error messages are handled with""" + + def __init__(self): + """ + init with default values, + names were chosen so not to interfere with existing names + """ + self.e_class = -1 + self.e_group = -1 + self.e_number = -1 + self.e_text = "" + self.dnc = False + + def __str__(self) -> str: + return "Class: %d, Group: %d, Number: %d, DNC?: %s, Text: '%s'" % ( + self.e_class, + self.e_group, + self.e_number, + self.dnc, + self.e_text, + ) + + @property + def e_class(self) -> int: + """class of the error message""" + return self._e_class + + @e_class.setter + def e_class(self, value: int): + self._e_class = value + + @property + def e_group(self) -> int: + """group of the error message""" + return self._e_group + + @e_group.setter + def e_group(self, value: int): + self._e_group = value + + @property + def e_number(self) -> int: + """number of the error message""" + return self._e_number + + @e_number.setter + def e_number(self, value: int): + self._e_number = value + + @property + def e_text(self) -> str: + """error message""" + return self._e_text + + @e_text.setter + def e_text(self, value: str): + self._e_text = value + + @property + def dnc(self) -> bool: + """if True, message is related to DNC?""" + return self._dnc + + @dnc.setter + def dnc(self, value: bool): + self._dnc = value + + +class StackState: + """data class for the current execution stack""" + + def __init__(self): + """init with default values""" + self.line_no = -1 + self.main = "" + self.current = "" + + def __str__(self) -> str: + return "Main '%s' Current '%s' @ Line %d" % ( + self.main, + self.current, + self.line_no, + ) + + @property + def line_no(self) -> int: + """current line number being executed""" + return self._current_line + + @line_no.setter + def line_no(self, value: int): + self._current_line = value + + @property + def main(self) -> str: + """name of the current main program""" + return self._main_pgm + + @main.setter + def main(self, value: str): + self._main_pgm = value + + @property + def current(self) -> str: + """name of the current program being executed""" + return self._current_pgm + + @current.setter + def current(self, value: str): + self._current_pgm = value + + +class FileEntry: + """data class for file information""" + + def __init__(self): + """init with default values""" + self.size = -1 + self.timestamp = datetime.fromtimestamp(0) + self.attributes = bytearray() + + self.is_changable = False + self.is_drive = False + self.is_directory = False + self.is_protected = False + self.is_hidden = False + self.is_selected = False + + self.name = "" + + @property + def size(self) -> int: + """file size in bytes""" + return self._size + + @size.setter + def size(self, value: int): + self._size = value + + @property + def timestamp(self) -> datetime: + """timestamp of last file change""" + return self._timestamp + + @timestamp.setter + def timestamp(self, value: datetime): + self._timestamp = value + + @property + def attributes(self) -> bytearray: + """byte array of file attributes""" + return self._attributes + + @attributes.setter + def attributes(self, value: bytearray): + self._attributes = value + + @property + def is_protected(self) -> bool: + """if True, file is write protected""" + return self._is_protected + + @is_protected.setter + def is_protected(self, value: bool): + self._is_protected = value + + @property + def is_drive(self) -> bool: + """if True, entry describes a drive""" + return self._is_drive + + @is_drive.setter + def is_drive(self, value: bool): + self._is_drive = value + + @property + def is_directory(self) -> bool: + """if True, entry describes a directory""" + return self._is_directory + + @is_directory.setter + def is_directory(self, value: bool): + self._is_directory = value + + @property + def is_hidden(self) -> bool: + """if True, object is hidden in file browser""" + return self._is_hidden + + @is_hidden.setter + def is_hidden(self, value: bool): + self._is_hidden = value + + @property + def is_selected(self) -> bool: + """if True, object is selected for execution""" + return self._is_selected + + @is_selected.setter + def is_selected(self, value: bool): + self._is_selected = value + + @property + def name(self) -> str: + """name of the file system object""" + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + +class DirectoryEntry: + """data class for directory information""" + + def __init__(self): + """init with default values""" + self.free_size = -1 + self.dir_attributes = [] + self.attributes = bytearray() + self.path = "" + + @property + def free_size(self) -> int: + """number of free bytes""" + return self._free_size + + @free_size.setter + def free_size(self, value: int): + self._free_size = value + + @property + def dir_attributes(self) -> list: + """attriutes of this directory""" + return self._dir_attributes + + @dir_attributes.setter + def dir_attributes(self, value: list): + self._dir_attributes = value + + @property + def attributes(self) -> bytearray: + """bytes of unknown data""" + return self._attributes + + @attributes.setter + def attributes(self, value: bytearray): + self._attributes = value + + @property + def path(self) -> str: + """path of this directory""" + return self._path + + @path.setter + def path(self, value: str): + self._path = value + + +class DriveEntry: + """data class for drive information""" + + def __init__(self): + """init with default values""" + self.unknown_0 = -1 + self.unknown_1 = "" + self.unknown_2 = -1 + self.name = "" + + @property + def unknown_0(self) -> int: + """unknown numerical value""" + return self._unknown_0 + + @unknown_0.setter + def unknown_0(self, value: int): + self._unknown_0 = value + + @property + def unknown_1(self) -> str: + """unknown string value""" + return self._unknown_1 + + @unknown_1.setter + def unknown_1(self, value: str): + self._unknown_1 = value + + @property + def unknown_2(self) -> int: + """unknown numerical value""" + return self._unknown_2 + + @unknown_2.setter + def unknown_2(self, value: int): + self._unknown_2 = value + + @property + def name(self) -> str: + """name of the drive""" + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + +class LSV2Error: + """data class for LSV2 errors""" + + def __init__(self): + """init with default values""" + self.e_type = -1 + self.e_code = LSV2StatusCode.T_ER_NON + + def __str__(self): + return "Error Type: %d, Error Code: %d" % (self.e_type, self.e_code) + + @property + def e_type(self) -> int: + """error type""" + return self._error_type + + @e_type.setter + def e_type(self, value: int): + self._error_type = value + + @property + def e_code(self) -> LSV2StatusCode: + """error code""" + return self._error_code + + @e_code.setter + def e_code(self, value: LSV2StatusCode): + self._error_code = value + + @staticmethod + def from_ba(err_bytes: bytearray): + """convert byte array to error object""" + err = LSV2Error() + err.e_type = struct.unpack("!BB", err_bytes)[0] + err.e_code = struct.unpack("!BB", err_bytes)[1] + return err diff --git a/pyLSV2/err.py b/pyLSV2/err.py new file mode 100644 index 0000000..12c12ea --- /dev/null +++ b/pyLSV2/err.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Exception for pyLSV2""" + + +class LSV2StateException(Exception): + """raised for unknown or inconsistent states""" + + +class LSV2DataException(Exception): + """raised if received data could not be parsed""" + + +class LSV2InputException(Exception): + """raised if input data could not be parsed""" + + +class LSV2ProtocolException(Exception): + """raised when an unexpected response is received""" diff --git a/pyLSV2/low_level_com.py b/pyLSV2/low_level_com.py index b32730c..4a43aee 100644 --- a/pyLSV2/low_level_com.py +++ b/pyLSV2/low_level_com.py @@ -4,162 +4,322 @@ import logging import socket import struct +from typing import Union +from .const import CMD, RSP +from .dat_cls import LSV2Error +from .err import LSV2StateException, LSV2ProtocolException -class LLLSV2Com: + +class LSV2TCP: """Implementation of the low level communication functions for sending and receiving LSV2 telegrams via TCP""" - DEFAULT_PORT = 19000 # Default port for LSV2 on control side + DEFAULT_PORT = 19000 + # Default port for LSV2 on control side + DEFAULT_BUFFER_SIZE = 256 + # Default size of send and receive buffer - def __init__(self, hostname, port=19000, timeout=15.0): + def __init__(self, hostname: str, port: int = 19000, timeout: float = 15.0): """Set connection parameters - :param str hostname: ip or hostname of control. - :param int port: port number, defaults to 19000. - :param float timeout: number of seconds for time out of connection. + :param hostname: ip or hostname of control. + :param port: port number, defaults to 19000. + :param timeout: number of seconds for time out of connection. + + :raises socket.gaierror: Hostname could not be resolved + :raises socket.error: could not create socket """ + self._logger = logging.getLogger("LSV2 TCP") + try: self._host_ip = socket.gethostbyname(hostname) except socket.gaierror: - logging.error("there was an error resolving the host") + logging.error( + "there was an error getting the IP for the hostname %s", hostname + ) raise - self._port = port + self._port = self.DEFAULT_PORT + if port > 0: + self._port = port + + self.buffer_size = LSV2TCP.DEFAULT_BUFFER_SIZE try: self._tcpsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._tcpsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self._tcpsock.settimeout(timeout) except socket.error as err: - logging.error("socket creation failed with error %s", err) + self._logger.error("socket creation failed with error %s", err) raise + self._is_connected = False + self._last_lsv2_response = RSP.NONE + self._last_error = LSV2Error() - logging.debug( + self._logger.debug( "Socket successfully created, host %s was resolved to IP %s", hostname, self._host_ip, ) + @property + def last_response(self) -> RSP: + """get the response to the last telegram""" + return self._last_lsv2_response + + @property + def last_error(self) -> LSV2Error: + """get the error if the last telegram failed""" + return self._last_error + + @property + def buffer_size(self) -> int: + """size of the buffer used for sending and receiving data. + has to be negotiated with the control""" + return self._buffer_size + + @buffer_size.setter + def buffer_size(self, value: int): + if value < 8: + self._buffer_size = self.DEFAULT_BUFFER_SIZE + self._logger.warning( + "size of receive buffer to small, set to default of %d bytes", + self._buffer_size, + ) + else: + self._buffer_size = value + def connect(self): - """Establish connection to control + """ + Establish connection to control - :raise: Exception if connection times out. - :rtype: None + :raise socket.timeout: Exception if connection times out. """ try: self._tcpsock.connect((self._host_ip, self._port)) except socket.timeout: - logging.error("could not connect to control") + self._logger.error( + "could not connect to address '%s' on port %d", + self._host_ip, + self._port, + ) + raise + except ConnectionRefusedError: + self._logger.error( + "connection to address '%s' on port %d was refused", + self._host_ip, + self._port, + ) raise + self._is_connected = True - logging.debug("Connected to host %s at port %s", self._host_ip, self._port) + self._last_lsv2_response = RSP.NONE + self._last_error = LSV2Error() + + self._logger.debug("Connected to host %s at port %s", self._host_ip, self._port) def disconnect(self): - """Close connection + """ + Close connection - :raise: Exception if connection times out. - :rtype: None + :raise socket.timeout: Exception if connection times out. """ try: if self._tcpsock is not None: self._tcpsock.close() except socket.timeout: - logging.error("error while closing socket") + self._logger.error("error while closing socket") raise + self._is_connected = False - logging.debug("Connection to %s closed", self._host_ip) - - def telegram(self, command, payload=None, buffer_size=0, wait_for_response=True): - """Send LSV2 telegram and receive response if necessary. - - :param str command: command string. - :param byte array payload: command payload. - :param int buffer_size: size of message buffer used by control. - :param bool wait_for_response: switch for waiting for response from control. - :raise: Exception if connection is not already open or error during transmission. - :return: response message and content - :rtype: list + self._last_lsv2_response = RSP.NONE + self._last_error = LSV2Error() + + self._logger.debug("Connection to %s closed", self._host_ip) + + def telegram( + self, + command: Union[CMD, RSP], + payload: bytearray = bytearray(), + wait_for_response: bool = True, + ) -> bytearray: + """ + Send LSV2 telegram and receive response if necessary. + + :param command: command string + :param payload: command payload + :param wait_for_response: switch for waiting for response from control. + :raise LSV2StateException: if connection is not already open or error during transmission. + :raise OverflowError: if payload is to long for current buffer size + :raise LSV2ProtocolException: if the reviced response is too short for a minimal telegram + :raise Exception: """ if self._is_connected is False: - raise Exception("connection is not open!") + raise LSV2StateException("connection is not open!") - if payload is None: + if payload is None or len(payload) == 0: + payload = bytearray() payload_length = 0 else: payload_length = len(payload) - if buffer_size < 8: - buffer_size = LLLSV2Com.DEFAULT_BUFFER_SIZE - logging.warning( - "size of receive buffer to small, set to default of %d bytes", - LLLSV2Com.DEFAULT_BUFFER_SIZE, - ) + self._last_lsv2_response = RSP.NONE telegram = bytearray() # L -> unsigned long -> 32 bit telegram.extend(struct.pack("!L", payload_length)) telegram.extend(map(ord, command)) - if payload is not None: + + if len(payload) > 0: telegram.extend(payload) - logging.debug( + self._logger.debug( "telegram to transmit: command %s payload length %d bytes data: %s", command, payload_length, telegram, ) - if len(telegram) >= buffer_size: + if len(telegram) >= self.buffer_size: raise OverflowError( "telegram to long for set current buffer size: %d >= %d" - % (len(telegram), buffer_size) + % (len(telegram), self.buffer_size) ) - response = None + data_recived = bytearray() try: + # send bytes to control self._tcpsock.send(bytes(telegram)) if wait_for_response: - response = self._tcpsock.recv(buffer_size) + data_recived = self._tcpsock.recv(self.buffer_size) except Exception: - logging.error( + self._logger.error( "something went wrong while waiting for new data to arrive, buffer was set to %d", - buffer_size, + self.buffer_size, ) raise - if response is not None: - logging.debug("recived block of data with lenght %d" % len(response)) - # logging.debug("recived data: %s" % response) - if len(response) >= 8: + if len(data_recived) > 0: + self._logger.debug( + "received block of data with length %d", len(data_recived) + ) + if len(data_recived) >= 8: # read 4 bytes for response length - response_length = struct.unpack("!L", response[0:4])[0] + response_length = struct.unpack("!L", data_recived[0:4])[0] + # read 4 bytes for response type - response_command = response[4:8].decode("utf-8", "ignore") + self._last_lsv2_response = RSP( + data_recived[4:8].decode("utf-8", "ignore") + ) else: - # respons is less than 8 bytes long which is not enough space for package lengh and response message! - raise Exception("response to short: %s" % response) + # response is less than 8 bytes long which is not enough space for package length and response message! + raise LSV2ProtocolException( + "response to short, less than 8 bytes: %s" % data_recived + ) else: response_length = 0 - response_command = None + self._last_lsv2_response = RSP.NONE if response_length > 0: - response_content = bytearray(response[8:]) + response_content = bytearray(data_recived[8:]) while len(response_content) < response_length: - logging.debug( + self._logger.debug( "waiting for more data to arrive, %d bytes missing", len(response_content) < response_length, ) try: response_content.extend( - self._tcpsock.recv(response_length - len(response[8:])) + self._tcpsock.recv(response_length - len(data_recived[8:])) ) except Exception: - logging.error( - "something went wrong while waiting for more data to arrive" + self._logger.error( + "something went wrong while waiting for more data to arrive. expected %d, received %d, content so far: %s", + response_length, + len(data_recived), + data_recived, ) raise else: - response_content = None + response_content = bytearray() + + if self._last_lsv2_response is RSP.T_ER: + self._last_error = LSV2Error.from_ba(response_content) + else: + self._last_error = LSV2Error() + + return response_content + + +class LSV2RS232: + """placeholder implementation of the low level communication functions for sending and + receiving LSV2 telegrams via RS232""" + + DEFAULT_BUFFER_SIZE = 256 + # Default size of send and receive buffer + + def __init__(self, port: str, speed: int, timeout: float = 15.0): + self._logger = logging.getLogger("LSV2 RS232") + self.buffer_size = LSV2RS232.DEFAULT_BUFFER_SIZE + + self._is_connected = False + self._last_lsv2_response = RSP.NONE + self._last_error = LSV2Error() + raise NotImplementedError() + import serial + + @property + def last_response(self) -> RSP: + """get the response to the last telegram""" + return self._last_lsv2_response + + @property + def last_error(self) -> LSV2Error: + """get the error if the last telegram failed""" + return self._last_error + + @property + def buffer_size(self) -> int: + """size of the buffer used for sending and receiving data. + has to be negotiated with the control""" + return self._buffer_size - return response_command, response_content + @buffer_size.setter + def buffer_size(self, value: int): + if value < 8: + self._buffer_size = self.DEFAULT_BUFFER_SIZE + self._logger.warning( + "size of receive buffer to small, set to default of %d bytes", + self._buffer_size, + ) + else: + self._buffer_size = value + + def connect(self): + """ + Establish connection to control + """ + raise NotImplementedError() + pass + + def disconnect(self): + """ + Close connection + """ + raise NotImplementedError() + pass + + def telegram( + self, + command: Union[CMD, RSP], + payload: bytearray = bytearray(), + wait_for_response: bool = True, + ) -> bytearray: + """ + Send LSV2 telegram and receive response if necessary. + + :param command: command string + :param payload: command payload + :param wait_for_response: switch for waiting for response from control. + """ + raise NotImplementedError() diff --git a/pyLSV2/misc.py b/pyLSV2/misc.py index d3097ac..3102da8 100644 --- a/pyLSV2/misc.py +++ b/pyLSV2/misc.py @@ -4,188 +4,281 @@ import struct from datetime import datetime from pathlib import Path +from typing import Union, List, Dict -from .const import ControlType, BIN_FILES +from . import dat_cls as ld +from .const import BIN_FILES, PATH_SEP, ControlType +from .err import LSV2DataException -def decode_system_parameters(result_set): - """decode the result system parameter query +def decode_system_parameters(result_set: bytearray) -> ld.SystemParameters: + """ + Decode the result system parameter query + + :param result_set: bytes returned by the system parameter query command R_PR, - :param tuple result_set: bytes returned by the system parameter query command R_PR - :returns: dictionary with system parameter values - :rtype: dict + :raises LSV2DataException: Error during parsing of data values """ message_length = len(result_set) - info_list = list() + info_list = [] if message_length == 120: info_list = struct.unpack("!14L8B8L2BH4B2L2HL", result_set) elif message_length == 124: info_list = struct.unpack("!14L8B8L2BH4B2L2HLL", result_set) else: - raise ValueError( - "unexpected length {} of message content {}".format( - message_length, result_set - ) + raise LSV2DataException( + "unexpected length %s of message content %s" % (message_length, result_set) ) - sys_par = dict() - sys_par["Marker_Start"] = info_list[0] - sys_par["Markers"] = info_list[1] - sys_par["Input_Start"] = info_list[2] - sys_par["Inputs"] = info_list[3] - sys_par["Output_Start"] = info_list[4] - sys_par["Outputs"] = info_list[5] - sys_par["Counter_Start"] = info_list[6] - sys_par["Counters"] = info_list[7] - sys_par["Timer_Start"] = info_list[8] - sys_par["Timers"] = info_list[9] - sys_par["Word_Start"] = info_list[10] - sys_par["Words"] = info_list[11] - sys_par["String_Start"] = info_list[12] - sys_par["Strings"] = info_list[13] - sys_par["String_Length"] = info_list[14] - sys_par["Input_Word_Start"] = info_list[22] - sys_par["Input Words"] = info_list[23] - sys_par["Output_Word_Start"] = info_list[24] - sys_par["Output_Words"] = info_list[25] - sys_par["LSV2_Version"] = info_list[30] - sys_par["LSV2_Version_Flags"] = info_list[31] - sys_par["Max_Block_Length"] = info_list[32] - sys_par["HDH_Bin_Version"] = info_list[33] - sys_par["HDH_Bin_Revision"] = info_list[34] - sys_par["ISO_Bin_Version"] = info_list[35] - sys_par["ISO_Bin_Revision"] = info_list[36] - sys_par["HardwareVersion"] = info_list[37] - sys_par["LSV2_Version_Flags_Ex"] = info_list[38] - sys_par["Max_Trace_Line"] = info_list[39] - sys_par["Scope_Channels"] = info_list[40] - sys_par["PW_Encryption_Key"] = info_list[41] + sys_par = ld.SystemParameters() + sys_par.markers_start_address = info_list[0] + sys_par.number_of_markers = info_list[1] + + sys_par.inputs_start_address = info_list[2] + sys_par.number_of_inputs = info_list[3] + + sys_par.outputs_start_address = info_list[4] + sys_par.number_of_outputs = info_list[5] + + sys_par.counters_start_address = info_list[6] + sys_par.number_of_counters = info_list[7] + + sys_par.timers_start_address = info_list[8] + sys_par.number_of_timers = info_list[9] + + sys_par.words_start_address = info_list[10] + sys_par.number_of_words = info_list[11] + + sys_par.strings_start_address = info_list[12] + sys_par.number_of_strings = info_list[13] + sys_par.max_string_lenght = info_list[14] + + sys_par.input_words_start_address = info_list[22] + sys_par.number_of_input_words = info_list[23] + + sys_par.output_words_start_address = info_list[24] + sys_par.number_of_output_words = info_list[25] + + sys_par.lsv2_version = info_list[30] + sys_par.lsv2_version_flags = info_list[31] + sys_par.lsv2_version_flags_ex = info_list[38] + + sys_par.max_block_length = info_list[32] + + sys_par.bin_version = info_list[33] + sys_par.bin_revision = info_list[34] + + sys_par.iso_version = info_list[35] + sys_par.iso_revision = info_list[36] + + sys_par.hardware_version = info_list[37] + + sys_par.max_trace_line = info_list[39] + sys_par.number_of_scope_channels = info_list[40] + sys_par.password_encryption_key = info_list[41] return sys_par -def decode_file_system_info(data_set, control_type=ControlType.UNKNOWN): - """decode result from file system entry +def decode_file_system_info( + data_set: bytearray, control_type: ControlType = ControlType.UNKNOWN +) -> ld.FileEntry: + """ + Decode result from file system entry - :param tuple result_set: bytes returned by the system parameter query command R_FI or CR_DR - :returns: dictionary with file system entry parameters - :rtype: dict + :param result_set: bytes returned by the system parameter query command R_FI or CR_DR """ - flag_is_protected = 0x08 - if control_type in ( - ControlType.UNKNOWN, - ControlType.MILL_NEW, - ControlType.LATHE_NEW, - ): - flag_is_dir = 0x20 + if control_type in (ControlType.MILL_OLD, ControlType.LATHE_OLD): + # print("select old") + # according to documentation an LSV version 1 this should be: + # flag_display = 0x01 + flag_changable = 0x02 + # flag_highlighted = 0x04 + flag_hidden = 0x08 + flag_drive = None + flag_subdir = 0x40 + flag_protected = 0x20 + flag_selected = None else: - flag_is_dir = 0x40 + # another newer document has + # flag_display = 0x01 + flag_changable = 0x02 + # flag_highlighted = 0x04 # was 0x08, this might be an error in the documentation! + flag_hidden = 0x08 + flag_drive = 0x10 + flag_subdir = 0x20 + flag_protected = 0x40 + flag_selected = 0x80 - file_info = dict() - file_info["Size"] = struct.unpack("!L", data_set[:4])[0] - file_info["Timestamp"] = datetime.fromtimestamp( - struct.unpack("!L", data_set[4:8])[0] - ) + file_entry = ld.FileEntry() + file_entry.size = struct.unpack("!L", data_set[:4])[0] + file_entry.timestamp = datetime.fromtimestamp(struct.unpack("!L", data_set[4:8])[0]) - attributes = struct.unpack("!L", data_set[8:12])[0] + file_entry.attributes = struct.unpack("!L", data_set[8:12])[0] - file_info["Attributes"] = attributes - file_info["is_file"] = False - file_info["is_directory"] = False + file_entry.is_changable = (file_entry.attributes & flag_changable) != 0 - if bool(attributes & flag_is_dir): - file_info["is_directory"] = True - else: - file_info["is_file"] = True + if flag_drive is not None: + file_entry.is_drive = (file_entry.attributes & flag_drive) != 0 + + file_entry.is_directory = (file_entry.attributes & flag_subdir) != 0 + + file_entry.is_protected = (file_entry.attributes & flag_protected) != 0 + file_entry.is_hidden = (file_entry.attributes & flag_hidden) != 0 - file_info["is_write_protected"] = bool(attributes & flag_is_protected) + if flag_selected is not None: + file_entry.is_selected = (file_entry.attributes & flag_selected) != 0 - file_info["Name"] = data_set[12:].decode("latin1").strip("\x00").replace("\\", "/") + file_entry.name = ba_to_ustr(data_set[12:]).replace("/", PATH_SEP) - return file_info + # print(file_entry.name, file_entry.is_directory, file_entry.attributes) + return file_entry -def decode_directory_info(data_set): - """decode result from directory entry +def decode_drive_info(data_set: bytearray) -> List[ld.DriveEntry]: + """ + Split and decode result from drive info + + :param result_set: bytes returned by the system parameter query command R_DR mode 'DRIVE' + """ + offset = 0 + fixed_length = 15 + drive_entries = [] + + while (offset + fixed_length + 1) < len(data_set): + drive_entry = ld.DriveEntry() + drive_entry.unknown_0 = struct.unpack("!L", data_set[offset : offset + 4])[0] + drive_entry.unknown_1 = struct.unpack("!4s", data_set[offset + 4 : offset + 8])[ + 0 + ] + drive_entry.unknown_2 = struct.unpack("!L", data_set[offset + 8 : offset + 12])[ + 0 + ] + + if chr(data_set[offset + fixed_length]) == ":": + drive_entry.name = ba_to_ustr(data_set[offset + 12 : offset + 17]) + offset += fixed_length + 2 + else: + drive_entry.name = ba_to_ustr(data_set[offset + 12 : offset + 19]) + offset += fixed_length + 2 + drive_entries.append(drive_entry) + + return drive_entries + + +def decode_directory_info(data_set: bytearray) -> ld.DirectoryEntry: + """ + Decode result from directory entry - :param tuple result_set: bytes returned by the system parameter query command R_DI - :returns: dictionary with file system entry parameters - :rtype: dict + :param result_set: bytes returned by the system parameter query command R_DI """ - dir_info = dict() - dir_info["Free Size"] = struct.unpack("!L", data_set[:4])[0] - attribute_list = list() + dir_entry = ld.DirectoryEntry() + dir_entry.free_size = struct.unpack("!L", data_set[:4])[0] + + attribute_list = [] for i in range(4, len(data_set[4:132]), 4): - attr = data_set[i : i + 4].decode("latin1").strip("\x00") + attr = ba_to_ustr(data_set[i : i + 4]) if len(attr) > 0: attribute_list.append(attr) - dir_info["Dir_Attributs"] = attribute_list - - dir_info["Attributes"] = struct.unpack("!32B", data_set[132:164]) + dir_entry.dir_attributes = attribute_list - dir_info["Path"] = data_set[164:].decode("latin1").strip("\x00").replace("\\", "/") + dir_entry.attributes = bytearray(struct.unpack("!32B", data_set[132:164])) + dir_entry.path = ba_to_ustr(data_set[164:]).replace("/", PATH_SEP) - return dir_info + return dir_entry -def decode_tool_information(data_set): - """decode result from tool info +def decode_tool_info(data_set: bytearray) -> ld.ToolInformation: + """ + Decode result from tool info - :param tuple result_set: bytes returned by the system parameter query command R_RI for tool info - :returns: dictionary with tool info values - :rtype: dict + :param result_set: bytes returned by the system parameter query command R_RI for tool info """ - tool_info = dict() - tool_info["Number"] = struct.unpack("!L", data_set[0:4])[0] - tool_info["Index"] = struct.unpack("!H", data_set[4:6])[0] - tool_info["Axis"] = {0: "X", 1: "Y", 2: "Z"}.get( + tool_info = ld.ToolInformation() + tool_info.number = struct.unpack("!L", data_set[0:4])[0] + tool_info.index = struct.unpack("!H", data_set[4:6])[0] + tool_info.axis = {0: "X", 1: "Y", 2: "Z"}.get( struct.unpack("!H", data_set[6:8])[0], "unknown" ) if len(data_set) > 8: - tool_info["Length"] = struct.unpack(" ld.OverrideState: + """ + Decode result from override info + + :param result_set: bytes returned by the system parameter query command R_RI for override info + """ + ovr_state = ld.OverrideState() + ovr_state.feed = struct.unpack("!L", data_set[0:4])[0] / 100 + ovr_state.spindle = struct.unpack("!L", data_set[4:8])[0] / 100 + ovr_state.rapid = struct.unpack("!L", data_set[8:12])[0] / 100 + return ovr_state + + +def decode_error_message(data_set: bytearray) -> ld.NCErrorMessage: + """ + Decode result from reading error messages + + :param result_set: bytes returned by the system parameter query command R_RI for first and next error + """ + err_msg = ld.NCErrorMessage() + err_msg.e_class = struct.unpack("!H", data_set[0:2])[0] + err_msg.e_group = struct.unpack("!H", data_set[2:4])[0] + err_msg.e_number = struct.unpack("!l", data_set[4:8])[0] + err_msg.e_text = ba_to_ustr(data_set[8:]) + err_msg.dnc = True + return err_msg + - :param tuple result_set: bytes returned by the system parameter query command R_RI for - override info - :returns: dictionary with override info values - :rtype: dict +def decode_stack_info(data_set: bytearray) -> ld.StackState: """ - override_info = dict() - override_info["Feed_override"] = struct.unpack("!L", data_set[0:4])[0] / 100 - override_info["Speed_override"] = struct.unpack("!L", data_set[4:8])[0] / 100 - override_info["Rapid_override"] = struct.unpack("!L", data_set[8:12])[0] / 100 + Decode result from reading stack information - return override_info + :param data_set: bytes returned from query + """ + stack = ld.StackState() + stack.line_no = struct.unpack("!L", data_set[:4])[0] + stack.main = ba_to_ustr(data_set[4:].split(b"\x00")[0]) + stack.current = ba_to_ustr(data_set[4:].split(b"\x00")[1]) + return stack -def decode_error_message(data_set): - """decode result from reading error messages +def decode_axis_location(data_set: bytearray) -> Dict[str, float]: + """ + Decode result from reading axis position + Returns dictionary with key = axis name, value = position + + :param data_set: bytes returned from query - :param tuple result_set: bytes returned by the system parameter query command R_RI for - first and next error - :returns: dictionary with error message values - :rtype: dict + :raises LSV2DataException: Error during parsing of data values """ - error_info = dict() - error_info["Class"] = struct.unpack("!H", data_set[0:2])[0] - error_info["Group"] = struct.unpack("!H", data_set[2:4])[0] - error_info["Number"] = struct.unpack("!l", data_set[4:8])[0] - error_info["Text"] = data_set[8:].decode("latin1").strip("\x00") - return error_info + # unknown = result[0:1] # <- ??? + number_of_axes = struct.unpack("!b", data_set[1:2])[0] + + split_list = [] + start = 2 + for i, byte in enumerate(data_set[start:]): + if byte == 0x00: + value = data_set[start : i + 3] + split_list.append(ba_to_ustr(value)) + start = i + 3 + if len(split_list) != (2 * number_of_axes): + raise LSV2DataException("error while parsing axis data: %s" % data_set) -def is_file_binary(file_name): - """Check if file is expected to be binary by comparing with known expentions. - - :param file_name: name of the file to check - :returns: True if file matches know binary file type - :rtype: bool + axes_values = {} + for i in range(number_of_axes): + axes_values[split_list[i + number_of_axes]] = float(split_list[i]) + + return axes_values + + +def is_file_binary(file_name: Union[str, Path]) -> bool: + """ + Check if file is expected to be binary by comparing with known expentions. + Returns ``True`` if file matches know binary file type """ for bin_type in BIN_FILES: if isinstance(file_name, Path): @@ -193,4 +286,24 @@ def is_file_binary(file_name): return True elif file_name.endswith(bin_type): return True - return False \ No newline at end of file + return False + + +def ba_to_ustr(bytes_to_convert: bytearray) -> str: + """ + convert a bytearry of characters to unicode string + + :param bytes_to_convert: bytes to convert to unicode string + """ + return bytes_to_convert.decode("latin1").strip("\x00").rstrip() + + +def ustr_to_ba(str_to_convert: str) -> bytearray: + """ + convert a string to a byte array with string termination + + :param str_to_convert: string that should be converted to byte array + """ + str_bytes = bytearray(map(ord, str(str_to_convert))) + str_bytes.append(0x00) + return str_bytes diff --git a/pyLSV2/table_reader.py b/pyLSV2/table_reader.py index fff9f36..14b611e 100644 --- a/pyLSV2/table_reader.py +++ b/pyLSV2/table_reader.py @@ -1,168 +1,29 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """module with reader and writer for TNC tables""" +import csv import json import logging +import pathlib import re -from pathlib import Path +from typing import Union -class TableReader: - """generic parser for table files commonly used by TNC, iTNC, CNCPILOT, - MANUALplus and 6000i CNC - """ - - def __init__(self): - """init object variables logging""" - logging.getLogger(__name__).addHandler(logging.NullHandler()) - - def parse_table(self, table_path): - """Parse a file of one of the common table formats - - :param str or Path table_path: Path to the table file - - :returns: list od dictionaries. key is the column name, value the content of the table cell - :rtype: NCTabel - """ - nctable = NCTabel() - - table_file = Path(table_path) - if not table_file.is_file(): - raise FileNotFoundError("Could not open file %s" % table_path) - - try: - with table_file.open(mode="r", encoding="utf-8") as tfp: - header_line = tfp.readline().strip() - logging.debug("Checking line for header: %s", header_line) - header = re.match( - r"^BEGIN (?P[a-zA-Z_ 0-9]*)\.(?P[A-Za-z0-9]{1,4})(?P MM| INCH)?(?: (Version|VERSION): \'Update:(?P\d+\.\d+)\')?(?P U)?$", - header_line, - ) - - if header is None: - raise Exception( - "File has wrong format: incorrect header for file %s" - % table_path - ) - - nctable.name = header.group("name").strip() - nctable.suffix = header.group("suffix") - nctable.version = header.group("version") - - if header.group("unit") is not None: - nctable.has_unit = True - if "MM" in header.group("unit"): - nctable.is_metric = True - logging.debug( - 'Header Information for file "%s" Name "%s", file is metric, Version: "%s"', - table_file, - nctable.name, - nctable.version, - ) - else: - nctable.is_metric = False - logging.debug( - 'Header Information for file "%s" Name "%s", file is inch, Version: "%s"', - table_file, - nctable.name, - nctable.version, - ) - else: - nctable.has_unit = False - nctable.is_metric = False - logging.debug( - 'Header Information for file "%s" Name "%s", file has no units, Version: "%s"', - table_file, - nctable.name, - nctable.version, - ) - - next_line = tfp.readline() - if "#STRUCTBEGIN" in next_line: - in_preamble = True - next_line = tfp.readline() - while in_preamble: - if next_line.startswith("#"): - in_preamble = False - else: - next_line = tfp.readline() - next_line = tfp.readline() - elif "TableDescription" in next_line: - in_preamble = True - next_line = tfp.readline() - while in_preamble: - if next_line.startswith(")"): - in_preamble = False - else: - next_line = tfp.readline() - next_line = tfp.readline() - - column_pattern = re.compile(r"([A-Za-z-\d_:\.]+)(?:\s+)") - for column_match in column_pattern.finditer(next_line): - nctable.append_column( - name=column_match.group().strip(), - start=column_match.start(), - end=column_match.end(), - ) - - logging.debug("Found %d columns", len(nctable.get_column_names())) - - for line in tfp.readlines(): - if line.startswith("[END]"): - break - - table_entry = {} - for column in nctable.get_column_names(): - table_entry[column] = line[ - nctable.get_column_start(column) : nctable.get_column_end( - column - ) - ].strip() - nctable.append_row(table_entry) - - logging.debug("Found %d entries", len(nctable.rows)) - except UnicodeDecodeError: - logging.error("File has invalid utf-8 encoding") - return nctable - - @staticmethod - def format_entry_float(str_value): - """convert the string value of a table cell to float value""" - str_value = str_value.strip() - if str_value == "-" or len(str_value) == 0: - return None - - return float(str_value) - - @staticmethod - def format_entry_int(str_value): - """convert the string value of a table cell to int value""" - str_value = str_value.strip() - if str_value == "-" or len(str_value) == 0: - return None - return int(str_value) - - @staticmethod - def format_entry_bool(str_value): - """convert the string value of a table cell to boolean value""" - str_value = str_value.strip() - if len(str_value) == 0: - return None - elif str_value == "1": - return True - return False - - -class NCTabel: - """generic object for table files commonly used by TTNC, iTNC, CNCPILOT, +class NCTable: + """generic object for table files commonly used by TNC, iTNC, CNCPILOT, MANUALplus and 6000i CNC """ def __init__( - self, name=None, suffix=None, version=None, has_unit=False, is_metric=False + self, + name: str = "", + suffix: str = "", + version: str = "", + has_unit: bool = False, + is_metric: bool = False, ): """init object variables logging""" - logging.getLogger(__name__).addHandler(logging.NullHandler()) + self._logger = logging.getLogger("NCTable") self.name = name self.suffix = suffix self.version = version @@ -172,34 +33,35 @@ def __init__( self._columns = [] self._column_format = {} + def __len__(self): + """length of table is equal to number of rows in table""" + return len(self._content) + @property - def name(self): + def name(self) -> str: """Name of the table read from the header""" return self._name @name.setter - def name(self, value): + def name(self, value: str): self._name = value @property - def suffix(self): + def suffix(self) -> str: """file suffix of table""" return self._suffix @suffix.setter - def suffix(self, value): - if value is None: - self._suffix = None - else: - self._suffix = value.lower() + def suffix(self, value: str): + self._suffix = value.lower() @property - def version(self): + def version(self) -> str: """version string of from table header""" return self._version @version.setter - def version(self, value): + def version(self, value: str): self._version = value @property @@ -208,54 +70,70 @@ def has_unit(self): return self._has_unit @has_unit.setter - def has_unit(self, value): + def has_unit(self, value: bool): self._has_unit = value @property - def is_metric(self): + def is_metric(self) -> bool: """if true all values should be interpreted as metric""" return self._is_metric @is_metric.setter - def is_metric(self, value): + def is_metric(self, value: bool): self._is_metric = value - def append_column(self, name, start, end, width=0, empty_value=None): + @property + def rows(self) -> list: + """data entries in this table""" + return self._content + + @property + def column_names(self) -> list: + """list of columns used in this table""" + return self._columns + + def append_column( + self, name: str, start: int, end: int, width: int = 0, empty_value=None + ): """add column to the table format""" self._columns.append(name) if width == 0: width = end - start self._column_format[name] = { - "width": int(width), - "start": int(start), - "end": int(end), + "width": width, + "start": start, + "end": end, + "empty_value": empty_value, + "min": None, + "max": None, + "unique": None, + "unit": None, + "read_only": None, + "is_inch": False, } - if empty_value is not None: - self._column_format[name]["empty_value"] = empty_value - def remove_column(self, name): + def remove_column(self, name: str): """remove column by name from table format""" self._columns.remove(name) del self._column_format[name] - def get_column_start(self, name): + def get_column_start(self, name: str): """get start index of column""" return self._column_format[name]["start"] - def get_column_end(self, name): + def get_column_end(self, name: str): """get end index of column""" return self._column_format[name]["end"] - def get_column_width(self, name): + def get_column_width(self, name: str): """get width if column""" return self._column_format[name]["width"] - def get_column_empty_value(self, name): + def get_column_empty_value(self, name: str): """get value define as default value for column""" if "empty_value" in self._column_format[name]: return self._column_format[name]["empty_value"] - else: - return None + return None def set_column_empty_value(self, name, value): """set the default value of a column""" @@ -263,9 +141,35 @@ def set_column_empty_value(self, name, value): raise Exception("value to long for column") self._column_format[name]["empty_value"] = value - def get_column_names(self): + def update_column_format(self, name: str, parameters: dict): + """takes a column name and a dictionaly to update the current table configuration""" + for key, value in parameters.items(): + if key == "unit": + self._column_format[name]["unit"] = value + elif key == "minimum": + self._column_format[name]["min"] = value + elif key == "maximum": + self._column_format[name]["max"] = value + elif key == "unique": + self._column_format[name]["unique"] = value + elif key == "initial": + self._column_format[name]["empty_value"] = value + elif key == "readonly": + self._column_format[name]["read_only"] = value + elif key == "key": + pass + elif key == "width": + pass + elif key == "unitIsInch": + self._column_format[name]["is_inch"] = value + else: + raise NotImplementedError("key '%s' not implemented" % key) + + def _get_column_names(self): """get list of columns used in this table""" - return self._columns + raise DeprecationWarning( + "Do not use this function anymore! Use ```column_names```" + ) def append_row(self, row): """add a data entry to the table""" @@ -275,41 +179,16 @@ def extend_rows(self, rows): """add multiple data entries at onec""" self._content.extend(rows) - @property - def rows(self): - """data entries in this table""" - return self._content - - def format_to_json(self): + def format_to_json(self) -> str: """return json configuration representing the table format""" json_data = {} json_data["version"] = self.version json_data["suffix"] = self.suffix - json_data["column_list"] = self.get_column_names() + json_data["column_list"] = self.column_names json_data["column_config"] = self._column_format return json.dumps(json_data, ensure_ascii=False, indent=2) - @staticmethod - def from_json(file_path): - """return a new NCTable object based on a json configuration file""" - with open(file_path, "r", encoding="utf-8") as jfp: - json_data = json.load(jfp) - nct = NCTabel() - nct.version = json_data["version"] - nct.suffix = json_data["suffix"] - for column in json_data["column_config"]: - nct.append_column( - name=column, - start=json_data["column_config"][column]["start"], - end=json_data["column_config"][column]["end"], - ) - if "empty_value" in json_data["column_config"][column]: - nct.set_column_empty_value( - column, json_data["column_config"][column]["empty_value"] - ) - return nct - - def dump(self, file_path, renumber_column=None): + def dump_native(self, file_path: pathlib.Path, renumber_column=None): """write table data to a file in the format used by the controls""" row_counter = 0 file_name = file_path.name.upper() @@ -367,3 +246,330 @@ def dump(self, file_path, renumber_column=None): row_counter += 1 tfp.write("[END]\n") + + def dump_csv(self, file_path: pathlib.Path, decimal_char: str = "."): + """ + save content of table as csv file + + :param file_path: file location for csv file + """ + self._logger.debug("write table to csv, using decimal char '%s'", decimal_char) + + def localize_floats(row): + float_pattern = re.compile(r"^[+-]?\d+\.\d+$") + for key in row.keys(): + if float_pattern.match(row[key]): + row[key] = row[key].replace(".", decimal_char) + return row + + with open(file_path, "w", newline="", encoding="utf8") as csvfp: + csv_writer = csv.DictWriter( + csvfp, + delimiter=";", + quotechar='"', + quoting=csv.QUOTE_ALL, + fieldnames=self.column_names, + ) + csv_writer.writeheader() + for row in self.rows: + csv_writer.writerow(localize_floats(row)) + self._logger.info("csv file saved successfully") + + def find_string( + self, column_name: str, search_value: Union[str, re.Pattern] + ) -> list: + """ + search for string rows by string or pattern + returns list of lines that contain the search result + + :param column_name: name of the table column which should be checked + :param search_value: the value to check for, can be string or regular expression + """ + search_results = [] + if not column_name in self._columns: + self._logger.error( + "column with name %s not part of this table", column_name + ) + else: + if isinstance(search_value, (str,)): + search_results = [ + itm for itm in self._content if search_value in itm[column_name] + ] + elif isinstance(search_value, (re.Pattern,)): + search_results = [ + itm + for itm in self._content + if search_value.match(itm[column_name]) is not None + ] + return search_results + + @staticmethod + def parse_table(table_path: pathlib.Path) -> "NCTable": + """Parse a file of one of the common table formats + + :param str or Path table_path: Path to the table file + + :returns: list of dictionaries. key is the column name, value the content of the table cell + :rtype: NCTable + """ + logger = logging.getLogger("NCTable parser") + nctable = NCTable() + + table_config = None + + table_file = pathlib.Path(table_path) + if not table_file.is_file(): + raise FileNotFoundError("Could not open file %s" % table_path) + + try: + with table_file.open(mode="r", encoding="utf-8") as tfp: + header_line = tfp.readline().strip() + logger.debug("Checking line for header: %s", header_line) + header = re.match( + r"^BEGIN (?P[a-zA-Z_ 0-9]*)\.(?P[A-Za-z0-9]{1,4})(?P MM| INCH)?(?: (Version|VERSION): \'Update:(?P\d+\.\d+)(?: Date:(?P\d{4}-\d{2}-\d{2}))?\')?(?P U)?$", + header_line, + ) + + if header is None: + raise Exception( + "File has wrong format: incorrect header for file %s" + % table_path + ) + + nctable.name = header.group("name").strip() + nctable.suffix = header.group("suffix") + nctable.version = header.group("version") + + if header.group("unit") is not None: + nctable.has_unit = True + if "MM" in header.group("unit"): + nctable.is_metric = True + logger.debug( + 'Header Information for file "%s" Name "%s", file is metric, Version: "%s"', + table_file, + nctable.name, + nctable.version, + ) + else: + nctable.is_metric = False + logger.debug( + 'Header Information for file "%s" Name "%s", file is inch, Version: "%s"', + table_file, + nctable.name, + nctable.version, + ) + else: + nctable.has_unit = False + nctable.is_metric = False + logger.debug( + 'Header Information for file "%s" Name "%s", file has no units, Version: "%s"', + table_file, + nctable.name, + nctable.version, + ) + + next_line = tfp.readline() + if "#STRUCTBEGIN" in next_line: + in_preamble = True + next_line = tfp.readline() + while in_preamble: + if next_line.startswith("#"): + in_preamble = False + else: + next_line = tfp.readline() + next_line = tfp.readline() + elif "TableDescription" in next_line: + tab_desc = [] + tab_desc.append(next_line.strip()) + in_preamble = True + next_line = tfp.readline() + while in_preamble: + tab_desc.append(next_line.strip()) + if next_line.startswith(")"): + in_preamble = False + else: + next_line = tfp.readline() + next_line = tfp.readline() + table_config = NCTable.parse_table_description(tab_desc) + + column_pattern = re.compile(r"([A-Za-z-\d_:\.]+)(?:\s+)") + for column_match in column_pattern.finditer(next_line): + if column_match.group().endswith("\n"): + cl_end = column_match.end() - 1 + else: + cl_end = column_match.end() + nctable.append_column( + name=column_match.group().strip(), + start=column_match.start(), + end=cl_end, + ) + + logger.debug("Found %d columns", len(nctable.column_names)) + + for line in tfp.readlines(): + if line.startswith("[END]"): + break + + table_entry = {} + for column in nctable.column_names: + table_entry[column] = line[ + nctable.get_column_start(column) : nctable.get_column_end( + column + ) + ].strip() + nctable.append_row(table_entry) + + logger.debug("Found %d entries", len(nctable.rows)) + + if table_config is not None: + logger.debug("update column config from table description") + for c_d in table_config["TableDescription"]["columns"]: + cfg_column_name = c_d["CfgColumnDescription"]["key"] + if cfg_column_name not in nctable.column_names: + raise Exception( + "found unexpected column %s" % cfg_column_name + ) + if c_d["CfgColumnDescription"][ + "width" + ] != nctable.get_column_width(cfg_column_name): + raise Exception( + "found difference in column width for colmun %s: %d : %d" + % ( + cfg_column_name, + c_d["CfgColumnDescription"]["width"], + nctable.get_column_width(cfg_column_name), + ) + ) + nctable.update_column_format( + cfg_column_name, c_d["CfgColumnDescription"] + ) + + except UnicodeDecodeError: + logger.error("File has invalid utf-8 encoding") + return nctable + + @staticmethod + def parse_table_description(lines: list): + """ + parse the header of a table to get the table configuration + + :param list lines: list of strings cut from the table header + """ + config_data = {} + object_list = [] + object_list.append(config_data) + + def str_to_typed_value(value_string: str): + if re.match(r"^\"?[+-]?\d+[.,]\d+\"?$", value_string): + return float(value_string.strip('"')) + if re.match(r"^\"?[+-]?\d+\"?$", value_string): + return int(value_string.strip('"')) + if value_string.startswith('"') and value_string.endswith('"'): + return value_string.strip('"') + if value_string.upper() == "TRUE": + return True + if value_string.upper() == "FALSE": + return False + return value_string + + for line in lines: + line = line.rstrip(",") + + if line.endswith("("): + last_object = object_list[-1] + new_category = {} + name = line.split(" ")[0] + if isinstance(last_object, (list,)): + last_object.append({name: new_category}) + else: + if name in last_object: + raise Exception("Element already in dict") + last_object[name] = new_category + object_list.append(new_category) + + elif line.endswith("["): + last_object = object_list[-1] + new_group = [] + name = line.split(":=")[0] + if isinstance(last_object, (list,)): + last_object.append({name: new_group}) + else: + if name in last_object: + raise Exception("Element already in dict") + last_object[name] = new_group + object_list.append(new_group) + + elif line.endswith(")") or line.endswith("]"): + object_list.pop() + else: + last_object = object_list[-1] + if isinstance(last_object, (list,)): + if ":=" in line: + parts = line.split(":=") + last_object.append({parts[0]: str_to_typed_value(parts[1])}) + else: + last_object.append(line) + + elif isinstance(last_object, (dict,)): + if ":=" in line: + parts = line.split(":=") + last_object[parts[0]] = str_to_typed_value(parts[1]) + else: + raise Exception("no keyname??") + # last_object["value_%d" % id_counter] = line + + return config_data + + @staticmethod + def from_json_format(file_path: pathlib.Path) -> "NCTable": + """return a new NCTable object based on a json configuration file""" + logger = logging.getLogger("NCTable format parser") + nct = NCTable() + with open(file_path, "r", encoding="utf-8") as jfp: + json_data = json.load(jfp) + nct.version = json_data["version"] + nct.suffix = json_data["suffix"] + for column in json_data["column_config"]: + logger.debug( + "add column %s [%d:%d]", + column, + json_data["column_config"]["start"], + json_data["column_config"][column]["end"], + ) + nct.append_column( + name=column, + start=json_data["column_config"][column]["start"], + end=json_data["column_config"][column]["end"], + ) + if "empty_value" in json_data["column_config"][column]: + nct.set_column_empty_value( + column, json_data["column_config"][column]["empty_value"] + ) + return nct + + @staticmethod + def format_entry_float(str_value: str) -> Union[float, None]: + """convert the string value of a table cell to float value""" + str_value = str_value.strip() + if str_value == "-" or len(str_value) == 0: + return None + + return float(str_value) + + @staticmethod + def format_entry_int(str_value: str) -> Union[int, None]: + """convert the string value of a table cell to int value""" + str_value = str_value.strip() + if str_value == "-" or len(str_value) == 0: + return None + return int(str_value) + + @staticmethod + def format_entry_bool(str_value: str) -> Union[bool, None]: + """convert the string value of a table cell to boolean value""" + str_value = str_value.strip() + if len(str_value) == 0: + return None + if str_value == "1": + return True + return False diff --git a/pyLSV2/translate_messages.py b/pyLSV2/translate_messages.py index 64d676f..f0dbf10 100644 --- a/pyLSV2/translate_messages.py +++ b/pyLSV2/translate_messages.py @@ -3,11 +3,16 @@ """error code definitions and decoding, translation of status information into readable text""" import gettext import os +from typing import Union -from .const import ExecState, PgmState, LSV2Err +from .dat_cls import LSV2Error +from .const import ExecState, LSV2StatusCode, PgmState -def get_error_text(error_type, error_code, language=None, locale_path=None): + +def get_error_text( + t_error: LSV2Error, language: str = "", locale_path: Union[str, None] = None +) -> str: """Parse error type and error code and return the error message. :param int error_type: type of error code. @@ -20,7 +25,8 @@ def get_error_text(error_type, error_code, language=None, locale_path=None): if locale_path is None: locale_path = os.path.join(os.path.dirname(__file__), "locales") - if language is None: + + if len(language) < 2: translate = gettext.translation( domain="error_text", localedir=locale_path, fallback=True ) @@ -35,90 +41,94 @@ def get_error_text(error_type, error_code, language=None, locale_path=None): ) _ = translate.gettext - if error_type != 1: - raise NotImplementedError("Unknown error type: %d" % error_type) + if t_error.e_type != 1: + raise NotImplementedError("Unknown error type: %d" % t_error.e_type) return { - LSV2Err.T_ER_BAD_FORMAT: _("LSV2_ERROR_T_ER_BAD_FORMAT"), - LSV2Err.T_ER_UNEXPECTED_TELE: _("LSV2_ERROR_T_ER_UNEXPECTED_TELE"), - LSV2Err.T_ER_UNKNOWN_TELE: _("LSV2_ERROR_T_ER_UNKNOWN_TELE"), - LSV2Err.T_ER_NO_PRIV: _("LSV2_ERROR_T_ER_NO_PRIV"), - LSV2Err.T_ER_WRONG_PARA: _("LSV2_ERROR_T_ER_WRONG_PARA"), - LSV2Err.T_ER_BREAK: _("LSV2_ERROR_T_ER_BREAK"), - LSV2Err.T_ER_BAD_KEY: _("LSV2_ERROR_T_ER_BAD_KEY"), - LSV2Err.T_ER_BAD_FNAME: _("LSV2_ERROR_T_ER_BAD_FNAME"), - LSV2Err.T_ER_NO_FILE: _("LSV2_ERROR_T_ER_NO_FILE"), - LSV2Err.T_ER_OPEN_FILE: _("LSV2_ERROR_T_ER_OPEN_FILE"), - LSV2Err.T_ER_FILE_EXISTS: _("LSV2_ERROR_T_ER_FILE_EXISTS"), - LSV2Err.T_ER_BAD_FILE: _("LSV2_ERROR_T_ER_BAD_FILE"), - LSV2Err.T_ER_NO_DELETE: _("LSV2_ERROR_T_ER_NO_DELETE"), - LSV2Err.T_ER_NO_NEW_FILE: _("LSV2_ERROR_T_ER_NO_NEW_FILE"), - LSV2Err.T_ER_NO_CHANGE_ATT: _("LSV2_ERROR_T_ER_NO_CHANGE_ATT"), - LSV2Err.T_ER_BAD_EMULATEKEY: _("LSV2_ERROR_T_ER_BAD_EMULATEKEY"), - LSV2Err.T_ER_NO_MP: _("LSV2_ERROR_T_ER_NO_MP"), - LSV2Err.T_ER_NO_WIN: _("LSV2_ERROR_T_ER_NO_WIN"), - LSV2Err.T_ER_WIN_NOT_AKTIV: _("LSV2_ERROR_T_ER_WIN_NOT_AKTIV"), - LSV2Err.T_ER_ANZ: _("LSV2_ERROR_T_ER_ANZ"), - LSV2Err.T_ER_FONT_NOT_DEFINED: _("LSV2_ERROR_T_ER_FONT_NOT_DEFINED"), - LSV2Err.T_ER_FILE_ACCESS: _("LSV2_ERROR_T_ER_FILE_ACCESS"), - LSV2Err.T_ER_WRONG_DNC_STATUS: _("LSV2_ERROR_T_ER_WRONG_DNC_STATUS"), - LSV2Err.T_ER_CHANGE_PATH: _("LSV2_ERROR_T_ER_CHANGE_PATH"), - LSV2Err.T_ER_NO_RENAME: _("LSV2_ERROR_T_ER_NO_RENAME"), - LSV2Err.T_ER_NO_LOGIN: _("LSV2_ERROR_T_ER_NO_LOGIN"), - LSV2Err.T_ER_BAD_PARAMETER: _("LSV2_ERROR_T_ER_BAD_PARAMETER"), - LSV2Err.T_ER_BAD_NUMBER_FORMAT: _("LSV2_ERROR_T_ER_BAD_NUMBER_FORMAT"), - LSV2Err.T_ER_BAD_MEMADR: _("LSV2_ERROR_T_ER_BAD_MEMADR"), - LSV2Err.T_ER_NO_FREE_SPACE: _("LSV2_ERROR_T_ER_NO_FREE_SPACE"), - LSV2Err.T_ER_DEL_DIR: _("LSV2_ERROR_T_ER_DEL_DIR"), - LSV2Err.T_ER_NO_DIR: _("LSV2_ERROR_T_ER_NO_DIR"), - LSV2Err.T_ER_OPERATING_MODE: _("LSV2_ERROR_T_ER_OPERATING_MODE"), - LSV2Err.T_ER_NO_NEXT_ERROR: _("LSV2_ERROR_T_ER_NO_NEXT_ERROR"), - LSV2Err.T_ER_ACCESS_TIMEOUT: _("LSV2_ERROR_T_ER_ACCESS_TIMEOUT"), - LSV2Err.T_ER_NO_WRITE_ACCESS: _("LSV2_ERROR_T_ER_NO_WRITE_ACCESS"), - LSV2Err.T_ER_STIB: _("LSV2_ERROR_T_ER_STIB"), - LSV2Err.T_ER_REF_NECESSARY: _("LSV2_ERROR_T_ER_REF_NECESSARY"), - LSV2Err.T_ER_PLC_BUF_FULL: _("LSV2_ERROR_T_ER_PLC_BUF_FULL"), - LSV2Err.T_ER_NOT_FOUND: _("LSV2_ERROR_T_ER_NOT_FOUND"), - LSV2Err.T_ER_WRONG_FILE: _("LSV2_ERROR_T_ER_WRONG_FILE"), - LSV2Err.T_ER_NO_MATCH: _("LSV2_ERROR_T_ER_NO_MATCH"), - LSV2Err.T_ER_TOO_MANY_TPTS: _("LSV2_ERROR_T_ER_TOO_MANY_TPTS"), - LSV2Err.T_ER_NOT_ACTIVATED: _("LSV2_ERROR_T_ER_NOT_ACTIVATED"), - LSV2Err.T_ER_DSP_CHANNEL: _("LSV2_ERROR_T_ER_DSP_CHANNEL"), - LSV2Err.T_ER_DSP_PARA: _("LSV2_ERROR_T_ER_DSP_PARA"), - LSV2Err.T_ER_OUT_OF_RANGE: _("LSV2_ERROR_T_ER_OUT_OF_RANGE"), - LSV2Err.T_ER_INVALID_AXIS: _("LSV2_ERROR_T_ER_INVALID_AXIS"), - LSV2Err.T_ER_STREAMING_ACTIVE: _("LSV2_ERROR_T_ER_STREAMING_ACTIVE"), - LSV2Err.T_ER_NO_STREAMING_ACTIVE: _("LSV2_ERROR_T_ER_NO_STREAMING_ACTIVE"), - LSV2Err.T_ER_TO_MANY_OPEN_TCP: _("LSV2_ERROR_T_ER_TO_MANY_OPEN_TCP"), - LSV2Err.T_ER_NO_FREE_HANDLE: _("LSV2_ERROR_T_ER_NO_FREE_HANDLE"), - LSV2Err.T_ER_PLCMEMREMA_CLEAR: _("LSV2_ERROR_T_ER_PLCMEMREMA_CLEAR"), - LSV2Err.T_ER_OSZI_CHSEL: _("LSV2_ERROR_T_ER_OSZI_CHSEL"), - LSV2Err.LSV2_BUSY: _("LSV2_ERROR_LSV2_BUSY"), - LSV2Err.LSV2_X_BUSY: _("LSV2_ERROR_LSV2_X_BUSY"), - LSV2Err.LSV2_NOCONNECT: _("LSV2_ERROR_LSV2_NOCONNECT"), - LSV2Err.LSV2_BAD_BACKUP_FILE: _("LSV2_ERROR_LSV2_BAD_BACKUP_FILE"), - LSV2Err.LSV2_RESTORE_NOT_FOUND: _("LSV2_ERROR_LSV2_RESTORE_NOT_FOUND"), - LSV2Err.LSV2_DLL_NOT_INSTALLED: _("LSV2_ERROR_LSV2_DLL_NOT_INSTALLED"), - LSV2Err.LSV2_BAD_CONVERT_DLL: _("LSV2_ERROR_LSV2_BAD_CONVERT_DLL"), - LSV2Err.LSV2_BAD_BACKUP_LIST: _("LSV2_ERROR_LSV2_BAD_BACKUP_LIST"), - LSV2Err.LSV2_UNKNOWN_ERROR: _("LSV2_ERROR_LSV2_UNKNOWN_ERROR"), - LSV2Err.T_BD_NO_NEW_FILE: _("LSV2_ERROR_T_BD_NO_NEW_FILE"), - LSV2Err.T_BD_NO_FREE_SPACE: _("LSV2_ERROR_T_BD_NO_FREE_SPACE"), - LSV2Err.T_BD_FILE_NOT_ALLOWED: _("LSV2_ERROR_T_BD_FILE_NOT_ALLOWED"), - LSV2Err.T_BD_BAD_FORMAT: _("LSV2_ERROR_T_BD_BAD_FORMAT"), - LSV2Err.T_BD_BAD_BLOCK: _("LSV2_ERROR_T_BD_BAD_BLOCK"), - LSV2Err.T_BD_END_PGM: _("LSV2_ERROR_T_BD_END_PGM"), - LSV2Err.T_BD_ANZ: _("LSV2_ERROR_T_BD_ANZ"), - LSV2Err.T_BD_WIN_NOT_DEFINED: _("LSV2_ERROR_T_BD_WIN_NOT_DEFINED"), - LSV2Err.T_BD_WIN_CHANGED: _("LSV2_ERROR_T_BD_WIN_CHANGED"), - LSV2Err.T_BD_DNC_WAIT: _("LSV2_ERROR_T_BD_DNC_WAIT"), - LSV2Err.T_BD_CANCELLED: _("LSV2_ERROR_T_BD_CANCELLED"), - LSV2Err.T_BD_OSZI_OVERRUN: _("LSV2_ERROR_T_BD_OSZI_OVERRUN"), - LSV2Err.T_BD_FD: _("LSV2_ERROR_T_BD_FD"), - LSV2Err.T_USER_ERROR: _("LSV2_ERROR_T_USER_ERROR"), - }.get(error_code, _("LSV2_ERROR_UNKNOWN_CODE")) - - -def get_program_status_text(code, language=None, locale_path=None): + LSV2StatusCode.T_ER_BAD_FORMAT: _("LSV2_ERROR_T_ER_BAD_FORMAT"), + LSV2StatusCode.T_ER_UNEXPECTED_TELE: _("LSV2_ERROR_T_ER_UNEXPECTED_TELE"), + LSV2StatusCode.T_ER_UNKNOWN_TELE: _("LSV2_ERROR_T_ER_UNKNOWN_TELE"), + LSV2StatusCode.T_ER_NO_PRIV: _("LSV2_ERROR_T_ER_NO_PRIV"), + LSV2StatusCode.T_ER_WRONG_PARA: _("LSV2_ERROR_T_ER_WRONG_PARA"), + LSV2StatusCode.T_ER_BREAK: _("LSV2_ERROR_T_ER_BREAK"), + LSV2StatusCode.T_ER_BAD_KEY: _("LSV2_ERROR_T_ER_BAD_KEY"), + LSV2StatusCode.T_ER_BAD_FNAME: _("LSV2_ERROR_T_ER_BAD_FNAME"), + LSV2StatusCode.T_ER_NO_FILE: _("LSV2_ERROR_T_ER_NO_FILE"), + LSV2StatusCode.T_ER_OPEN_FILE: _("LSV2_ERROR_T_ER_OPEN_FILE"), + LSV2StatusCode.T_ER_FILE_EXISTS: _("LSV2_ERROR_T_ER_FILE_EXISTS"), + LSV2StatusCode.T_ER_BAD_FILE: _("LSV2_ERROR_T_ER_BAD_FILE"), + LSV2StatusCode.T_ER_NO_DELETE: _("LSV2_ERROR_T_ER_NO_DELETE"), + LSV2StatusCode.T_ER_NO_NEW_FILE: _("LSV2_ERROR_T_ER_NO_NEW_FILE"), + LSV2StatusCode.T_ER_NO_CHANGE_ATT: _("LSV2_ERROR_T_ER_NO_CHANGE_ATT"), + LSV2StatusCode.T_ER_BAD_EMULATEKEY: _("LSV2_ERROR_T_ER_BAD_EMULATEKEY"), + LSV2StatusCode.T_ER_NO_MP: _("LSV2_ERROR_T_ER_NO_MP"), + LSV2StatusCode.T_ER_NO_WIN: _("LSV2_ERROR_T_ER_NO_WIN"), + LSV2StatusCode.T_ER_WIN_NOT_AKTIV: _("LSV2_ERROR_T_ER_WIN_NOT_AKTIV"), + LSV2StatusCode.T_ER_ANZ: _("LSV2_ERROR_T_ER_ANZ"), + LSV2StatusCode.T_ER_FONT_NOT_DEFINED: _("LSV2_ERROR_T_ER_FONT_NOT_DEFINED"), + LSV2StatusCode.T_ER_FILE_ACCESS: _("LSV2_ERROR_T_ER_FILE_ACCESS"), + LSV2StatusCode.T_ER_WRONG_DNC_STATUS: _("LSV2_ERROR_T_ER_WRONG_DNC_STATUS"), + LSV2StatusCode.T_ER_CHANGE_PATH: _("LSV2_ERROR_T_ER_CHANGE_PATH"), + LSV2StatusCode.T_ER_NO_RENAME: _("LSV2_ERROR_T_ER_NO_RENAME"), + LSV2StatusCode.T_ER_NO_LOGIN: _("LSV2_ERROR_T_ER_NO_LOGIN"), + LSV2StatusCode.T_ER_BAD_PARAMETER: _("LSV2_ERROR_T_ER_BAD_PARAMETER"), + LSV2StatusCode.T_ER_BAD_NUMBER_FORMAT: _("LSV2_ERROR_T_ER_BAD_NUMBER_FORMAT"), + LSV2StatusCode.T_ER_BAD_MEMADR: _("LSV2_ERROR_T_ER_BAD_MEMADR"), + LSV2StatusCode.T_ER_NO_FREE_SPACE: _("LSV2_ERROR_T_ER_NO_FREE_SPACE"), + LSV2StatusCode.T_ER_DEL_DIR: _("LSV2_ERROR_T_ER_DEL_DIR"), + LSV2StatusCode.T_ER_NO_DIR: _("LSV2_ERROR_T_ER_NO_DIR"), + LSV2StatusCode.T_ER_OPERATING_MODE: _("LSV2_ERROR_T_ER_OPERATING_MODE"), + LSV2StatusCode.T_ER_NO_NEXT_ERROR: _("LSV2_ERROR_T_ER_NO_NEXT_ERROR"), + LSV2StatusCode.T_ER_ACCESS_TIMEOUT: _("LSV2_ERROR_T_ER_ACCESS_TIMEOUT"), + LSV2StatusCode.T_ER_NO_WRITE_ACCESS: _("LSV2_ERROR_T_ER_NO_WRITE_ACCESS"), + LSV2StatusCode.T_ER_STIB: _("LSV2_ERROR_T_ER_STIB"), + LSV2StatusCode.T_ER_REF_NECESSARY: _("LSV2_ERROR_T_ER_REF_NECESSARY"), + LSV2StatusCode.T_ER_PLC_BUF_FULL: _("LSV2_ERROR_T_ER_PLC_BUF_FULL"), + LSV2StatusCode.T_ER_NOT_FOUND: _("LSV2_ERROR_T_ER_NOT_FOUND"), + LSV2StatusCode.T_ER_WRONG_FILE: _("LSV2_ERROR_T_ER_WRONG_FILE"), + LSV2StatusCode.T_ER_NO_MATCH: _("LSV2_ERROR_T_ER_NO_MATCH"), + LSV2StatusCode.T_ER_TOO_MANY_TPTS: _("LSV2_ERROR_T_ER_TOO_MANY_TPTS"), + LSV2StatusCode.T_ER_NOT_ACTIVATED: _("LSV2_ERROR_T_ER_NOT_ACTIVATED"), + LSV2StatusCode.T_ER_DSP_CHANNEL: _("LSV2_ERROR_T_ER_DSP_CHANNEL"), + LSV2StatusCode.T_ER_DSP_PARA: _("LSV2_ERROR_T_ER_DSP_PARA"), + LSV2StatusCode.T_ER_OUT_OF_RANGE: _("LSV2_ERROR_T_ER_OUT_OF_RANGE"), + LSV2StatusCode.T_ER_INVALID_AXIS: _("LSV2_ERROR_T_ER_INVALID_AXIS"), + LSV2StatusCode.T_ER_STREAMING_ACTIVE: _("LSV2_ERROR_T_ER_STREAMING_ACTIVE"), + LSV2StatusCode.T_ER_NO_STREAMING_ACTIVE: _( + "LSV2_ERROR_T_ER_NO_STREAMING_ACTIVE" + ), + LSV2StatusCode.T_ER_TO_MANY_OPEN_TCP: _("LSV2_ERROR_T_ER_TO_MANY_OPEN_TCP"), + LSV2StatusCode.T_ER_NO_FREE_HANDLE: _("LSV2_ERROR_T_ER_NO_FREE_HANDLE"), + LSV2StatusCode.T_ER_PLCMEMREMA_CLEAR: _("LSV2_ERROR_T_ER_PLCMEMREMA_CLEAR"), + LSV2StatusCode.T_ER_OSZI_CHSEL: _("LSV2_ERROR_T_ER_OSZI_CHSEL"), + LSV2StatusCode.LSV2_BUSY: _("LSV2_ERROR_LSV2_BUSY"), + LSV2StatusCode.LSV2_X_BUSY: _("LSV2_ERROR_LSV2_X_BUSY"), + LSV2StatusCode.LSV2_NOCONNECT: _("LSV2_ERROR_LSV2_NOCONNECT"), + LSV2StatusCode.LSV2_BAD_BACKUP_FILE: _("LSV2_ERROR_LSV2_BAD_BACKUP_FILE"), + LSV2StatusCode.LSV2_RESTORE_NOT_FOUND: _("LSV2_ERROR_LSV2_RESTORE_NOT_FOUND"), + LSV2StatusCode.LSV2_DLL_NOT_INSTALLED: _("LSV2_ERROR_LSV2_DLL_NOT_INSTALLED"), + LSV2StatusCode.LSV2_BAD_CONVERT_DLL: _("LSV2_ERROR_LSV2_BAD_CONVERT_DLL"), + LSV2StatusCode.LSV2_BAD_BACKUP_LIST: _("LSV2_ERROR_LSV2_BAD_BACKUP_LIST"), + LSV2StatusCode.LSV2_UNKNOWN_ERROR: _("LSV2_ERROR_LSV2_UNKNOWN_ERROR"), + LSV2StatusCode.T_BD_NO_NEW_FILE: _("LSV2_ERROR_T_BD_NO_NEW_FILE"), + LSV2StatusCode.T_BD_NO_FREE_SPACE: _("LSV2_ERROR_T_BD_NO_FREE_SPACE"), + LSV2StatusCode.T_BD_FILE_NOT_ALLOWED: _("LSV2_ERROR_T_BD_FILE_NOT_ALLOWED"), + LSV2StatusCode.T_BD_BAD_FORMAT: _("LSV2_ERROR_T_BD_BAD_FORMAT"), + LSV2StatusCode.T_BD_BAD_BLOCK: _("LSV2_ERROR_T_BD_BAD_BLOCK"), + LSV2StatusCode.T_BD_END_PGM: _("LSV2_ERROR_T_BD_END_PGM"), + LSV2StatusCode.T_BD_ANZ: _("LSV2_ERROR_T_BD_ANZ"), + LSV2StatusCode.T_BD_WIN_NOT_DEFINED: _("LSV2_ERROR_T_BD_WIN_NOT_DEFINED"), + LSV2StatusCode.T_BD_WIN_CHANGED: _("LSV2_ERROR_T_BD_WIN_CHANGED"), + LSV2StatusCode.T_BD_DNC_WAIT: _("LSV2_ERROR_T_BD_DNC_WAIT"), + LSV2StatusCode.T_BD_CANCELLED: _("LSV2_ERROR_T_BD_CANCELLED"), + LSV2StatusCode.T_BD_OSZI_OVERRUN: _("LSV2_ERROR_T_BD_OSZI_OVERRUN"), + LSV2StatusCode.T_BD_FD: _("LSV2_ERROR_T_BD_FD"), + LSV2StatusCode.T_USER_ERROR: _("LSV2_ERROR_T_USER_ERROR"), + }.get(t_error.e_code, _("LSV2_ERROR_UNKNOWN_CODE")) + + +def get_program_status_text( + code: PgmState, language: str = "", locale_path: Union[str, None] = None +) -> str: """Translate status code of program state to text :param int code: status code of program state @@ -129,7 +139,8 @@ def get_program_status_text(code, language=None, locale_path=None): if locale_path is None: locale_path = os.path.join(os.path.dirname(__file__), "locales") - if language is None: + + if len(language) < 2: translate = gettext.translation( domain="message_text", localedir=locale_path, fallback=True ) @@ -157,7 +168,9 @@ def get_program_status_text(code, language=None, locale_path=None): }.get(code, translate.gettext("PGM_STATE_UNKNOWN")) -def get_execution_status_text(code, language=None, locale_path=None): +def get_execution_status_text( + code: ExecState, language: str = "", locale_path: Union[str, None] = None +): """Translate status code of execution state to text See https://github.com/drunsinn/pyLSV2/issues/1 @@ -169,7 +182,8 @@ def get_execution_status_text(code, language=None, locale_path=None): if locale_path is None: locale_path = os.path.join(os.path.dirname(__file__), "locales") - if language is None: + + if len(language) < 2: translate = gettext.translation( domain="message_text", localedir=locale_path, fallback=True ) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..bcd0752 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +log_cli = True +log_cli_level = INFO +log_format = %(asctime)s %(levelname)s %(message)s +log_date_format = %Y-%m-%d %H:%M:%S diff --git a/scripts/check_for_LSV2_commands.py b/scripts/check_for_LSV2_commands.py deleted file mode 100644 index 738e208..0000000 --- a/scripts/check_for_LSV2_commands.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import logging -import struct -import pyLSV2 - - -def test_command(lsv2, command_string, payload=None): - """check commands for validity""" - response, content = lsv2._llcom.telegram( - command_string, payload, buffer_size=lsv2._buffer_size - ) - if content is None: - response_length = -1 - else: - response_length = len(content) - if response in lsv2.RESPONSE_T_ER: - if len(content) == 2: - ( - byte_1, - byte_2, - ) = struct.unpack("!BB", content) - error_text = lsv2.get_error_text(byte_1, byte_2) - else: - error_text = "Unknown Error Number {}".format(content) - else: - error_text = "NONE" - return "sent {} payload {} received {} message_length {} content {} error text : {}".format( - command_string, payload, response, response_length, content, error_text - ) - - -if __name__ == "__main__": - logging.basicConfig(format="%(levelname)s : %(message)s", level=logging.ERROR) - - lsv2 = pyLSV2.LSV2("192.168.56.103", safe_mode=False) - - lsv2.connect() - lsv2.login(login=pyLSV2.LOGIN_INSPECT) - lsv2.login(login=pyLSV2.LOGIN_DNC) - lsv2.login(login=pyLSV2.LOGIN_INSPECT) - lsv2.login(login=pyLSV2.LOGIN_DIAG) - lsv2.login(login=pyLSV2.LOGIN_PLCDEBUG) - lsv2.login(login=pyLSV2.LOGIN_FILETRANSFER) - lsv2.login(login=pyLSV2.LOGIN_MONITOR) - lsv2.login(login=pyLSV2.LOGIN_DSP) - lsv2.login(login=pyLSV2.LOGIN_DNC) - lsv2.login(login=pyLSV2.LOGIN_SCOPE) - lsv2.login(login=pyLSV2.LOGIN_FILEPLC, password="") - - # base_string = 'A_' - # with open('./out.txt', 'a') as fp: - # for first_letter in list(string.ascii_uppercase)[:]: - # for second_letter in list(string.ascii_uppercase)[:]: - # cmd = base_string + first_letter + second_letter - # fp.write(lsv2._test_command(command_string=cmd, payload=None) + '\n') - # time.sleep(0.5) - - with open("./out.txt", "a") as fp: - for i in range(0, 0xFFFFFFFF): - fp.write( - test_command(lsv2, command_string="R_MB", payload=struct.pack("!L", i)) - + "\n" - ) - - lsv2.disconnect() diff --git a/scripts/lsv2_demo.py b/scripts/lsv2_demo.py index 3ff02bd..2df98af 100644 --- a/scripts/lsv2_demo.py +++ b/scripts/lsv2_demo.py @@ -1,204 +1,199 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """This script contains examples on how to use different functions of pyLSV2 - Not all functions are shown here, especially the file functions aren't shown here + Not all functions are shown here """ import argparse import logging -import sys -import tempfile import time -from pathlib import Path import pyLSV2 from pyLSV2.const import MemoryType -logging.basicConfig(level=logging.INFO) +__author__ = "drunsinn" +__license__ = "MIT" +__version__ = "1.0" +__email__ = "dr.unsinn@googlemail.com" if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('address', nargs='?', default='192.168.56.101', type=str) + parser.add_argument("address", nargs="?", default="192.168.56.101", type=str) parser.add_argument( - '-d', '--debug', + "-d", + "--debug", help="Print lots of debugging statements", - action="store_const", dest="loglevel", const=logging.DEBUG, + action="store_const", + dest="loglevel", + const=logging.DEBUG, default=logging.WARNING, ) parser.add_argument( - '-v', '--verbose', + "-v", + "--verbose", help="Be verbose", - action="store_const", dest="loglevel", const=logging.INFO, + action="store_const", + dest="loglevel", + const=logging.INFO, ) args = parser.parse_args() logging.basicConfig(level=args.loglevel) print("Connecting to {}".format(args.address)) - con = pyLSV2.LSV2(args.address, port=19000, timeout=5, safe_mode=False) - con.connect() + with pyLSV2.LSV2(args.address, port=19000, timeout=5) as con: - print( - 'Connected to "{Control}" with NC Software "{NC_Version}"'.format( - **con.get_versions() - ) - ) - print( - "Running LSV2 Version {LSV2_Version} with Flags {LSV2_Version_Flags}".format( - **con.get_system_parameter() - ) - ) - - print("Drive Info: {}".format(con.get_drive_info())) - print( - "Current Folder {Path} Free Space: {Free Size} Attrib: {Dir_Attributs}".format( - **con.get_directory_info() - ) - ) - - print("Axes positions: {}".format(con.get_axes_location())) - - print( - "PLC Marker: {}".format( - con.read_plc_memory(address=0, mem_type=MemoryType.MARKER, count=15) - ) - ) - print( - "PLC Word: {}".format( - con.read_plc_memory(address=6, mem_type=MemoryType.WORD, count=10) - ) - ) - print( - "PLC Double Word: {}".format( - con.read_plc_memory(address=0, mem_type=MemoryType.DWORD, count=10) - ) - ) - print( - "PLC String: {}".format( - con.read_plc_memory(address=2, mem_type=MemoryType.STRING, count=2) - ) - ) - print( - "PLC Input: {}".format( - con.read_plc_memory(address=0, mem_type=MemoryType.INPUT, count=5) - ) - ) - print( - "PLC Word Output: {}".format( - con.read_plc_memory(address=10, mem_type=MemoryType.OUTPUT_WORD, count=5) - ) - ) + con = pyLSV2.LSV2(args.address, port=19000, timeout=5, safe_mode=False) + con.connect() - # read values from PLC and other locations with telegram R_DP. Only works on iTNC - if con.is_itnc(): - print( - "PLC Double Word 7928: {}".format(con.read_data_path("/PLC/memory/D/7928")) - ) - print("PLC Constant 1: {}".format(con.read_data_path("/PLC/memory/K/1"))) - print("PLC Marker 211: {}".format(con.read_data_path("/PLC/memory/M/211"))) - print("PLC Byte 7192: {}".format(con.read_data_path("/PLC/memory/B/7192"))) - print("PLC Word 10908: {}".format(con.read_data_path("/PLC/memory/W/10908"))) - print("PLC Input 11: {}".format(con.read_data_path("/PLC/memory/I/11"))) - print("PLC String 30: {}".format(con.read_data_path("/PLC/memory/S/30"))) - print( - "DOC column of tool 1: {}".format(con.read_data_path("/TABLE/TOOL/T/1/DOC")) - ) - print("L column of tool 1: {}".format(con.read_data_path("/TABLE/TOOL/T/1/L"))) - print("R column of tool 1: {}".format(con.read_data_path("/TABLE/TOOL/T/1/R"))) + print("Basics:") print( - "PLC column of tool 1: {}".format(con.read_data_path("/TABLE/TOOL/T/1/PLC")) - ) - print( - "TMAT column of tool 1: {}".format( - con.read_data_path("/TABLE/TOOL/T/1/TMAT") + "# Connected to a '{:s}' running software version '{:s}'".format( + con.versions.control, con.versions.nc_sw ) ) print( - "LAST_USE column of tool 1: {}".format( - con.read_data_path("/TABLE/TOOL/T/1/LAST_USE") + "# Using LSV2 version '{:d}' with version flags '0x{:02x}' and '0x{:02x}'".format( + con.parameters.lsv2_version, + con.parameters.lsv2_version_flags, + con.parameters.lsv2_version_flags_ex, ) ) - # reading of machine parameter for old an new style names - if not con.is_itnc(): - # new stype + # read error messages via LSV2, works only on iTNC controls + print("# read error messages, only available on iTNC530") + if con.versions.is_itnc(): + e_m = con.get_error_messages() + print("## Number of currently active error messages: {:d}".format(len(e_m))) + for i, msg in enumerate(e_m): + print("### Error {:d} : {:s}".format(i, str(msg))) + else: + print("## function 'get_error_messages()' not suportet for this control") + + print("Machine:") + print("# axes positions: {}".format(con.axes_location())) + exec_stat = con.execution_state() + exec_stat_text = pyLSV2.get_execution_status_text(exec_stat) + print("# execution: {:d} - '{:s}'".format(exec_stat, exec_stat_text)) + pgm_stat = con.program_status() + if pgm_stat is not None: + pgm_stat_text = pyLSV2.get_program_status_text(pgm_stat) + print("# program: {:d} - '{:s}'".format(pgm_stat, pgm_stat_text)) + + pgm_stack = con.program_stack() + if pgm_stack is not None: + print("# selected program: '{:s}'".format(pgm_stack.main)) + print( + "## currently execution '{:s}' on line {:d}".format( + pgm_stack.current, pgm_stack.line_no + ) + ) + + ovr_stat = con.override_state() + if ovr_stat is not None: + print( + "# override states: feed {:f}%, rapid {:f}%, spindle {:f}%".format( + ovr_stat.feed, ovr_stat.rapid, ovr_stat.spindle + ) + ) + + print("PLC memory:") + print("# the first 5 entries for some memory types:") + print("## marker: {}".format(con.read_plc_memory(0, MemoryType.MARKER, 5))) + print("## word: {}".format(con.read_plc_memory(0, MemoryType.WORD, 5))) + print("## double word: {}".format(con.read_plc_memory(0, MemoryType.DWORD, 5))) + print("## string: {}".format(con.read_plc_memory(0, MemoryType.STRING, 5))) + print("## input: {}".format(con.read_plc_memory(0, MemoryType.INPUT, 5))) + print("## output: {}".format(con.read_plc_memory(0, MemoryType.OUTPUT_WORD, 5))) + + print("# data values via data path, only available on iTNC530") + if con.versions.is_itnc(): + print("## marker 0: {}".format(con.read_data_path("/PLC/memory/M/0"))) + print("## marker 1: {}".format(con.read_data_path("/PLC/memory/M/1"))) + print("## string 0: {}".format(con.read_data_path("/PLC/memory/S/0"))) + print("## word 10908: {}".format(con.read_data_path("/PLC/memory/W/10908"))) + else: + print("## function 'read_data_path()' not suportet for this control") + print("# table values via data path, only available on iTNC530") + if con.versions.is_itnc(): + print("## values from tool table for tool T1:") + print("## DOC column: {}".format(con.read_data_path("/TABLE/TOOL/T/1/DOC"))) + print("## L column: {}".format(con.read_data_path("/TABLE/TOOL/T/1/L"))) + print("## R column: {}".format(con.read_data_path("/TABLE/TOOL/T/1/R"))) + else: + print("## function 'read_data_path()' not suportet for this control") + + print("Configuration:") + if con.versions.is_itnc(): + # old style + lang = con.get_machine_parameter("7230.0") + else: + # new style + lang = con.get_machine_parameter("CfgDisplayLanguage.ncLanguage") + print("# Value of machine parameter for NC language: {:s}".format(lang)) + + print("UI Interface") + print("# switch to mode manual") + con.set_keyboard_access(False) + con.send_key_code(pyLSV2.KeyCode.MODE_MANUAL) + con.set_keyboard_access(True) + print("# wait 5 seconds") + time.sleep(5) + print("# switch to mode edit") + con.set_keyboard_access(False) + con.send_key_code(pyLSV2.KeyCode.MODE_PGM_EDIT) + con.set_keyboard_access(True) + + print("File access") + drv_info = con.drive_info() print( - "Current Language: {}".format( - con.get_machine_parameter("CfgDisplayLanguage.ncLanguage") + "# names of disk drives: {:s}".format( + ", ".join([drv.name for drv in drv_info]) ) ) - else: - # old style - print("Current Language: {}".format(con.get_machine_parameter("7230.0"))) - - # changing the value of a machine parameter - # con.login(pyLSV2.Login.PLCDEBUG) - # new style: con.set_machine_parameter('CfgDisplayLanguage.ncLanguage', 'CZECH', safe_to_disk=False) - # old style: con.set_machine_parameter('7230.0', '2', safe_to_disk=False) - # con.logout(pyLSV2.Login.PLCDEBUG) - - # read program stack - print("Current Program Stack: {}".format(con.get_program_stack())) - - # demo for sending key codes - con.login(pyLSV2.Login.MONITOR) - con.set_keyboard_access(False) - con.send_key_code(pyLSV2.KeyCode.MODE_MANUAL) - time.sleep(3) - con.send_key_code(pyLSV2.KeyCode.MODE_PGM_EDIT) - con.set_keyboard_access(True) - con.logout(pyLSV2.Login.MONITOR) - - # demo for reading the current tool with fallback if it is not supported by control - t_info = con.get_spindle_tool_status() - if t_info is not False: + dir_info = con.directory_info() print( - "Current tool number {Number}.{Index} with Axis {Axis} Length {Length} Radius {Radius}".format( - **t_info + "# current directory is '{:s}' with {:d} bytes of free drive space".format( + dir_info.path, dir_info.free_size ) ) - else: - print( - "Direct reading to current tool not supported for this control type. using backup strategy" + + dir_content = con.directory_content() + only_files = filter( + lambda f_e: f_e.is_directory is False and f_e.is_drive is False, + dir_content, ) - if con.is_tnc(): - pocket_table_path = "TNC:/table/tool_p.tch" - transfer_binary = True - spindel_lable = "0.0" - elif con.is_itnc(): - pocket_table_path = "TNC:/TOOL_P.TCH" - transfer_binary = False - spindel_lable = "0" - else: - pocket_table_path = "TNC:/table/ToolAllo.tch" - transfer_binary = True - spindel_lable = "0.0" - - with tempfile.TemporaryDirectory(suffix=None, prefix="pyLSV2_") as tmp_dir_name: - local_recive_path = Path(tmp_dir_name).joinpath("tool_p.tch") - con.recive_file( - local_path=str(local_recive_path), - remote_path=pocket_table_path, - binary_mode=transfer_binary, + + for file_entry in only_files: + print( + "## file name: {:s}, date {:}, size {:d} bytes".format( + file_entry.name, file_entry.timestamp, file_entry.size + ) + ) + only_dir = filter( + lambda f_e: f_e.is_directory is True and f_e.is_drive is False, dir_content + ) + for file_entry in only_dir: + print( + "## directory name: {:s}, date {:}".format( + file_entry.name, file_entry.timestamp + ) ) - tr = pyLSV2.TableReader() - pockets = tr.parse_table(local_recive_path) - spindel = list( - filter(lambda pocket: pocket["P"] == spindel_lable, pockets) - )[0] - print("Current tool number {T} with name {TNAME}".format(**spindel)) - - # read error messages via LSV2, works only on iTNC controls - if con.is_itnc(): - e_m = con.get_error_messages() - print("Number of currently ative error messages: {:d}".format(len(e_m))) - for i, msg in enumerate(e_m): - print("Error {:d} : {:s}".format(i, msg["Text"])) - - # list all NC-Programms in TNC partition - h_files = con.get_file_list(path="TNC:", pattern=r"[\$A-Za-z0-9_-]*\.[hH]$") - print("Found {:d} Klartext programs: {:}".format(len(h_files), h_files)) - i_files = con.get_file_list(path="TNC:", pattern=r"[\$A-Za-z0-9_-]*\.[iI]$") - print("Found {:d} DIN/ISO programs: {:}".format(len(i_files), i_files)) - - con.disconnect() + + print("# file search") + h_files = con.get_file_list(path="TNC:", pattern=r"[\$A-Za-z0-9_-]*\.[hH]$") + print("## found {:d} klartext programs on TNC drive".format(len(h_files))) + i_files = con.get_file_list(path="TNC:", pattern=r"[\$A-Za-z0-9_-]*\.[iI]$") + print("## found {:d} ISO programs on TNC drive".format(len(i_files))) + + print("Read spindle tool information") + t_info = con.spindle_tool_status() + if t_info is not None: + print("# direct reading of current tool successful") + print( + "# current tool in spindle: {:d}.{:d} '{:s}'".format( + t_info.number, t_info.index, t_info.name + ) + ) + else: + print("# direct reading of current tool not supported for this control") diff --git a/scripts/lsv2cmd.py b/scripts/lsv2cmd.py index 39b03f3..e2d63e3 100644 --- a/scripts/lsv2cmd.py +++ b/scripts/lsv2cmd.py @@ -3,17 +3,21 @@ """This script lets you use some of the functions included in pyLSV2 via the command line. """ +import os import sys import logging import argparse import re import socket -from pathlib import Path import pyLSV2 -REMOTE_PATH_REGEX = r"^(?Plsv2):\/\/(?P[\w\.]*)(?::(?P\d{2,5}))?(?:\/(?P(TNC|PLC):))(?P(\/[\$\.\w\d_-]+)*)\/?$" -logging.basicConfig(level=logging.INFO) +__author__ = "drunsinn" +__license__ = "MIT" +__version__ = "1.0" +__email__ = "dr.unsinn@googlemail.com" + +REMOTE_PATH_REGEX = r"^(?Plsv2(\+ssh)?):\/\/(?P[\w\.-]*)(?::(?P\d{2,5}))?(?:\/(?P(TNC|PLC):))(?P(\/[\$\.\w\d_-]+)*)\/?$" if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -29,6 +33,7 @@ help="destination file. Either local path or URL with format lsv2://:/TNC:/", type=str, ) + log_group = parser.add_mutually_exclusive_group() log_group.add_argument( "-d", @@ -46,7 +51,9 @@ action="store_const", dest="loglevel", const=logging.INFO, + default=logging.WARNING, ) + parser.add_argument( "-t", "--timeout", help="timeout duration in seconds", type=float, default=10.0 ) @@ -60,26 +67,34 @@ args = parser.parse_args() logging.basicConfig(level=args.loglevel) + logger = logging.getLogger("lsv2cmd") - logging.debug('Start logging with level "%s"', logging.getLevelName(args.loglevel)) + logger.debug("Start logging with level '%s'", logging.getLevelName(args.loglevel)) + logger.debug("Source Path: %s", args.source) + logger.debug("Destination Path: %s", args.destination) source_is_remote = False dest_is_remote = False - host_machine = None + host_machine = "" host_port = 19000 use_ssh = False + source_path = "" + dest_path = "" + source_match = re.match(REMOTE_PATH_REGEX, args.source) + logger.debug("result of regex for source: %s", source_match) + if source_match is not None: source_is_remote = True source_path = source_match.group("drive") + source_match.group("path") - host_machine = source_match.group("host") + host_machine = str(source_match.group("host")) if source_match.group("port") is not None: host_port = int(source_match.group("port")) - if source_match.group("prot") == "ssh": + if "ssh" in source_match.group("prot"): use_ssh = True - logging.info( + logger.info( "Source path %s is on remote %s:%d via %s", source_path, host_machine, @@ -87,28 +102,29 @@ source_match.group("prot"), ) else: - source_path = Path(args.source) - logging.info("Source path %s is local", source_path.resolve()) + source_path = args.source + logger.info("Source path %s is local", os.path.abspath(source_path)) dest_match = re.match(REMOTE_PATH_REGEX, args.destination) + logger.debug("result of regex for destination: %s", dest_match) + if dest_match is not None: dest_is_remote = True dest_path = dest_match.group("drive") + dest_match.group("path") - if source_is_remote and host_machine != dest_match.group("host"): - logging.error( - 'Cant copy between different remotes "%s" and "%s"', + logger.error( + "Can't copy between different remotes '%s' and '%s'", host_machine, dest_match.group("host"), ) sys.exit(-1) - host_machine = dest_match.group("host") + host_machine = str(dest_match.group("host")) if dest_match.group("port") is not None: host_port = int(dest_match.group("port")) - if dest_match.group("prot") == "ssh": + if "ssh" in dest_match.group("prot"): use_ssh = True - logging.info( + logger.info( "Destination path %s is on remote %s:%s via %s", dest_path, host_machine, @@ -116,8 +132,8 @@ dest_match.group("prot"), ) else: - dest_path = Path(args.destination) - logging.info("Destination path %s is local", dest_path.resolve()) + dest_path = args.destination + logger.info("Destination path %s is local", os.path.abspath(dest_path)) if use_ssh: import sshtunnel @@ -128,52 +144,54 @@ ssh_forwarder.start() host_machine = "127.0.0.1" host_port = ssh_forwarder.local_bind_port - logging.info("SSH tunnel established. local port is %d", host_port) + logger.info("SSH tunnel established. local port is %d", host_port) try: con = pyLSV2.LSV2(hostname=host_machine, port=host_port, timeout=args.timeout) con.connect() except socket.gaierror as ex: - logging.error('An Exception occurred: "%s"', ex) - logging.error('Could not resove host information: "%s"', host_machine) + logger.error("An Exception occurred: '%s'", ex) + logger.error("Could not resove host information: '%s'", host_machine) sys.exit(-2) if source_is_remote: - file_info = con.get_file_info(remote_file_path=source_path) + file_info = con.file_info(remote_file_path=str(source_path)) if not file_info: - logging.error('source file dose not exist on remote: "%s"', source_path) + logger.error("source file dose not exist on remote: '%s'", source_path) sys.exit(-3) - elif file_info["is_directory"]: - logging.error( - 'source on remote is not file but directory: "%s"', source_path + elif file_info.is_directory or file_info.is_drive: + logger.error( + "source on remote is not file but directory: '%s'", source_path ) sys.exit(-4) else: - if not source_path.exists(): - if source_path.is_file(): - logging.error('source file dose not exist: "%s"', source_path) + if os.path.exists(source_path): + logger.debug("source file exists") + else: + if os.path.isfile(source_path): + logger.error("source file dose not exist: '%s'", source_path) sys.exit(-5) - else: # source_path.is_dir(): - logging.error('source folder dose not exist: "%s"', source_path) + else: + logger.error("source folder dose not exist: '%s'", source_path) sys.exit(-6) success = False if source_is_remote and dest_is_remote: - logging.debug("Local copy on remote") - success = con.copy_local_file(source_path=source_path, target_path=dest_path) + logger.debug("Local copy on remote") + success = con.copy_remote_file(source_path=source_path, target_path=dest_path) elif source_is_remote and not dest_is_remote: - logging.debug("copy from remote to this device") + logger.debug("copy from remote to local") success = con.recive_file( remote_path=source_path, local_path=dest_path, override_file=args.force ) else: - logging.debug("copy from this device to remote") + logger.debug("copy from local to remote") success = con.send_file( local_path=source_path, remote_path=dest_path, override_file=args.force ) con.disconnect() if success: - logging.info("File copied successful") + logger.info("File copied successful") sys.exit(0) sys.exit(-10) diff --git a/scripts/ssh_tunnel_demo.py b/scripts/ssh_tunnel_demo.py index cf66b9b..207b86c 100644 --- a/scripts/ssh_tunnel_demo.py +++ b/scripts/ssh_tunnel_demo.py @@ -13,6 +13,11 @@ import pyLSV2 from sshtunnel import SSHTunnelForwarder +__author__ = "drunsinn" +__license__ = "MIT" +__version__ = "1.0" +__email__ = "dr.unsinn@googlemail.com" + logging.basicConfig(level=logging.INFO) if __name__ == "__main__": @@ -38,17 +43,18 @@ ) print("Establish regular LSV2 connection via local port") - lsv2 = pyLSV2.LSV2( + + con = pyLSV2.LSV2( "127.0.0.1", port=ssh_forwarder.local_bind_port, timeout=5, safe_mode=False ) - lsv2.connect() + con.connect() print( - 'Connected to "{Control}" with NC Software "{NC_Version}"'.format( - **lsv2.get_versions() + "Connected to '{:s}' with NC Software '{:s}'".format( + con.versions.control, con.versions.nc_sw ) ) print("Close Connection") - lsv2.disconnect() + con.disconnect() print("Close SSH tunnel") ssh_forwarder.stop() diff --git a/scripts/tab2csv.py b/scripts/tab2csv.py new file mode 100644 index 0000000..8f6a350 --- /dev/null +++ b/scripts/tab2csv.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""script to convert table files to csv""" +import argparse +import logging +import pathlib +import sys + +from pyLSV2 import NCTable + +__author__ = "drunsinn" +__license__ = "MIT" +__version__ = "1.0" +__email__ = "dr.unsinn@googlemail.com" + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="command line script parsing table files" + ) + parser.add_argument("source", help="table file to parse", type=pathlib.Path) + parser.add_argument( + "--decimal_char", help="override local decimal char", type=str, default="," + ) + log_group = parser.add_mutually_exclusive_group() + log_group.add_argument( + "-d", + "--debug", + help="enable log level DEBUG", + action="store_const", + dest="loglevel", + const=logging.DEBUG, + default=logging.WARNING, + ) + log_group.add_argument( + "-v", + "--verbose", + help="enable log level INFO", + action="store_const", + dest="loglevel", + const=logging.INFO, + ) + args = parser.parse_args() + + logging.basicConfig(level=args.loglevel) + logging.debug('Start logging with level "%s"', logging.getLevelName(args.loglevel)) + + if args.source.is_file(): + nc_table = NCTable.parse_table(args.source) + print("number of rows in table: %d" % len(nc_table)) + print("columns in table %s" % nc_table.column_names) + if nc_table.has_unit: + print("table file specifies unit system") + if nc_table.is_metric: + print("values are metric") + else: + print("values are imperial") + else: + print("table has no apparent unit system") + + csv_file_name = args.source.with_suffix(".csv") + print("write table to file %s" % csv_file_name) + nc_table.dump_csv(csv_file_name, args.decimal_char) + print("csv export finished") + + else: + print("table file does not exits %s" % args.source) + sys.exit(-1) + + sys.exit(0) diff --git a/scripts/table_reader_demo.py b/scripts/table_reader_demo.py deleted file mode 100644 index b1928d4..0000000 --- a/scripts/table_reader_demo.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import logging -import pathlib -from pyLSV2 import TableReader, NCTabel - -logging.basicConfig(level=logging.DEBUG) - -if __name__ == "__main__": - tr = TableReader() - - input_file = pathlib.Path("../data/tool.t") - - # read table - nc_table = tr.parse_table(input_file) - - # safe table format to json - if nc_table.version is not None: - format_file_name = "output_format_%s.json" % str(nc_table.version) - else: - format_file_name = "output_format_generic.json" - format_file_path = input_file.parents[0] / format_file_name - with open(format_file_path, "w", encoding="utf-8") as fp: - fp.write(nc_table.format_to_json()) - - # combine two tables - second_file = pathlib.Path("../data/tool2.t") - output_table = input_file.parents[0] / "combined.t" - second_table = tr.parse_table(second_file) - nc_table.extend_rows(second_table.rows) - nc_table.dump(output_table, renumber_column="T") diff --git a/setup.py b/setup.py index ffbe2b6..78e78e5 100644 --- a/setup.py +++ b/setup.py @@ -2,10 +2,11 @@ # -*- coding: utf-8 -* """package configuration for pyLSV2""" from setuptools import find_packages, setup -from pyLSV2 import __version__, __doc__ +from pyLSV2 import __doc__, __version__, __author__, __license__, __email__ setup( name="pyLSV2", + python_requires=">=3.4", packages=find_packages( include=[ "pyLSV2", @@ -18,19 +19,30 @@ description=__doc__, long_description=open("README.md").read(), long_description_content_type="text/markdown", - author="drunsinn", - author_email="dr.unsinn@googlemail.com", + author=__author__, + author_email=__email__, url="https://github.com/drunsinn/pyLSV2", - license="MIT", + license=__license__, install_requires=[], - scripts=["scripts/lsv2cmd.py"], - zip_safe=True, + scripts=["scripts/lsv2cmd.py", "scripts/tab2csv.py"], keywords="LSV2 cnc communication transfer plc", classifiers=[ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 4 - Beta", "Topic :: System :: Archiving", + "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Monitoring", + "Typing :: Typed", ], ) diff --git a/tests/conftest.py b/tests/conftest.py index a7ddbfa..afdf192 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ def address(request): """process commandline option 'address'""" par = request.config.getoption("--address") if par is None: - par = "localhost" + par = "192.168.56.101" return par diff --git a/tests/test_connection.py b/tests/test_connection.py index eb83fba..b3a8fef 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -5,32 +5,64 @@ import pyLSV2 -def test_login(address, timeout): +def test_login(address: str, timeout: float): """test to see if login and safe mode works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - + assert lsv2.login(login=pyLSV2.Login.INSPECT) is True assert lsv2.login(login=pyLSV2.Login.FILETRANSFER) is True assert lsv2.logout(login=pyLSV2.Login.FILETRANSFER) is True assert lsv2.login(login=pyLSV2.Login.MONITOR) is True - + assert lsv2.login(login=pyLSV2.Login.DNC) is False lsv2.disconnect() -def test_read_versions(address, timeout): + +def test_read_versions(address: str, timeout: float): """test if login and basic query works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - assert lsv2.get_versions() is not False + assert (len(lsv2.versions.control) > 1) is True lsv2.disconnect() -def test_unsafe_logins(address, timeout): + +def test_context_manager(address: str, timeout: float): + """test if login and basic query works""" + with pyLSV2.LSV2(address, port=19000, timeout=timeout) as lsv2: + assert lsv2.versions.type is not pyLSV2.ControlType.UNKNOWN + + +def test_switching_safe_mode(address: str, timeout: float): + """check if enabling and diabeling safe mode works""" + with pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) as lsv2: + assert lsv2.login(login=pyLSV2.Login.DNC) is False + lsv2.switch_safe_mode(enable_safe_mode=False) + assert lsv2.login(login=pyLSV2.Login.DNC) is True + lsv2.logout(login=pyLSV2.Login.DNC) + lsv2.switch_safe_mode(enable_safe_mode=True) + assert lsv2.login(login=pyLSV2.Login.DNC) is False + + +def test_login_with_password(address: str, timeout: float): + """check if logging in with a password works""" + with pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) as lsv2: + if not ( + lsv2.versions.control.startswith("iTNC530 Program") + or lsv2.versions.control.startswith("iTNC530Program") + ): + # logon to plc is not locked? + lsv2.logout(pyLSV2.Login.FILEPLC) + assert lsv2.login(login=pyLSV2.Login.FILEPLC) is False + assert lsv2.login(login=pyLSV2.Login.FILEPLC, password="807667") is True + + +def test_unsafe_logins(address: str, timeout: float): """test to see if login without safe mode works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() assert lsv2.login(login=pyLSV2.Login.DNC) is True - lsv2.disconnect() \ No newline at end of file + lsv2.disconnect() diff --git a/tests/test_file_functions.py b/tests/test_file_functions.py index dd27fcd..1036abf 100644 --- a/tests/test_file_functions.py +++ b/tests/test_file_functions.py @@ -5,32 +5,32 @@ import pyLSV2 -def test_read_info(address, timeout): +def test_read_info(address: str, timeout: float): """test if reading of file information works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): mdi_path = "TNC:\\$MDI.H" - elif lsv2.is_pilot(): + elif lsv2.versions.is_pilot(): mdi_path = "TNC:\\nc_prog\\ncps\\PGM01.nc" else: mdi_path = "TNC:\\nc_prog\\$mdi.h" assert lsv2.change_directory(remote_directory="TNC:\\nc_prog") is True - assert lsv2.get_directory_info() is not False - assert lsv2.get_file_info(remote_file_path=mdi_path) is not False - assert lsv2.get_directory_content() is not False - assert lsv2.get_drive_info() is not False + assert lsv2.directory_info() is not False + assert lsv2.file_info(remote_file_path=mdi_path) is not None + assert lsv2.directory_content() is not False + assert lsv2.drive_info() is not False lsv2.disconnect() -def test_directory_functions(address, timeout): +def test_directory_functions(address: str, timeout: float): """test if functions to change, create and delete directories work""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) - test_dir = "TNC:/nc_prog/pyLSV2_test/" + test_dir = "TNC:\\nc_prog\\pyLSV2_test_dir_func\\" lsv2.connect() assert lsv2.change_directory("TNC:\\nc_prog") is True @@ -39,7 +39,7 @@ def test_directory_functions(address, timeout): assert lsv2.change_directory(test_dir + "T1\\T2\\T3") is False assert lsv2.change_directory(test_dir + "T1\\T2") is True - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): assert lsv2.delete_empty_directory(test_dir + "T1\\T2") is True assert lsv2.change_directory("TNC:\\nc_prog") is True else: @@ -52,17 +52,17 @@ def test_directory_functions(address, timeout): lsv2.disconnect() -def test_remote_file_functions(address, timeout): +def test_remote_file_functions(address: str, timeout: float): """test if functions for manipulating the remote file system work""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - test_dir = "TNC:\\nc_prog\\pyLSV2_test\\" + test_dir = "TNC:\\nc_prog\\pyLSV2_test_file_func\\" - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): mdi_dir = "TNC:\\" mdi_name = "$MDI.H" - elif lsv2.is_pilot(): + elif lsv2.versions.is_pilot(): mdi_dir = "TNC:\\nc_prog\\ncps\\" mdi_name = "PGM01.nc" else: @@ -77,33 +77,31 @@ def test_remote_file_functions(address, timeout): assert lsv2.change_directory(test_dir) is True - if lsv2.is_tnc(): + if lsv2.versions.is_tnc(): # only test for tnc controls assert ( - lsv2.copy_local_file(source_path=mdi_dir + mdi_name, target_path=test_dir) + lsv2.copy_remote_file(source_path=mdi_dir + mdi_name, target_path=test_dir) is True ) - assert lsv2.get_file_info(test_dir + mdi_name) is not False + assert lsv2.file_info(test_dir + mdi_name) is not None assert lsv2.delete_file(test_dir + mdi_name) is True assert ( - lsv2.copy_local_file(source_path=mdi_dir + mdi_name, target_path=test_file_1) + lsv2.copy_remote_file(source_path=mdi_dir + mdi_name, target_path=test_file_1) is True ) - assert lsv2.get_file_info(test_file_1) is not False + assert lsv2.file_info(test_file_1) is not None assert lsv2.change_directory(remote_directory=mdi_dir) is True - assert lsv2.copy_local_file(source_path=mdi_name, target_path=test_file_2) is True + assert lsv2.copy_remote_file(source_path=mdi_name, target_path=test_file_2) is True - assert lsv2.get_file_info(test_file_2) is not False + assert lsv2.file_info(test_file_2) is not None - assert ( - lsv2.move_local_file(source_path=test_file_2, target_path=test_file_3) is True - ) - assert lsv2.get_file_info(test_file_3) is not False + assert lsv2.move_file(source_path=test_file_2, target_path=test_file_3) is True + assert lsv2.file_info(test_file_3) is not None assert lsv2.delete_file(test_file_3) is True - assert lsv2.get_file_info(test_file_3) is False + assert lsv2.file_info(test_file_3) is None assert lsv2.delete_file(test_file_1) is True @@ -112,23 +110,24 @@ def test_remote_file_functions(address, timeout): lsv2.disconnect() -def test_path_formating(address, timeout): +def test_path_formating(address: str, timeout: float): """test if reading of file information with / instead of \\ as path separator""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): mdi_path = "TNC:/$MDI.H" - elif lsv2.is_pilot(): + elif lsv2.versions.is_pilot(): mdi_path = "TNC:/nc_prog/ncps/PGM01.nc" else: mdi_path = "TNC:/nc_prog/$mdi.h" - assert lsv2.get_file_info(mdi_path) is not False + assert lsv2.file_info(mdi_path) is not None lsv2.disconnect() -def test_file_search(address, timeout): + +def test_file_search(address: str, timeout: float): """test if searching for files works. assumes that at least one file is present in root directory""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() @@ -139,14 +138,42 @@ def test_file_search(address, timeout): assert result2 > 0 assert (result2 > result1) is True - - if lsv2.is_itnc(): - assert len(lsv2.get_file_list(pyLSV2.DriveName.TNC, descend=False, pattern=pyLSV2.REGEX_FILE_NAME_H)) > 0 - elif lsv2.is_pilot(): - file_path = pyLSV2.DriveName.TNC + pyLSV2.PATH_SEP + "nc_prog" + pyLSV2.PATH_SEP + "ncps" - assert len(lsv2.get_file_list(file_path, descend=False, pattern=pyLSV2.REGEX_FILE_NAME_H)) > 0 + if lsv2.versions.is_itnc(): + assert ( + len( + lsv2.get_file_list( + pyLSV2.DriveName.TNC, + descend=False, + pattern=pyLSV2.REGEX_FILE_NAME_H, + ) + ) + > 0 + ) + elif lsv2.versions.is_pilot(): + file_path = ( + pyLSV2.DriveName.TNC + + pyLSV2.PATH_SEP + + "nc_prog" + + pyLSV2.PATH_SEP + + "ncps" + ) + assert ( + len( + lsv2.get_file_list( + file_path, descend=False, pattern=pyLSV2.REGEX_FILE_NAME_H + ) + ) + > 0 + ) else: file_path = pyLSV2.DriveName.TNC + pyLSV2.PATH_SEP + "nc_prog" - assert len(lsv2.get_file_list(file_path, descend=False, pattern=pyLSV2.REGEX_FILE_NAME_H)) > 0 + assert ( + len( + lsv2.get_file_list( + file_path, descend=False, pattern=pyLSV2.REGEX_FILE_NAME_H + ) + ) + > 0 + ) - lsv2.disconnect() \ No newline at end of file + lsv2.disconnect() diff --git a/tests/test_keys.py b/tests/test_keys.py index a5fdedb..e05328a 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -2,10 +2,12 @@ # -*- coding: utf-8 -*- """test to see if keypress functions work""" +import time + import pyLSV2 -def test_key_press_sim(address, timeout): +def test_key_press_sim(address: str, timeout: float): """test to see if reading of machine parameters works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() @@ -15,8 +17,16 @@ def test_key_press_sim(address, timeout): assert lsv2.set_keyboard_access(False) is True assert lsv2.send_key_code(pyLSV2.KeyCode.MODE_MANUAL) is True + time.sleep(1) + assert lsv2.execution_state() is pyLSV2.ExecState.MANUAL assert lsv2.send_key_code(pyLSV2.KeyCode.MODE_PGM_EDIT) is True + time.sleep(1) + assert lsv2.execution_state() is pyLSV2.ExecState.MANUAL + + assert lsv2.send_key_code(pyLSV2.KeyCode.MODE_AUTOMATIC) is True + time.sleep(1) + assert lsv2.execution_state() is pyLSV2.ExecState.AUTOMATIC assert lsv2.set_keyboard_access(True) is True diff --git a/tests/test_machine_parameters.py b/tests/test_machine_parameters.py index fba1686..c9a8c9a 100644 --- a/tests/test_machine_parameters.py +++ b/tests/test_machine_parameters.py @@ -5,12 +5,12 @@ import pyLSV2 -def test_read_machine_parameter(address, timeout): +def test_read_machine_parameter(address: str, timeout: float): """test to see if reading of machine parameters works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): # old style assert lsv2.get_machine_parameter("7230.0") is not False else: @@ -20,24 +20,26 @@ def test_read_machine_parameter(address, timeout): lsv2.disconnect() -def test_rw_machine_parameter(address, timeout): +def test_rw_machine_parameter(address: str, timeout: float): """test to see if reading and writing of machine parameters works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): # old style parameter_name = "7230.0" else: # new stype parameter_name = "CfgDisplayLanguage.ncLanguage" - lsv2.login(pyLSV2.Login.PLCDEBUG) + assert lsv2.login(pyLSV2.Login.PLCDEBUG) is True + current_value = lsv2.get_machine_parameter(parameter_name) assert ( lsv2.set_machine_parameter(parameter_name, current_value, safe_to_disk=False) is not False ) + lsv2.logout(pyLSV2.Login.PLCDEBUG) lsv2.disconnect() diff --git a/tests/test_misc.py b/tests/test_misc.py index 2f5a2b6..d812fcc 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,7 +7,7 @@ import pyLSV2 -def test_grab_screen_dump(address, timeout): +def test_grab_screen_dump(address: str, timeout: float): """test if creating a screen dump works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() diff --git a/tests/test_plc_read.py b/tests/test_plc_read.py index a3c37d4..b6bec98 100644 --- a/tests/test_plc_read.py +++ b/tests/test_plc_read.py @@ -5,59 +5,39 @@ import pyLSV2 -def test_plc_read(address, timeout): +def test_plc_read(address: str, timeout: float): """test to see if reading of plc data works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - assert ( - lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.MARKER, count=1) - is not False - ) - assert ( - lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.WORD, count=1) - is not False - ) - assert ( - lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.DWORD, count=1) - is not False - ) - assert ( - lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.STRING, count=1) - is not False - ) - assert ( - lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.INPUT, count=1) - is not False - ) - assert ( - lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.OUTPUT_WORD, count=1) - is not False - ) + assert lsv2.login(pyLSV2.Login.PLCDEBUG) is True + + assert lsv2.read_plc_memory(0, pyLSV2.MemoryType.MARKER, 1) is not False + assert lsv2.read_plc_memory(0, pyLSV2.MemoryType.WORD, 1) is not False + assert lsv2.read_plc_memory(0, pyLSV2.MemoryType.DWORD, 1) is not False + assert lsv2.read_plc_memory(0, pyLSV2.MemoryType.STRING, 1) is not False + assert lsv2.read_plc_memory(0, pyLSV2.MemoryType.INPUT, 1) is not False + assert lsv2.read_plc_memory(0, pyLSV2.MemoryType.OUTPUT_WORD, 1) is not False + + lsv2.logout(pyLSV2.Login.PLCDEBUG) lsv2.disconnect() -def test_plc_read_marker(address, timeout): +def test_plc_read_marker(address: str, timeout: float): """test reading of plc markers""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - marker_data_0 = lsv2.read_plc_memory( - address=0, mem_type=pyLSV2.MemoryType.MARKER, count=1 - ) + marker_data_0 = lsv2.read_plc_memory(0, pyLSV2.MemoryType.MARKER, 1) assert isinstance(marker_data_0, (list,)) is True assert (len(marker_data_0) == 1) is True - marker_data_1 = lsv2.read_plc_memory( - address=1, mem_type=pyLSV2.MemoryType.MARKER, count=1 - ) + marker_data_1 = lsv2.read_plc_memory(1, pyLSV2.MemoryType.MARKER, 1) assert isinstance(marker_data_1, (list,)) is True assert (len(marker_data_1) == 1) is True - marker_data = lsv2.read_plc_memory( - address=0, mem_type=pyLSV2.MemoryType.MARKER, count=2 - ) + marker_data = lsv2.read_plc_memory(0, pyLSV2.MemoryType.MARKER, 2) assert (len(marker_data) == 2) is True assert (marker_data[0] == marker_data_0[0]) is True assert (marker_data[1] == marker_data_1[0]) is True @@ -65,20 +45,20 @@ def test_plc_read_marker(address, timeout): lsv2.disconnect() -def test_plc_read_string(address, timeout): +def test_plc_read_string(address: str, timeout: float): """test reading of plc strings""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - data_0 = lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.STRING, count=1) + data_0 = lsv2.read_plc_memory(0, pyLSV2.MemoryType.STRING, 1) assert isinstance(data_0, (list,)) is True assert (len(data_0) == 1) is True - data_1 = lsv2.read_plc_memory(address=1, mem_type=pyLSV2.MemoryType.STRING, count=1) + data_1 = lsv2.read_plc_memory(1, pyLSV2.MemoryType.STRING, 1) assert isinstance(data_1, (list,)) is True assert (len(data_1) == 1) is True - data = lsv2.read_plc_memory(address=0, mem_type=pyLSV2.MemoryType.STRING, count=2) + data = lsv2.read_plc_memory(0, pyLSV2.MemoryType.STRING, 2) assert (len(data) == 2) is True assert (data[0] == data_0[0]) is True assert (data[1] == data_1[0]) is True @@ -86,29 +66,28 @@ def test_plc_read_string(address, timeout): lsv2.disconnect() -def test_plc_read_errors(address, timeout): +def test_plc_read_errors(address: str, timeout: float): """test error states for reading plc data""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - num_words = lsv2.get_system_parameter()["Words"] + num_words = lsv2.parameters.number_of_words + + with pytest.raises(pyLSV2.LSV2InputException) as exc_info: + _ = lsv2.read_plc_memory(0, pyLSV2.MemoryType.WORD, (num_words + 1)) - with pytest.raises(ValueError) as exc_info: - data = lsv2.read_plc_memory( - address=0, mem_type=pyLSV2.MemoryType.WORD, count=(num_words + 1) - ) exception_raised = exc_info.value - assert isinstance(exception_raised, (ValueError,)) is True + assert isinstance(exception_raised, (pyLSV2.LSV2InputException,)) is True lsv2.disconnect() -def test_data_path_read(address, timeout): +def test_data_path_read(address: str, timeout: float): """test to see if reading via data path works. run only on iTNC""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): assert lsv2.read_data_path("/PLC/memory/K/1") is not None assert lsv2.read_data_path("/PLC/memory/M/1") is not None assert lsv2.read_data_path("/PLC/memory/B/1") is not None diff --git a/tests/test_read_info.py b/tests/test_read_info.py index 5aa2d30..b637b3b 100644 --- a/tests/test_read_info.py +++ b/tests/test_read_info.py @@ -5,59 +5,74 @@ import pyLSV2 -def test_read_pgm_status(address, timeout): +def test_read_pgm_status(address: str, timeout: float): """test if reading the status of the selected program works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - assert lsv2.get_program_status() is not False + assert lsv2.program_status() in [ + pyLSV2.PgmState.CANCELLED, + pyLSV2.PgmState.ERROR, + pyLSV2.PgmState.ERROR_CLEARED, + pyLSV2.PgmState.FINISHED, + pyLSV2.PgmState.IDLE, + pyLSV2.PgmState.INTERRUPTED, + pyLSV2.PgmState.STARTED, + pyLSV2.PgmState.STOPPED, + ] lsv2.disconnect() -def test_read_pgm_stack(address, timeout): +def test_read_pgm_stack(address: str, timeout: float): """test if reading the program stack works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - assert lsv2.get_program_stack() is not False + assert lsv2.program_stack() is not False lsv2.disconnect() -def test_read_execution_state(address, timeout): +def test_read_execution_state(address: str, timeout: float): """test if reading the executions state works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - assert lsv2.get_execution_status() in [0, 1, 2, 3, 4, 5] + assert lsv2.execution_state() in [ + pyLSV2.ExecState.AUTOMATIC, + pyLSV2.ExecState.MANUAL, + pyLSV2.ExecState.MDI, + pyLSV2.ExecState.PASS_REFERENCES, + pyLSV2.ExecState.SINGLE_STEP, + ] lsv2.disconnect() -def test_read_tool_information(address, timeout): +def test_read_tool_information(address: str, timeout: float): """test if reading tool information on iTNC works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - if lsv2.is_itnc(): - assert lsv2.get_spindle_tool_status() is not False + if lsv2.versions.is_itnc(): + assert lsv2.spindle_tool_status() is not False lsv2.disconnect() -def test_read_override_information(address, timeout): +def test_read_override_information(address: str, timeout: float): """test if reading override values works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - assert lsv2.get_override_info() is not False + assert lsv2.override_state() is not False lsv2.disconnect() -def test_read_error_messages(address, timeout): +def test_read_error_messages(address: str, timeout: float): """test if reading error messages on iTNC works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): assert lsv2.get_error_messages() is not False lsv2.disconnect() -def test_read_axes_location(address, timeout): - """test if reading error messages on iTNC works""" +def test_read_axes_location(address: str, timeout: float): + """test if reading axis location works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=False) lsv2.connect() - assert lsv2.get_axes_location() is not False + assert lsv2.axes_location() is not False lsv2.disconnect() diff --git a/tests/test_transfer.py b/tests/test_transfer.py index 8cc92fa..4bd6a30 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -8,15 +8,15 @@ import pyLSV2 -def test_file_recive(address, timeout): +def test_file_recive(address: str, timeout: float): """test if loading a file from the controls works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): mdi_path = "TNC:/$MDI.H" tool_t_path = "TNC:/TOOL.T" - elif lsv2.is_pilot(): + elif lsv2.versions.is_pilot(): mdi_path = "TNC:/nc_prog/ncps/PGM01.nc" tool_t_path = "TNC:/table/toolturn.htt" else: @@ -43,7 +43,7 @@ def test_file_recive(address, timeout): lsv2.disconnect() -def test_file_transfer_binary(address, timeout): +def test_file_transfer_binary(address: str, timeout: float): """test if transferring a file in binary mode works""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() @@ -51,9 +51,9 @@ def test_file_transfer_binary(address, timeout): with tempfile.TemporaryDirectory(suffix=None, prefix="pyLSV2_") as tmp_dir_name: local_send_path = Path("./data/testdata.bmp") local_recive_path = Path(tmp_dir_name).joinpath("test.bmp") - remote_path = pyLSV2.DriveName.TNC + "/" + local_send_path.name + remote_path = pyLSV2.DriveName.TNC + pyLSV2.PATH_SEP + local_send_path.name - assert lsv2.get_file_info(remote_path) is False + assert lsv2.file_info(remote_path) is not True # is False doesn't work... assert ( lsv2.send_file( @@ -90,14 +90,14 @@ def test_file_transfer_binary(address, timeout): lsv2.disconnect() -def test_recive_with_path_formating(address, timeout): - """test if reading of file information with / instead of \\ as path seperator""" +def test_recive_with_path_formating(address: str, timeout: float): + """test if reading of file information with / instead of \\ as path separator""" lsv2 = pyLSV2.LSV2(address, port=19000, timeout=timeout, safe_mode=True) lsv2.connect() - if lsv2.is_itnc(): + if lsv2.versions.is_itnc(): mdi_path = "TNC:/$MDI.H" - elif lsv2.is_pilot(): + elif lsv2.versions.is_pilot(): mdi_path = "TNC:/nc_prog/ncps/PGM01.nc" else: mdi_path = "TNC:/nc_prog/$mdi.h" diff --git a/tests/test_translation.py b/tests/test_translation.py index d532940..3bce79c 100644 --- a/tests/test_translation.py +++ b/tests/test_translation.py @@ -7,14 +7,12 @@ def test_translation(): """simple test if the content of error strings changes between languages""" - text_en = pyLSV2.get_error_text( - error_type=1, error_code=pyLSV2.LSV2Err.T_BD_NO_NEW_FILE, language="en" - ) - # text_de = pyLSV2.get_error_text(error_type=1, error_code=pyLSV2.LSV2_ERROR_T_BD_NO_NEW_FILE, language='de') + test_error = pyLSV2.LSV2Error() + test_error.e_code = pyLSV2.LSV2StatusCode.T_BD_NO_NEW_FILE + test_error.e_type = 1 + text_en = pyLSV2.get_error_text(test_error, language="en") assert ("LSV2_ERROR_T_BD_NO_NEW_FILE" in text_en) is False - # assert ('LSV2_ERROR_T_BD_NO_NEW_FILE' in text_de) is False - # assert (text_en in text_de) is False def test_state_string_conv():