From cc2b9852b1bd75c004f6a814207c006bdcce6f39 Mon Sep 17 00:00:00 2001 From: KOLANICH Date: Wed, 16 Feb 2022 21:27:15 +0300 Subject: [PATCH] Initial commit --- .ci/aptPackagesToInstall.txt | 0 .ci/pythonPackagesToInstallFromGit.txt | 4 + .editorconfig | 12 + .github/.templateMarker | 1 + .github/dependabot.yml | 8 + .github/workflows/CI.yml | 15 + .gitignore | 15 + .gitlab-ci.yml | 51 + Code_Of_Conduct.md | 1 + MANIFEST.in | 4 + ReadMe.md | 72 + SMSD/__init__.py | 0 SMSD/__main__.py | 68 + SMSD/kaitai/__init__.py | 0 SMSD/kaitai/checksum_simple_additive_u1.py | 76 + SMSD/kaitai/smsd_lan.py | 1255 +++++++++++++++++ SMSD/kaitai/smsd_limits.py | 43 + SMSD/kaitai/smsd_modbus.py | 251 ++++ SMSD/lan/__init__.py | 0 SMSD/lan/powerStepSim/Clock.py | 39 + SMSD/lan/powerStepSim/PowerStepSimulator.py | 35 + SMSD/lan/powerStepSim/__init__.py | 43 + SMSD/lan/powerStepSim/gpio.py | 125 ++ .../lan/powerStepSim/instructionProcessors.py | 154 ++ SMSD/lan/powerStepSim/instructions.py | 224 +++ SMSD/lan/powerStepSim/microcode.py | 201 +++ SMSD/lan/powerStepSim/motor.py | 188 +++ SMSD/lan/powerStepSim/motorsData/__init__.py | 94 ++ .../powerStepSim/motorsData/motors_names.tsv | 55 + .../motorsData/proprietary_names.tsv | 6 + .../motorsData/unknown_motors.tsv | 13 + SMSD/lan/powerStepSim/nanoops.py | 133 ++ SMSD/lan/powerStepSim/vm.py | 33 + SMSD/lan/protocol.py | 217 +++ SMSD/lan/serializers.py | 135 ++ SMSD/lan/uart.py | 89 ++ SMSD/text/__init__.py | 2 + SMSD/text/asio.py | 71 + SMSD/text/commands.py | 214 +++ SMSD/text/formats.py | 116 ++ SMSD/text/hash.py | 124 ++ SMSD/text/serial.py | 19 + UNLICENSE | 24 + genHashTable.py | 48 + monotonic_ns_timings.py | 19 + pyproject.toml | 62 + smsd.cpp | 19 + smsd.yug | 92 ++ specs/checksum_simple_additive_u1.ksy | 39 + specs/smsd_lan.ksy | 1177 ++++++++++++++++ specs/smsd_limits.ksy | 13 + specs/smsd_modbus.ksy | 215 +++ tests/build.ninja | 16 + tests/lan_proto_from_pdf.hpp | 334 +++++ tests/lan_proto_test.cpp | 413 ++++++ tests/lan_proto_undocumented.hpp | 34 + .../testData/COMMANDS_RETURN_DATA_Type/0.dat | Bin 0 -> 7 bytes .../testData/COMMANDS_RETURN_DATA_Type/1.dat | Bin 0 -> 7 bytes .../testData/COMMANDS_RETURN_DATA_Type/2.dat | Bin 0 -> 7 bytes tests/testData/LAN_COMMAND_Type_header/0.dat | Bin 0 -> 6 bytes tests/testData/SMSD_CMD_Type/0.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/1.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/10.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/11.dat | 1 + tests/testData/SMSD_CMD_Type/12.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/13.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/2.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/3.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/4.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/5.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/6.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/7.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/8.dat | Bin 0 -> 4 bytes tests/testData/SMSD_CMD_Type/9.dat | Bin 0 -> 4 bytes tests/testData/SMSD_LAN_Config_Type/0.dat | Bin 0 -> 25 bytes tests/testData/commands/0_REQUEST.dat | Bin 0 -> 6 bytes tests/testData/commands/10_CONFIG_SET.dat | Bin 0 -> 31 bytes tests/testData/commands/11_CONFIG_GET.dat | Bin 0 -> 6 bytes tests/testData/commands/12_CONFIG_GET.dat | Bin 0 -> 31 bytes tests/testData/commands/13_ERROR_GET.dat | Bin 0 -> 6 bytes tests/testData/commands/14_ERROR_GET.dat | Bin 0 -> 74 bytes tests/testData/commands/1_REQUEST.dat | Bin 0 -> 14 bytes tests/testData/commands/2_PASSWORD_SET.dat | Bin 0 -> 14 bytes tests/testData/commands/3_RESPONSE_.dat | Bin 0 -> 13 bytes .../commands/4_POWERSTEP01_MOVE_F.dat | Bin 0 -> 10 bytes .../commands/5_POWERSTEP01_GET_SPEED.dat | Bin 0 -> 10 bytes .../commands/6_RESPONSE_COMMAND_GET_SPEED.dat | Bin 0 -> 13 bytes .../commands/7_POWERSTEP01_W_MEM0_1.dat | Bin 0 -> 10 bytes .../commands/8_POWERSTEP01_R_MEM0.dat | Bin 0 -> 6 bytes .../commands/9_POWERSTEP01_R_MEM0_1.dat | Bin 0 -> 10 bytes tests/testData/powerSTEP_STATUS_TypeDef/0.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/1.dat | Bin 0 -> 2 bytes .../testData/powerSTEP_STATUS_TypeDef/10.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/2.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/3.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/4.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/5.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/6.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/7.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/8.dat | Bin 0 -> 2 bytes tests/testData/powerSTEP_STATUS_TypeDef/9.dat | Bin 0 -> 2 bytes tests/tests.py | 134 ++ 102 files changed, 6846 insertions(+) create mode 100644 .ci/aptPackagesToInstall.txt create mode 100644 .ci/pythonPackagesToInstallFromGit.txt create mode 100644 .editorconfig create mode 100644 .github/.templateMarker create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/CI.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Code_Of_Conduct.md create mode 100644 MANIFEST.in create mode 100644 ReadMe.md create mode 100644 SMSD/__init__.py create mode 100644 SMSD/__main__.py create mode 100644 SMSD/kaitai/__init__.py create mode 100644 SMSD/kaitai/checksum_simple_additive_u1.py create mode 100644 SMSD/kaitai/smsd_lan.py create mode 100644 SMSD/kaitai/smsd_limits.py create mode 100644 SMSD/kaitai/smsd_modbus.py create mode 100644 SMSD/lan/__init__.py create mode 100644 SMSD/lan/powerStepSim/Clock.py create mode 100644 SMSD/lan/powerStepSim/PowerStepSimulator.py create mode 100644 SMSD/lan/powerStepSim/__init__.py create mode 100644 SMSD/lan/powerStepSim/gpio.py create mode 100644 SMSD/lan/powerStepSim/instructionProcessors.py create mode 100644 SMSD/lan/powerStepSim/instructions.py create mode 100644 SMSD/lan/powerStepSim/microcode.py create mode 100644 SMSD/lan/powerStepSim/motor.py create mode 100644 SMSD/lan/powerStepSim/motorsData/__init__.py create mode 100644 SMSD/lan/powerStepSim/motorsData/motors_names.tsv create mode 100644 SMSD/lan/powerStepSim/motorsData/proprietary_names.tsv create mode 100644 SMSD/lan/powerStepSim/motorsData/unknown_motors.tsv create mode 100644 SMSD/lan/powerStepSim/nanoops.py create mode 100644 SMSD/lan/powerStepSim/vm.py create mode 100644 SMSD/lan/protocol.py create mode 100644 SMSD/lan/serializers.py create mode 100644 SMSD/lan/uart.py create mode 100644 SMSD/text/__init__.py create mode 100644 SMSD/text/asio.py create mode 100644 SMSD/text/commands.py create mode 100644 SMSD/text/formats.py create mode 100644 SMSD/text/hash.py create mode 100644 SMSD/text/serial.py create mode 100644 UNLICENSE create mode 100755 genHashTable.py create mode 100644 monotonic_ns_timings.py create mode 100644 pyproject.toml create mode 100644 smsd.cpp create mode 100644 smsd.yug create mode 100644 specs/checksum_simple_additive_u1.ksy create mode 100644 specs/smsd_lan.ksy create mode 100644 specs/smsd_limits.ksy create mode 100644 specs/smsd_modbus.ksy create mode 100755 tests/build.ninja create mode 100644 tests/lan_proto_from_pdf.hpp create mode 100644 tests/lan_proto_test.cpp create mode 100644 tests/lan_proto_undocumented.hpp create mode 100644 tests/testData/COMMANDS_RETURN_DATA_Type/0.dat create mode 100644 tests/testData/COMMANDS_RETURN_DATA_Type/1.dat create mode 100644 tests/testData/COMMANDS_RETURN_DATA_Type/2.dat create mode 100644 tests/testData/LAN_COMMAND_Type_header/0.dat create mode 100644 tests/testData/SMSD_CMD_Type/0.dat create mode 100644 tests/testData/SMSD_CMD_Type/1.dat create mode 100644 tests/testData/SMSD_CMD_Type/10.dat create mode 100644 tests/testData/SMSD_CMD_Type/11.dat create mode 100644 tests/testData/SMSD_CMD_Type/12.dat create mode 100644 tests/testData/SMSD_CMD_Type/13.dat create mode 100644 tests/testData/SMSD_CMD_Type/2.dat create mode 100644 tests/testData/SMSD_CMD_Type/3.dat create mode 100644 tests/testData/SMSD_CMD_Type/4.dat create mode 100644 tests/testData/SMSD_CMD_Type/5.dat create mode 100644 tests/testData/SMSD_CMD_Type/6.dat create mode 100644 tests/testData/SMSD_CMD_Type/7.dat create mode 100644 tests/testData/SMSD_CMD_Type/8.dat create mode 100644 tests/testData/SMSD_CMD_Type/9.dat create mode 100644 tests/testData/SMSD_LAN_Config_Type/0.dat create mode 100644 tests/testData/commands/0_REQUEST.dat create mode 100644 tests/testData/commands/10_CONFIG_SET.dat create mode 100644 tests/testData/commands/11_CONFIG_GET.dat create mode 100644 tests/testData/commands/12_CONFIG_GET.dat create mode 100644 tests/testData/commands/13_ERROR_GET.dat create mode 100644 tests/testData/commands/14_ERROR_GET.dat create mode 100644 tests/testData/commands/1_REQUEST.dat create mode 100644 tests/testData/commands/2_PASSWORD_SET.dat create mode 100644 tests/testData/commands/3_RESPONSE_.dat create mode 100644 tests/testData/commands/4_POWERSTEP01_MOVE_F.dat create mode 100644 tests/testData/commands/5_POWERSTEP01_GET_SPEED.dat create mode 100644 tests/testData/commands/6_RESPONSE_COMMAND_GET_SPEED.dat create mode 100644 tests/testData/commands/7_POWERSTEP01_W_MEM0_1.dat create mode 100644 tests/testData/commands/8_POWERSTEP01_R_MEM0.dat create mode 100644 tests/testData/commands/9_POWERSTEP01_R_MEM0_1.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/0.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/1.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/10.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/2.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/3.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/4.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/5.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/6.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/7.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/8.dat create mode 100644 tests/testData/powerSTEP_STATUS_TypeDef/9.dat create mode 100755 tests/tests.py diff --git a/.ci/aptPackagesToInstall.txt b/.ci/aptPackagesToInstall.txt new file mode 100644 index 0000000..e69de29 diff --git a/.ci/pythonPackagesToInstallFromGit.txt b/.ci/pythonPackagesToInstallFromGit.txt new file mode 100644 index 0000000..84e3273 --- /dev/null +++ b/.ci/pythonPackagesToInstallFromGit.txt @@ -0,0 +1,4 @@ +https://github.com/KOLANICH-physics/motorAccelerationPlanner.py.git +https://github.com/KOLANICH-libs/SaneIO.py.git +https://github.com/pyserial/pyserial-asyncio.git +https://github.com/kaitai-io/kaitai_struct_python_runtime.git diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d6117cd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.{yml,yaml,yug,ksy}] +indent_style = space +indent_size = 2 diff --git a/.github/.templateMarker b/.github/.templateMarker new file mode 100644 index 0000000..5e3a3e0 --- /dev/null +++ b/.github/.templateMarker @@ -0,0 +1 @@ +KOLANICH/python_project_boilerplate.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..89ff339 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..7fe33b3 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,15 @@ +name: CI +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-22.04 + steps: + - name: typical python workflow + uses: KOLANICH-GHActions/typical-python-workflow@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a56bb25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +__pycache__ +*.pyc +*.pyo +/*.egg-info +*.srctrlbm +*.srctrldb +build +dist +.eggs +monkeytype.sqlite3 +/.ipynb_checkpoints + +.ninja_log +.ninja_deps +/tests/lan_proto_test diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..366f318 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,51 @@ +image: registry.gitlab.com/kolanich-subgroups/docker-images/fixed_python:latest + +variables: + DOCKER_DRIVER: overlay2 + SAST_ANALYZER_IMAGE_TAG: latest + SAST_DISABLE_DIND: "true" + SAST_CONFIDENCE_LEVEL: 5 + CODECLIMATE_VERSION: latest + +include: + - template: SAST.gitlab-ci.yml + - template: Code-Quality.gitlab-ci.yml + - template: License-Management.gitlab-ci.yml + +build: + tags: + - shared + - linux + stage: build + variables: + GIT_DEPTH: "1" + PYTHONUSERBASE: ${CI_PROJECT_DIR}/python_user_packages + + before_script: + - export PATH="$PATH:$PYTHONUSERBASE/bin" # don't move into `variables` + - apt-get update + # todo: + #- apt-get -y install + #- pip3 install --upgrade + #- python3 ./fix_python_modules_paths.py + + script: + - python3 -m build -nw bdist_wheel + - mv ./dist/*.whl ./dist/SMSD-0.CI-py3-none-any.whl + - pip3 install --upgrade ./dist/*.whl + - coverage run --source=SMSD -m --branch pytest --junitxml=./rspec.xml ./tests/test.py + - coverage report -m + - coverage xml + + coverage: "/^TOTAL(?:\\s+\\d+){4}\\s+(\\d+%).+/" + + cache: + paths: + - $PYTHONUSERBASE + + artifacts: + paths: + - dist + reports: + junit: ./rspec.xml + cobertura: ./coverage.xml diff --git a/Code_Of_Conduct.md b/Code_Of_Conduct.md new file mode 100644 index 0000000..bcaa2bf --- /dev/null +++ b/Code_Of_Conduct.md @@ -0,0 +1 @@ +No codes of conduct! \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..20f0fa8 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include UNLICENSE +include *.md +include tests +include .editorconfig diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..e5b7540 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,72 @@ +SMSD.py [![Unlicensed work](https://raw.githubusercontent.com/unlicense/unlicense.org/master/static/favicon.png)](https://unlicense.org/) +======= +[wheel (GHA via `nightly.link`)](https://nightly.link/KOLANICH-libs/SMSD.py/workflows/CI/master/SMSD-0.CI-py3-none-any.whl) +[![GitHub Actions](https://github.com/KOLANICH-libs/SMSD.py/workflows/CI/badge.svg)](https://github.com/KOLANICH-libs/SMSD.py/actions/) +[![Libraries.io Status](https://img.shields.io/librariesio/github/KOLANICH-libs/SMSD.py.svg)](https://libraries.io/github/KOLANICH-libs/SMSD.py) +[![Code style: antiflash](https://img.shields.io/badge/code%20style-antiflash-FFF.svg)](https://github.com/KOLANICH-tools/antiflash.py) + +## Disclaimers +* the author of this library is not affiliated with `Smart Motor Devices OÜ` or `НПФ Электропривод`; +* + + +A library implementing protocols for Smart Motor Devices SMSD controlling drivers for stepper motors. + +* SMSD-4.2LAN and SMSD-8.0LAN - implemented a protocol and a server under `.lan` submodule. +* SMSD-1.5K, SMSD-3.0, SMC-3 - implemented a client and classes for commands under `.text` submodule. Also implemented parsing of the file formats where programs are stored. + +## SMSD-*LAN +### Server +0. Import the class and instantiate it + +```python +from SMSD.lan.protocol import Server +s = Server() +``` + +1. Create the servr using one of the following ways. +1.a. You can create a server listening on a serial port. + +```python +await s.startUARTServer("/dev/ttyACM0") +``` + +The serial port on Linux can be a `pty` - a virtual one, but it must be created by some other app, like `socat` or [`PyVirtualSerialPorts`](https://github.com/ezramorris/PyVirtualSerialPorts). But see the cavheats! + +Cavheats: +* Wine COM port passthrough: symlinking to `~/.wine/dosdevices/com` simply doesn't work at all. +* VirtualBox COM port passthrough: + a. when a VM is loaded, the virtual device must exist. You may need to restart the server while keeping the VM running. It is possible through use of the virtual serial ports tools mentioned. + b. VirtualBox has the following modes of COM ports passthrough: + * `Host Device` - results in an error when connected to a `pty`. OK when connected to a real device. + * `Host Pipe` + * `pty` - error + * pipe - the communication is unidirectional. + * `Raw File` - the communication is unidirectional. + * **`TCP`** - **Works**! Can be set up as `socat TCP-LISTEN:14379 PTY,link=./host,rawer`. Bonus - wireshark can be used to listen to the communications. + +ToDo: Find a way to create a non-pty virtual serial port on Linux. + +1.b. You can create a TCP server. Address of listening is determined by `s.networkConfig.ip`, by default it is `localhost`. +```python +await s.startTCPServer() +``` + +1.c You can create a TCP server listening for UART-escaped and framed messages. It will allow you to connect VirtualBox to the tool directly without `socat`. The IP is still determined by `s.networkConfig.ip`, but the port is determined by the argument passed to the func. + +```python +await s.startUARTTCPServer(port=port) +``` + +## SMSD-1.5K, SMSD-3.0, SMC-3 + +In my case commands for using in-memory operational buffer and `Direct Control` mode have never worked. Neither with vendor-supplied software, nor with my one. + +**Don't use, unfinished and API is very unstable** + +P.S. I'm a very newbie to AsyncIO, so it is very probably that I use it incorrectly. + +### File formats used +The original `SMC_Program` software stores the user-created programs within pairs of files `.smc` and `._smc`. All the files are plain text files, where the data is stored in lines using `\r\n` separators and [`cp1251` encoding](https://en.wikipedia.org/wiki/Windows-1251). The files are scrambled by applying rotation `(byte[i] + 0x7E) & 0xFF`, to descramble you need `(byte[i] - 0x7E) & 0xFF`. **Both files are required** in order to allow `SMC_Program` to load the program, while technically only one should be enough. +* `.smc` contains a sequence of commands +* `._smc` contains human-readable description of what a command does. diff --git a/SMSD/__init__.py b/SMSD/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SMSD/__main__.py b/SMSD/__main__.py new file mode 100644 index 0000000..d10c7a2 --- /dev/null +++ b/SMSD/__main__.py @@ -0,0 +1,68 @@ +from pathlib import Path + +from plumbum import cli + + +class CLI(cli.Application): + """Tools to work with SMSD controllers.""" + + +@CLI.subcommand("emulator") +class CLIEmulator(cli.Application): + """Starts an emulator.""" + + +@CLIEmulator.subcommand("lan") +class CLIEmulatorLan(cli.Application): + """Starts an emulator for LAN models""" + + +@CLIEmulatorLan.subcommand("net") +class CLIEmulatorLanNet(cli.Application): + def main(self): + import asyncio + + from SMSD.lan.protocol import Server + + s = Server() + l = asyncio.get_event_loop() + l.run_until_complete(s.startTCPServer()) + l.run_until_complete(s.tcpServer.server.serve_forever()) + + +CLIEmulatorLanNet.__doc__ = CLIEmulatorLan.__doc__ + " as a TCP server, to which you can connect using SMC-PROGRAM-LAN running on host." + + +@CLIEmulatorLan.subcommand("net-uart") +class CLIEmulatorLanNetUart(cli.Application): + def main(self): + import asyncio + + from SMSD.lan.protocol import Server + + s = Server() + l = asyncio.get_event_loop() + l.run_until_complete(s.startUARTTCPServer()) + l.run_until_complete(s.uartTCPServer.server.serve_forever()) + + +CLIEmulatorLanNetUart.__doc__ = CLIEmulatorLan.__doc__ + " as a TCP server, to which you can connect VirtualBox by setting up providing a virtual COM port in TCP mode and using using SMC-PROGRAM-LAN in the guest to connect that virtual COM port." + + +@CLIEmulatorLan.subcommand("uart") +class CLIEmulatorLanUart(cli.Application): + def main(self, port: Path): + import asyncio + + from SMSD.lan.protocol import Server + + s = Server() + l = asyncio.get_event_loop() + l.run_until_complete(s.uartServer(port)) + l.run_until_complete(s.uartServer.server.serve_forever()) + + +CLIEmulatorLanUart.__doc__ = CLIEmulatorLan.__doc__ + " as a PTY device. Disclaimer: won't work with VirtualBox and Wine as it is." + +if __name__ == "__main__": + CLI.run() diff --git a/SMSD/kaitai/__init__.py b/SMSD/kaitai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SMSD/kaitai/checksum_simple_additive_u1.py b/SMSD/kaitai/checksum_simple_additive_u1.py new file mode 100644 index 0000000..5256662 --- /dev/null +++ b/SMSD/kaitai/checksum_simple_additive_u1.py @@ -0,0 +1,76 @@ +from pkg_resources import parse_version +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +if parse_version(kaitaistruct.__version__) < parse_version('0.9'): + raise Exception('Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s' % kaitaistruct.__version__) + +class ChecksumSimpleAdditiveU1(KaitaiStruct): + """assert calcChecksum(0xFF, b"abcde") == 238 + assert calcChecksum(0xFF, b"abcdef") == 84 + assert calcChecksum(0xFF, b"abcdefgh") == 35 + assert calcChecksum(0xFF, bytes(range(256))) == 127 + + assert calcChecksum(0, b"abcde") == 239 + assert calcChecksum(0, b"abcdef") == 85 + assert calcChecksum(0, b"abcdefgh") == 36 + assert calcChecksum(0, bytes(range(256))) == 128 + """ + + def __init__(self, initial, data, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.initial = initial + self.data = data + + def _read(self): + pass + + class Iteration(KaitaiStruct): + + def __init__(self, idx, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.idx = idx + + def _read(self): + pass + + @property + def prev(self): + if hasattr(self, '_m_prev'): + return self._m_prev if hasattr(self, '_m_prev') else None + self._m_prev = self._root.initial if self.idx == 0 else self._parent.reduction[self.idx - 1].res + return self._m_prev if hasattr(self, '_m_prev') else None + + @property + def res(self): + if hasattr(self, '_m_res'): + return self._m_res if hasattr(self, '_m_res') else None + self._m_res = self.prev + KaitaiStream.byte_array_index(self._parent.data, self.idx) + return self._m_res if hasattr(self, '_m_res') else None + + @property + def reduction(self): + if hasattr(self, '_m_reduction'): + return self._m_reduction if hasattr(self, '_m_reduction') else None + _pos = self._io.pos() + self._io.seek(0) + self._raw__m_reduction = [None] * len(self.data) + self._m_reduction = [None] * len(self.data) + for i in range(len(self.data)): + self._raw__m_reduction[i] = self._io.read_bytes(0) + _io__raw__m_reduction = KaitaiStream(BytesIO(self._raw__m_reduction[i])) + _t__m_reduction = ChecksumSimpleAdditiveU1.Iteration(i, _io__raw__m_reduction, self, self._root) + _t__m_reduction._read() + self._m_reduction[i] = _t__m_reduction + self._io.seek(_pos) + return self._m_reduction if hasattr(self, '_m_reduction') else None + + @property + def value(self): + if hasattr(self, '_m_value'): + return self._m_value if hasattr(self, '_m_value') else None + self._m_value = self.reduction[len(self.data) - 1].res & 255 + return self._m_value if hasattr(self, '_m_value') else None \ No newline at end of file diff --git a/SMSD/kaitai/smsd_lan.py b/SMSD/kaitai/smsd_lan.py new file mode 100644 index 0000000..75e6ae1 --- /dev/null +++ b/SMSD/kaitai/smsd_lan.py @@ -0,0 +1,1255 @@ +from pkg_resources import parse_version +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from enum import IntEnum +if parse_version(kaitaistruct.__version__) < parse_version('0.9'): + raise Exception('Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s' % kaitaistruct.__version__) +from . import checksum_simple_additive_u1 + +class SmsdLan(KaitaiStruct): + """It is possible to connect to devices either via TCP or via a virtual COM port through USB. + It is required to transfer data as whole information packets, every packet conforms the structure, described in this manual. Every packet contains only one data transmission command. It is not possible to transfer more than one data transmission command inside one information packet. Every information packet should be continuously transferred, without interruptions. After receiving an information packet, the controller handles it and sends a response, the response is sent the same physical line as the command was received. A sequence of bytes in the information packets is inverted – “little-endian”, (Intel). + These parameters can be changed afterwards by commands sent through a USB or Ethernet connection. + RS-232 parameters (USB connection): + · Baud rate - 115200 + · Data bits - 8 + · Parity – none + · Stop bits – 1 + + .. seealso:: + Source - https://smd.ee/manuals/smsd-lan-communication-protocol.pdf + + + .. seealso:: + Source - https://electroprivod.ru/pdf/smsd-4.2lan-communication-protocol.pdf + """ + + class Type(IntEnum): + password = 0 + response = 1 + power_step = 2 + powerstem_write_mem_0 = 3 + powerstem_write_mem_1 = 4 + powerstem_write_mem_2 = 5 + powerstem_write_mem_3 = 6 + powerstem_read_mem_0 = 7 + powerstem_read_mem_1 = 8 + powerstem_read_mem_2 = 9 + powerstem_read_mem_3 = 10 + network_config_set = 11 + network_config_get = 12 + password_set = 13 + error_counters = 14 + unknown_15 = 15 + version_data = 16 + + def __init__(self, limits, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.limits = limits + + def _read(self): + self.header = SmsdLan.Header(self._io, self, self._root) + self.header._read() + self.check_checksum = self._io.read_bytes(0) + _ = self.check_checksum + if not (len(_) == 0 and self.recomputed_checksum.value == self.header.checksum): + raise kaitaistruct.ValidationExprError(self.check_checksum, self._io, u'/seq/1') + if self.header.len: + _on = self.header.type + if _on == SmsdLan.Type.powerstem_read_mem_2: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.Empty(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_read_mem_1: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.Empty(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_read_mem_0: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.Empty(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.version_data: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.VersionData(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.network_config_get: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.Empty(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_write_mem_2: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.PowerstepCommands(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.network_config_set: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.NetworkConfig(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.power_step: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.PowerstepCommand(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.response: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.Response(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_write_mem_0: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.PowerstepCommands(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_read_mem_3: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.Empty(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_write_mem_1: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.PowerstepCommands(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.error_counters: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.ErrorCounters(_io__raw_data, self, self._root) + self.data._read() + elif _on == SmsdLan.Type.powerstem_write_mem_3: + self._raw_data = self._io.read_bytes(self.header.len) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdLan.PowerstepCommands(_io__raw_data, self, self._root) + self.data._read() + else: + self.data = self._io.read_bytes(self.header.len) + + class VersionData(KaitaiStruct): + """undocumented type. But in the manual for another controller something is found. + + .. seealso:: + 6.8. Identification registers - https://smd.ee/manuals/BLSD-20Modbus_PS.pdf + """ + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.hardware = SmsdLan.VersionData.Version(self._io, self, self._root) + self.hardware._read() + self.firmware = SmsdLan.VersionData.Version(self._io, self, self._root) + self.firmware._read() + self.protocol = self._io.read_u1() + + class Version(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.major = self._io.read_u2le() + self.minor = self._io.read_u2le() + + def __repr__(self): + return u'Version(' + str(self.major) + u', ' + str(self.minor) + u')' + + class SmsdChecksum(KaitaiStruct): + + def __init__(self, data, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.data = data + + def _read(self): + self.original_checksum = checksum_simple_additive_u1.ChecksumSimpleAdditiveU1(255, self.data, self._io) + self.original_checksum._read() + + @property + def value(self): + if hasattr(self, '_m_value'): + return self._m_value if hasattr(self, '_m_value') else None + self._m_value = (self.original_checksum.value ^ 255) & 255 + return self._m_value if hasattr(self, '_m_value') else None + + class Empty(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.must_have_zero_size = self._io.read_bytes_full() + _ = self.must_have_zero_size + if not len(_) == 0: + raise kaitaistruct.ValidationExprError(self.must_have_zero_size, self._io, u'/types/empty/seq/0') + + class SpeedVerifyRange(KaitaiStruct): + + def __init__(self, speed, min_allowed, max_allowed, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.speed = speed + self.min_allowed = min_allowed + self.max_allowed = max_allowed + + def _read(self): + self.hack = self._io.read_bytes(0) + _ = self.hack + if not (self.min_allowed <= self.speed and self.speed <= self.max_allowed and (len(_) == 0)): + raise kaitaistruct.ValidationExprError(self.hack, self._io, u'/types/speed_verify_range/seq/0') + + class PowerstepCommands(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.commands = [] + i = 0 + while not self._io.is_eof(): + _t_commands = SmsdLan.PowerstepCommand(self._io, self, self._root) + _t_commands._read() + self.commands.append(_t_commands) + i += 1 + + class MotorMode(KaitaiStruct): + + class MotorModel(IntEnum): + sm4247 = 7 + sm5776 = 25 + sm8680_parallel = 33 + sm8680_serial = 34 + sm110201 = 43 + + def __init__(self, raw, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.raw = raw + + def _read(self): + self.hack = self._io.read_bytes(0) + _ = self.hack + if not (1 <= self.work_current_raw and self.work_current_raw <= 80 and (len(_) == 0)): + raise kaitaistruct.ValidationExprError(self.hack, self._io, u'/types/motor_mode/seq/0') + + @property + def hold_current(self): + """share of an operating current.""" + if hasattr(self, '_m_hold_current'): + return self._m_hold_current if hasattr(self, '_m_hold_current') else None + self._m_hold_current = self.hold_current_percent / 100 + return self._m_hold_current if hasattr(self, '_m_hold_current') else None + + @property + def microstepping_nlog(self): + if hasattr(self, '_m_microstepping_nlog'): + return self._m_microstepping_nlog if hasattr(self, '_m_microstepping_nlog') else None + self._m_microstepping_nlog = self.raw >> 7 & 7 + return self._m_microstepping_nlog if hasattr(self, '_m_microstepping_nlog') else None + + @property + def microstepping_denominator(self): + if hasattr(self, '_m_microstepping_denominator'): + return self._m_microstepping_denominator if hasattr(self, '_m_microstepping_denominator') else None + self._m_microstepping_denominator = 1 << self.microstepping_nlog + return self._m_microstepping_denominator if hasattr(self, '_m_microstepping_denominator') else None + + @property + def work_current(self): + """operating current for the current control mode.""" + if hasattr(self, '_m_work_current'): + return self._m_work_current if hasattr(self, '_m_work_current') else None + self._m_work_current = self.work_current_raw / 10 + return self._m_work_current if hasattr(self, '_m_work_current') else None + + @property + def motor_model(self): + """motor type for the voltage control mode. See the table in the docs for the limitation values.""" + if hasattr(self, '_m_motor_model'): + return self._m_motor_model if hasattr(self, '_m_motor_model') else None + self._m_motor_model = KaitaiStream.resolve_enum(SmsdLan.MotorMode.MotorModel, self.raw >> 1 & 63) + return self._m_motor_model if hasattr(self, '_m_motor_model') else None + + @property + def reserved(self): + if hasattr(self, '_m_reserved'): + return self._m_reserved if hasattr(self, '_m_reserved') else None + self._m_reserved = self.raw >> 21 + return self._m_reserved if hasattr(self, '_m_reserved') else None + + @property + def hold_current_percent(self): + if hasattr(self, '_m_hold_current_percent'): + return self._m_hold_current_percent if hasattr(self, '_m_hold_current_percent') else None + self._m_hold_current_percent = (self.hold_current_raw + 1) * 25 + return self._m_hold_current_percent if hasattr(self, '_m_hold_current_percent') else None + + @property + def is_in_current_mode(self): + """motor control mode + false: voltage + true: current + """ + if hasattr(self, '_m_is_in_current_mode'): + return self._m_is_in_current_mode if hasattr(self, '_m_is_in_current_mode') else None + self._m_is_in_current_mode = self.raw & 1 == 1 + return self._m_is_in_current_mode if hasattr(self, '_m_is_in_current_mode') else None + + @property + def hold_current_raw(self): + if hasattr(self, '_m_hold_current_raw'): + return self._m_hold_current_raw if hasattr(self, '_m_hold_current_raw') else None + self._m_hold_current_raw = self.raw >> 17 & 3 + return self._m_hold_current_raw if hasattr(self, '_m_hold_current_raw') else None + + @property + def program_n(self): + if hasattr(self, '_m_program_n'): + return self._m_program_n if hasattr(self, '_m_program_n') else None + self._m_program_n = self.raw >> 19 & 3 + return self._m_program_n if hasattr(self, '_m_program_n') else None + + @property + def work_current_raw(self): + if hasattr(self, '_m_work_current_raw'): + return self._m_work_current_raw if hasattr(self, '_m_work_current_raw') else None + self._m_work_current_raw = self.raw >> 10 & 127 + return self._m_work_current_raw if hasattr(self, '_m_work_current_raw') else None + + def __repr__(self): + return u'MotorMode<' + (u'A' if self.is_in_current_mode else u'V') + u', ' + u'model=' + str(self.motor_model.value) + u', ' + u'1/' + str(self.microstepping_denominator) + u', ' + str(self.work_current_raw // 10) + u' A, ' + str(self.hold_current_percent) + u'%, ' + str(self.program_n) + u', ' + (str(self.reserved) if self.reserved != 0 else u'') + u'>' + + class ErrorCounters(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.starts = self._io.read_u4le() + self.xt = self._io.read_u4le() + self.timeouts = self._io.read_u4le() + self.chip_powerstep01_init = self._io.read_u4le() + self.chip_w5500_init = self._io.read_u4le() + self.fram_init = self._io.read_u4le() + self.lan = self._io.read_u4le() + self.fram_exchange = self._io.read_u4le() + self.interrupts = self._io.read_u4le() + self.overcurrents = self._io.read_u4le() + self.overvoltages = self._io.read_u4le() + self.overheatings_chip_powerstep01 = self._io.read_u4le() + self.overheatings_brake = self._io.read_u4le() + self.chip_powerstep01_command_transfer = self._io.read_u4le() + self.unkn_uvlo_powerstep = self._io.read_u4le() + self.unkn_stall_powerstep = self._io.read_u4le() + self.program_errors = self._io.read_u4le() + + class Response(KaitaiStruct): + + class Code(IntEnum): + success = 0 + auth_success = 1 + auth_failure = 2 + rate_limited = 3 + wrong_checksum = 4 + wrong_command = 5 + wrong_length = 6 + out_of_range = 7 + fail_write = 8 + fail_read = 9 + fail_program = 10 + fail_write_setup = 11 + no_next = 12 + end_programs = 13 + events_status = 14 + mode = 15 + position_absolute = 16 + position_microstepping_electrical = 17 + speed_current = 18 + speed_min = 19 + speed_max = 20 + instruction_pointer = 21 + relay_set = 22 + relay_clear = 23 + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.powerstep_status = SmsdLan.PowerstepStatus(self._io, self, self._root) + self.powerstep_status._read() + self.code = KaitaiStream.resolve_enum(SmsdLan.Response.Code, self._io.read_u1()) + _on = self.code + if _on == SmsdLan.Response.Code.speed_max: + self._raw_return_data = self._io.read_bytes(4) + _io__raw_return_data = KaitaiStream(BytesIO(self._raw_return_data)) + self.return_data = SmsdLan.Response.DecodeFromInt(self.code, _io__raw_return_data, self, self._root) + self.return_data._read() + elif _on == SmsdLan.Response.Code.speed_min: + self._raw_return_data = self._io.read_bytes(4) + _io__raw_return_data = KaitaiStream(BytesIO(self._raw_return_data)) + self.return_data = SmsdLan.Response.DecodeFromInt(self.code, _io__raw_return_data, self, self._root) + self.return_data._read() + elif _on == SmsdLan.Response.Code.events_status: + self._raw_return_data = self._io.read_bytes(4) + _io__raw_return_data = KaitaiStream(BytesIO(self._raw_return_data)) + self.return_data = SmsdLan.Response.EventsStatus(_io__raw_return_data, self, self._root) + self.return_data._read() + elif _on == SmsdLan.Response.Code.instruction_pointer: + self._raw_return_data = self._io.read_bytes(4) + _io__raw_return_data = KaitaiStream(BytesIO(self._raw_return_data)) + self.return_data = SmsdLan.Response.DecodeFromInt(self.code, _io__raw_return_data, self, self._root) + self.return_data._read() + elif _on == SmsdLan.Response.Code.speed_current: + self._raw_return_data = self._io.read_bytes(4) + _io__raw_return_data = KaitaiStream(BytesIO(self._raw_return_data)) + self.return_data = SmsdLan.Response.DecodeFromInt(self.code, _io__raw_return_data, self, self._root) + self.return_data._read() + elif _on == SmsdLan.Response.Code.position_microstepping_electrical: + self._raw_return_data = self._io.read_bytes(4) + _io__raw_return_data = KaitaiStream(BytesIO(self._raw_return_data)) + self.return_data = SmsdLan.Response.CurrentElectricalStepMicrostep(_io__raw_return_data, self, self._root) + self.return_data._read() + else: + self.return_data = self._io.read_bytes(4) + + class CurrentElectricalStepMicrostep(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.raw = self._io.read_u4le() + + @property + def full_step(self): + if hasattr(self, '_m_full_step'): + return self._m_full_step if hasattr(self, '_m_full_step') else None + self._m_full_step = self.raw >> 7 & 3 + return self._m_full_step if hasattr(self, '_m_full_step') else None + + @property + def microstep(self): + """current microstep inside the current full step (measured as 1/128 of the full step).""" + if hasattr(self, '_m_microstep'): + return self._m_microstep if hasattr(self, '_m_microstep') else None + self._m_microstep = self.raw & 127 + return self._m_microstep if hasattr(self, '_m_microstep') else None + + class EventsStatus(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.has_happenned = self._io.read_u1() + self.is_masking_raw = self._io.read_u1() + self.is_waiting = self._io.read_u1() + + @property + def is_masking(self): + if hasattr(self, '_m_is_masking'): + return self._m_is_masking if hasattr(self, '_m_is_masking') else None + _pos = self._io.pos() + self._io.seek(0) + self._raw__m_is_masking = self._io.read_bytes(0) + _io__raw__m_is_masking = KaitaiStream(BytesIO(self._raw__m_is_masking)) + self._m_is_masking = SmsdLan.EventMaskFromInt(self.is_masking_raw, _io__raw__m_is_masking, self, self._root) + self._m_is_masking._read() + self._io.seek(_pos) + return self._m_is_masking if hasattr(self, '_m_is_masking') else None + + class DecodeFromInt(KaitaiStruct): + + def __init__(self, code, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.code = code + + def _read(self): + self.raw = self._io.read_u4le() + + @property + def value(self): + if hasattr(self, '_m_value'): + return self._m_value if hasattr(self, '_m_value') else None + _pos = self._io.pos() + self._io.seek(0) + _on = self.code + if _on == SmsdLan.Response.Code.speed_max: + self._raw__m_value = self._io.read_bytes(0) + _io__raw__m_value = KaitaiStream(BytesIO(self._raw__m_value)) + self._m_value = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit, self._root.limits.speed_max_max_limit, _io__raw__m_value, self, self._root) + self._m_value._read() + elif _on == SmsdLan.Response.Code.speed_min: + self._raw__m_value = self._io.read_bytes(0) + _io__raw__m_value = KaitaiStream(BytesIO(self._raw__m_value)) + self._m_value = SmsdLan.SpeedVerifyRange(self.raw, 0, self._root.limits.speed_min_max_limit, _io__raw__m_value, self, self._root) + self._m_value._read() + elif _on == SmsdLan.Response.Code.current_speed: + self._raw__m_value = self._io.read_bytes(0) + _io__raw__m_value = KaitaiStream(BytesIO(self._raw__m_value)) + self._m_value = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_value, self, self._root) + self._m_value._read() + elif _on == SmsdLan.Response.Code.instruction_pointer: + self._raw__m_value = self._io.read_bytes(0) + _io__raw__m_value = KaitaiStream(BytesIO(self._raw__m_value)) + self._m_value = SmsdLan.InstructionPointer(self.raw, _io__raw__m_value, self, self._root) + self._m_value._read() + else: + self._m_value = self._io.read_bytes(0) + self._io.seek(_pos) + return self._m_value if hasattr(self, '_m_value') else None + + class EventMaskFromInt(KaitaiStruct): + + def __init__(self, raw, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.raw = raw + + def _read(self): + pass + + class Bit(KaitaiStruct): + + def __init__(self, idx, raw, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.idx = idx + self.raw = raw + + def _read(self): + pass + + @property + def value(self): + if hasattr(self, '_m_value'): + return self._m_value if hasattr(self, '_m_value') else None + self._m_value = self.raw >> self.idx & 1 == 1 + return self._m_value if hasattr(self, '_m_value') else None + + @property + def is_enabled(self): + if hasattr(self, '_m_is_enabled'): + return self._m_is_enabled if hasattr(self, '_m_is_enabled') else None + _pos = self._io.pos() + self._io.seek(0) + self._raw__m_is_enabled = [None] * 8 + self._m_is_enabled = [None] * 8 + for i in range(8): + self._raw__m_is_enabled[i] = self._io.read_bytes(0) + _io__raw__m_is_enabled = KaitaiStream(BytesIO(self._raw__m_is_enabled[i])) + _t__m_is_enabled = SmsdLan.EventMaskFromInt.Bit(i, self.raw, _io__raw__m_is_enabled, self, self._root) + _t__m_is_enabled._read() + self._m_is_enabled[i] = _t__m_is_enabled + self._io.seek(_pos) + return self._m_is_enabled if hasattr(self, '_m_is_enabled') else None + + class PowerstepStatus(KaitaiStruct): + + class AccelerationStatus(IntEnum): + stop = 0 + accelerates = 1 + decelerates = 2 + constant_speed = 3 + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.is_deenergized = self._io.read_bits_int_le(1) != 0 + self.is_ready = self._io.read_bits_int_le(1) != 0 + self.is_sw_on = self._io.read_bits_int_le(1) != 0 + self.has_sw_event_happenned = self._io.read_bits_int_le(1) != 0 + self.is_rotating_direction_forward = self._io.read_bits_int_le(1) != 0 + self.acceleration_status = KaitaiStream.resolve_enum(SmsdLan.PowerstepStatus.AccelerationStatus, self._io.read_bits_int_le(2)) + self.is_command_error = self._io.read_bits_int_le(1) != 0 + self._io.align_to_byte() + self.reserved = self._io.read_u1() + + def __repr__(self): + return u'PowerStepStatus<' + (u'Z' if self.is_deenergized else u'⚡') + u', ' + (u'\ud83d\udca4' if self.is_ready else u'⏳') + u', ' + (u'SW on, ' if self.is_sw_on else u'') + (u'\ud83d\udd0a' if self.has_sw_event_happenned else u'\ud83d\udd07') + u', ' + (u'→' if self.is_rotating_direction_forward else u'←') + u', ' + (u'⏹' if self.acceleration_status == SmsdLan.PowerstepStatus.AccelerationStatus.stop else u'⏩' if self.acceleration_status == SmsdLan.PowerstepStatus.AccelerationStatus.accelerates else u'⏪' if self.acceleration_status == SmsdLan.PowerstepStatus.AccelerationStatus.decelerates else u'⏵') + u', ' + (u'\ud83d\uddd9' if self.is_command_error else u'\ud83d\uddf8') + u', ' + (str(self.reserved) if self.reserved != 0 else u'') + u'>' + + class Header(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.checksum = self._io.read_u1() + self.version = self._io.read_u1() + self.type = KaitaiStream.resolve_enum(SmsdLan.Type, self._io.read_u1()) + self.id = self._io.read_u1() + self.len = self._io.read_u2le() + if not self.len <= 1024: + raise kaitaistruct.ValidationGreaterThanError(1024, self.len, self._io, u'/types/header/seq/4') + + class InstructionPointer(KaitaiStruct): + + def __init__(self, raw, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.raw = raw + + def _read(self): + pass + + @property + def program(self): + if hasattr(self, '_m_program'): + return self._m_program if hasattr(self, '_m_program') else None + self._m_program = self.raw >> 8 & 3 + return self._m_program if hasattr(self, '_m_program') else None + + @property + def command(self): + if hasattr(self, '_m_command'): + return self._m_command if hasattr(self, '_m_command') else None + self._m_command = self.raw & 255 + return self._m_command if hasattr(self, '_m_command') else None + + class PowerstepCommand(KaitaiStruct): + """The structure SMSD_CMD_Type is used in data transmission packets. + """ + + class Opcode(IntEnum): + end = 0 + speed_current_get = 1 + events_status_get = 2 + motor_mode_set = 3 + motor_mode_get = 4 + speed_min_set = 5 + speed_max_set = 6 + acceleration_set = 7 + decelleration_set = 8 + speed_full_step_set = 9 + event_mask_set = 10 + position_absolute_get = 11 + position_microstepping_electrical_get = 12 + status_and_clear_errors_get = 13 + speed_forward = 14 + speed_reverse = 15 + move_steps_forward = 16 + move_steps_reverse = 17 + move_to_position_forward = 18 + move_to_position_reverse = 19 + move_untill_sw_forward = 20 + move_untill_sw_reverse = 21 + move_untill_zero_forward_set_zero = 22 + move_untill_zero_reverse_set_zero = 23 + move_untill_in1_forward_set_label = 24 + move_untill_in1_reverse_set_label = 25 + move_to_recorded_zero = 26 + move_to_recorded_label = 27 + move_to_position = 28 + zero_set = 29 + reset_powerstep01 = 30 + stop_soft_hold = 31 + stop_hard_hold = 32 + stop_soft_deenergize = 33 + stop_hard_deenergize = 34 + sleep = 35 + relay_on = 36 + relay_off = 37 + relay_get = 38 + wait_for_in0 = 39 + wait_for_in1 = 40 + jump = 41 + jump_if_in0 = 42 + jump_if_in1 = 43 + loop = 44 + call = 45 + ret = 46 + program_start_mem0 = 47 + program_start_mem1 = 48 + program_start_mem2 = 49 + program_start_mem3 = 50 + halt = 51 + control_mode_set_en_step_dir = 52 + usb_stop = 53 + speed_min_get = 54 + speed_max_get = 55 + instruction_pointer_get = 56 + jump_if_at_zero = 57 + jump_if_zero = 58 + wait_for_continue = 59 + sleep_interruptible = 60 + move_untill_in1_forward_set_mark = 61 + move_untill_in1_reverse_set_mark = 62 + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.raw = self._io.read_u4le() + + class Loop(KaitaiStruct): + + def __init__(self, raw, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.raw = raw + + def _read(self): + pass + + @property + def cycles(self): + if hasattr(self, '_m_cycles'): + return self._m_cycles if hasattr(self, '_m_cycles') else None + self._m_cycles = self.raw >> 10 & (1 << 10) - 1 + return self._m_cycles if hasattr(self, '_m_cycles') else None + + @property + def commands(self): + if hasattr(self, '_m_commands'): + return self._m_commands if hasattr(self, '_m_commands') else None + self._m_commands = self.raw & (1 << 10) - 1 + return self._m_commands if hasattr(self, '_m_commands') else None + + class ZeroInt(KaitaiStruct): + + def __init__(self, input, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.input = input + + def _read(self): + self.hack = self._io.read_bytes(0) + _ = self.hack + if not (self.input == 0 and len(_) == 0): + raise kaitaistruct.ValidationExprError(self.hack, self._io, u'/types/powerstep_command/types/zero_int/seq/0') + + class Microsteps(KaitaiStruct): + + def __init__(self, raw, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.raw = raw + + def _read(self): + pass + + @property + def weird(self): + if hasattr(self, '_m_weird'): + return self._m_weird if hasattr(self, '_m_weird') else None + self._m_weird = 1 << 21 + return self._m_weird if hasattr(self, '_m_weird') else None + + @property + def modulus_mask(self): + if hasattr(self, '_m_modulus_mask'): + return self._m_modulus_mask if hasattr(self, '_m_modulus_mask') else None + self._m_modulus_mask = self.weird - 1 + return self._m_modulus_mask if hasattr(self, '_m_modulus_mask') else None + + @property + def microsteps(self): + """The motion commands are always set as microstepping measured displacements.""" + if hasattr(self, '_m_microsteps'): + return self._m_microsteps if hasattr(self, '_m_microsteps') else None + self._m_microsteps = (self.raw & self.modulus_mask) - self.weird if self.raw >= self.weird else self.raw + return self._m_microsteps if hasattr(self, '_m_microsteps') else None + + class AccelerationVerifyRange(KaitaiStruct): + + def __init__(self, acceleration, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.acceleration = acceleration + + def _read(self): + self.hack = self._io.read_bytes(0) + _ = self.hack + if not (15 <= self.acceleration and self.acceleration <= 59000 and (len(_) == 0)): + raise kaitaistruct.ValidationExprError(self.hack, self._io, u'/types/powerstep_command/types/acceleration_verify_range/seq/0') + + class SignalVerifyRange(KaitaiStruct): + + def __init__(self, signal, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.signal = signal + + def _read(self): + self.hack = self._io.read_bytes(0) + _ = self.hack + if not (self.signal <= 7 and len(_) == 0): + raise kaitaistruct.ValidationExprError(self.hack, self._io, u'/types/powerstep_command/types/signal_verify_range/seq/0') + + class TimeVerifyRange(KaitaiStruct): + + def __init__(self, time, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.time = time + + def _read(self): + self.hack = self._io.read_bytes(0) + _ = self.hack + if not (self.time <= 3600000 and len(_) == 0): + raise kaitaistruct.ValidationExprError(self.hack, self._io, u'/types/powerstep_command/types/time_verify_range/seq/0') + + @property + def argument_raw(self): + """Zero if not needed.""" + if hasattr(self, '_m_argument_raw'): + return self._m_argument_raw if hasattr(self, '_m_argument_raw') else None + self._m_argument_raw = self.raw >> 10 + return self._m_argument_raw if hasattr(self, '_m_argument_raw') else None + + @property + def reserved(self): + if hasattr(self, '_m_reserved'): + return self._m_reserved if hasattr(self, '_m_reserved') else None + self._m_reserved = self.raw & 7 + return self._m_reserved if hasattr(self, '_m_reserved') else None + + @property + def action(self): + """for internal use, send as 0.""" + if hasattr(self, '_m_action'): + return self._m_action if hasattr(self, '_m_action') else None + self._m_action = self.raw >> 3 & 1 == 1 + return self._m_action if hasattr(self, '_m_action') else None + + @property + def argument(self): + if hasattr(self, '_m_argument'): + return self._m_argument if hasattr(self, '_m_argument') else None + _pos = self._io.pos() + self._io.seek(0) + _on = self.operation + if _on == SmsdLan.PowerstepCommand.Opcode.program_start_mem2: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_steps_forward: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.Microsteps(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.program_start_mem0: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.stop_hard_hold: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.stop_hard_deenergize: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_full_step_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.argument_raw, 15, 15600, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_min_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, 0, self._root.limits.speed_min_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.events_status_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.relay_off: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_in1_reverse_set_mark: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.jump: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.InstructionPointer(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.end: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_forward: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.argument_raw, 15, 15600, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_reverse: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.argument_raw, 15, 15600, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.usb_stop: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_to_position_reverse: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.Microsteps(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_in1_forward_set_mark: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.ret: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.reset_powerstep01: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.loop: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.Loop(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.motor_mode_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.MotorMode(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.stop_soft_hold: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.relay_on: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_in1_reverse_set_label: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.wait_for_in0: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.zero_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_max_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.wait_for_in1: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_sw_reverse: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.SignalVerifyRange(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.relay_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.jump_if_zero: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.InstructionPointer(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.instruction_pointer_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.event_mask_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.EventMaskFromInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_zero_reverse_set_zero: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.acceleration_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.AccelerationVerifyRange(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.call: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.InstructionPointer(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_to_position_forward: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.Microsteps(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_zero_forward_set_zero: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.position_absolute_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_to_recorded_label: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_to_position: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.Microsteps(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.sleep: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.TimeVerifyRange(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.wait_for_continue: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_steps_reverse: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.Microsteps(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_sw_forward: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.SignalVerifyRange(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.program_start_mem3: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.decelleration_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.AccelerationVerifyRange(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.stop_soft_deenergize: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_untill_in1_forward_set_label: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit - 1, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.sleep_interruptible: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.TimeVerifyRange(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.halt: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.jump_if_in0: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.InstructionPointer(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_current_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.program_start_mem1: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.control_mode_set_en_step_dir: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.jump_if_at_zero: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.InstructionPointer(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.position_microstepping_electrical_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.jump_if_in1: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.InstructionPointer(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.status_and_clear_errors_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.motor_mode_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_min_get: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.move_to_recorded_zero: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.PowerstepCommand.ZeroInt(self.argument_raw, _io__raw__m_argument, self, self._root) + self._m_argument._read() + elif _on == SmsdLan.PowerstepCommand.Opcode.speed_max_set: + self._raw__m_argument = self._io.read_bytes(0) + _io__raw__m_argument = KaitaiStream(BytesIO(self._raw__m_argument)) + self._m_argument = SmsdLan.SpeedVerifyRange(self.raw, self._root.limits.speed_max_min_limit, self._root.limits.speed_max_max_limit, _io__raw__m_argument, self, self._root) + self._m_argument._read() + else: + self._m_argument = self._io.read_bytes(0) + self._io.seek(_pos) + return self._m_argument if hasattr(self, '_m_argument') else None + + @property + def operation(self): + """the executing command code.""" + if hasattr(self, '_m_operation'): + return self._m_operation if hasattr(self, '_m_operation') else None + self._m_operation = KaitaiStream.resolve_enum(SmsdLan.PowerstepCommand.Opcode, self.raw >> 4 & 63) + return self._m_operation if hasattr(self, '_m_operation') else None + + class NetworkConfig(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.mac = SmsdLan.NetworkConfig.MacAddr(self._io, self, self._root) + self.mac._read() + self.my_ip = SmsdLan.NetworkConfig.Ipv4(self._io, self, self._root) + self.my_ip._read() + self.subnet_mask = SmsdLan.NetworkConfig.Ipv4(self._io, self, self._root) + self.subnet_mask._read() + self.gateway = SmsdLan.NetworkConfig.Ipv4(self._io, self, self._root) + self.gateway._read() + self.dns = SmsdLan.NetworkConfig.Ipv4(self._io, self, self._root) + self.dns._read() + self.port = self._io.read_u2le() + self.dhcp_mode = self._io.read_u1() + + class MacAddr(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.mac = self._io.read_bytes(6) + + class Ipv4(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + self.ipv4 = self._io.read_bytes(4) + + @property + def checksummed_bytes(self): + if hasattr(self, '_m_checksummed_bytes'): + return self._m_checksummed_bytes if hasattr(self, '_m_checksummed_bytes') else None + _pos = self._io.pos() + self._io.seek(1) + self._m_checksummed_bytes = self._io.read_bytes(6 - 1 + self.header.len) + self._io.seek(_pos) + return self._m_checksummed_bytes if hasattr(self, '_m_checksummed_bytes') else None + + @property + def recomputed_checksum(self): + if hasattr(self, '_m_recomputed_checksum'): + return self._m_recomputed_checksum if hasattr(self, '_m_recomputed_checksum') else None + _pos = self._io.pos() + self._io.seek(0) + self._raw__m_recomputed_checksum = self._io.read_bytes(0) + _io__raw__m_recomputed_checksum = KaitaiStream(BytesIO(self._raw__m_recomputed_checksum)) + self._m_recomputed_checksum = SmsdLan.SmsdChecksum(self.checksummed_bytes, _io__raw__m_recomputed_checksum, self, self._root) + self._m_recomputed_checksum._read() + self._io.seek(_pos) + return self._m_recomputed_checksum if hasattr(self, '_m_recomputed_checksum') else None \ No newline at end of file diff --git a/SMSD/kaitai/smsd_limits.py b/SMSD/kaitai/smsd_limits.py new file mode 100644 index 0000000..b1ab1a2 --- /dev/null +++ b/SMSD/kaitai/smsd_limits.py @@ -0,0 +1,43 @@ +from pkg_resources import parse_version +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +if parse_version(kaitaistruct.__version__) < parse_version('0.9'): + raise Exception('Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s' % kaitaistruct.__version__) + +class SmsdLimits(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + + def _read(self): + pass + + @property + def speed_max_max_limit(self): + if hasattr(self, '_m_speed_max_max_limit'): + return self._m_speed_max_max_limit if hasattr(self, '_m_speed_max_max_limit') else None + self._m_speed_max_max_limit = 15600 + return self._m_speed_max_max_limit if hasattr(self, '_m_speed_max_max_limit') else None + + @property + def speed_max_min_limit(self): + if hasattr(self, '_m_speed_max_min_limit'): + return self._m_speed_max_min_limit if hasattr(self, '_m_speed_max_min_limit') else None + self._m_speed_max_min_limit = 16 + return self._m_speed_max_min_limit if hasattr(self, '_m_speed_max_min_limit') else None + + @property + def speed_min_max_limit(self): + if hasattr(self, '_m_speed_min_max_limit'): + return self._m_speed_min_max_limit if hasattr(self, '_m_speed_min_max_limit') else None + self._m_speed_min_max_limit = 950 + return self._m_speed_min_max_limit if hasattr(self, '_m_speed_min_max_limit') else None + + @property + def speed_movement_min_limit(self): + if hasattr(self, '_m_speed_movement_min_limit'): + return self._m_speed_movement_min_limit if hasattr(self, '_m_speed_movement_min_limit') else None + self._m_speed_movement_min_limit = self.speed_max_min_limit - 1 + return self._m_speed_movement_min_limit if hasattr(self, '_m_speed_movement_min_limit') else None \ No newline at end of file diff --git a/SMSD/kaitai/smsd_modbus.py b/SMSD/kaitai/smsd_modbus.py new file mode 100644 index 0000000..7570222 --- /dev/null +++ b/SMSD/kaitai/smsd_modbus.py @@ -0,0 +1,251 @@ +from pkg_resources import parse_version +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from enum import IntEnum +if parse_version(kaitaistruct.__version__) < parse_version('0.9'): + raise Exception('Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s' % kaitaistruct.__version__) + +class SmsdModbus(KaitaiStruct): + """ + .. seealso:: + Source - https://smd.ee/manuals/BLSD-20Modbus_PS.pdf + + + .. seealso:: + Source - https://electroprivod.ru/pdf/drivers/BLSD-20Modbus_PS_2020_10_08.pdf + """ + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + pass + + class Instruction(KaitaiStruct): + + class RegisterType(IntEnum): + discrete_inputs = 0 + coils = 1 + inputs = 2 + holding_registers = 3 + + class Command(IntEnum): + stop_program = 0 + register_system_set = 1 + register_modbus_write = 2 + read_reg_modbus = 3 + delay = 4 + jmp = 5 + jeq = 6 + jneq = 7 + jgt = 8 + jlt = 9 + call = 10 + ret = 11 + loop = 12 + full_stop_program = 13 + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.raw0 = self._io.read_u1() + self.raw1 = self._io.read_u1() + _on = self.command + if _on == SmsdModbus.Instruction.Command.register_system_set: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.RegisterSystem(_io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.jeq: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Jump(self.is_relative, _io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.delay: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Delay(_io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.register_modbus_write: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.RegisterModbus(_io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.jmp: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Jump(self.is_relative, _io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.jneq: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Jump(self.is_relative, _io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.jgt: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Jump(self.is_relative, _io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.jlt: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Jump(self.is_relative, _io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.loop: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Loop(_io__raw_data, self, self._root) + elif _on == SmsdModbus.Instruction.Command.call: + self._raw_data = self._io.read_bytes(2) + _io__raw_data = KaitaiStream(BytesIO(self._raw_data)) + self.data = SmsdModbus.Instruction.Jump(False, _io__raw_data, self, self._root) + else: + self.data = self._io.read_bytes(2) + + class Delay(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.delay = self._io.read_u2le() + + class Loop(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.repeats = self._io.read_u1() + self.commands = self._io.read_u1() + + class Jump(KaitaiStruct): + + def __init__(self, is_relative, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self.is_relative = is_relative + self._read() + + def _read(self): + _on = self.is_relative + if _on == True: + self.addr = SmsdModbus.Instruction.Jump.Relative(self._io, self, self._root) + elif _on == False: + self.addr = SmsdModbus.Instruction.Jump.Absolute(self._io, self, self._root) + + class Relative(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.addr = self._io.read_s2le() + if not self.addr >= -1024: + raise kaitaistruct.ValidationLessThanError(-1024, self.addr, self._io, u'/types/instruction/types/jump/types/relative/seq/0') + if not self.addr <= 1024: + raise kaitaistruct.ValidationGreaterThanError(1024, self.addr, self._io, u'/types/instruction/types/jump/types/relative/seq/0') + + class Absolute(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.addr = self._io.read_u2le() + if not self.addr >= 0: + raise kaitaistruct.ValidationLessThanError(0, self.addr, self._io, u'/types/instruction/types/jump/types/absolute/seq/0') + if not self.addr <= 1024: + raise kaitaistruct.ValidationGreaterThanError(1024, self.addr, self._io, u'/types/instruction/types/jump/types/absolute/seq/0') + + class RegisterModbus(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.reg = self._io.read_u2le() + + class RegisterSystem(KaitaiStruct): + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.value = self._io.read_u2le() + + @property + def command(self): + if hasattr(self, '_m_command'): + return self._m_command if hasattr(self, '_m_command') else None + self._m_command = KaitaiStream.resolve_enum(SmsdModbus.Instruction.Command, self.raw0 & 127) + return self._m_command if hasattr(self, '_m_command') else None + + @property + def is_relative(self): + """displacement type for movement commands.""" + if hasattr(self, '_m_is_relative'): + return self._m_is_relative if hasattr(self, '_m_is_relative') else None + self._m_is_relative = self.raw0 >> 7 & 1 == 1 + return self._m_is_relative if hasattr(self, '_m_is_relative') else None + + @property + def register_address(self): + """address of the system register AX_REG ... FX_REG with numbers 0 ... 5.""" + if hasattr(self, '_m_register_address'): + return self._m_register_address if hasattr(self, '_m_register_address') else None + self._m_register_address = self.raw1 & 15 + return self._m_register_address if hasattr(self, '_m_register_address') else None + + @property + def register_type(self): + """type of a Modbus register.""" + if hasattr(self, '_m_register_type'): + return self._m_register_type if hasattr(self, '_m_register_type') else None + self._m_register_type = KaitaiStream.resolve_enum(SmsdModbus.Instruction.RegisterType, self.raw1 >> 4 & 15) + return self._m_register_type if hasattr(self, '_m_register_type') else None + + class Error(KaitaiStruct): + """5023h register of modbus. + errors that occur during controller operation + """ + + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.voltage_out_of_range = self._io.read_bits_int_le(1) != 0 + self.short_circuit_winding = self._io.read_bits_int_le(1) != 0 + self.overheat_brake = self._io.read_bits_int_le(1) != 0 + self.overheat_power = self._io.read_bits_int_le(1) != 0 + self.not_connected_hall = self._io.read_bits_int_le(1) != 0 + self.emergency_stop = self._io.read_bits_int_le(1) != 0 + self.overheat_mcu = self._io.read_bits_int_le(1) != 0 + self.test_control_program = self._io.read_bits_int_le(1) != 0 + self.runtime = self._io.read_bits_int_le(1) != 0 + self.io_settings = self._io.read_bits_int_le(1) != 0 + self.output_analog = self._io.read_bits_int_le(1) != 0 + self.warning_breakpoint = self._io.read_bits_int_le(1) != 0 + self.register_out_of_range = self._io.read_bits_int_le(1) != 0 + self.parity = self._io.read_bits_int_le(1) != 0 \ No newline at end of file diff --git a/SMSD/lan/__init__.py b/SMSD/lan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/SMSD/lan/powerStepSim/Clock.py b/SMSD/lan/powerStepSim/Clock.py new file mode 100644 index 0000000..8a3c61a --- /dev/null +++ b/SMSD/lan/powerStepSim/Clock.py @@ -0,0 +1,39 @@ +import threading +import typing +from time import sleep + + +class Clockable: + __slots__ = () + + def tick(self) -> None: + """Called on every tick of the clock""" + + raise NotImplementedError + + +class Clock(Clockable): + __slots__ = ("period", "time", "tickingThread", "clockables") + + def __init__(self, clockables: typing.Collection[Clockable]): + self.clockables = clockables + self.period = 1 # type: float + self.time = 0 # type: int + self.tickingThread = None # type: threading.Thread + + def startTicking(self): + if self.tickingThread is None: + self.tickingThread = threading.Thread(target=self._tickingThreadFunc) + self.tickingThread.start() + else: + raise RuntimeError("Ticking timer is already started", self.tickingThread) + + def tick(self) -> None: + self.time += 1 + for clockable in self.clockables: + clockable.tick() + + def _tickingThreadFunc(self): + while True: + self.tick() + sleep(self.period) diff --git a/SMSD/lan/powerStepSim/PowerStepSimulator.py b/SMSD/lan/powerStepSim/PowerStepSimulator.py new file mode 100644 index 0000000..cda708d --- /dev/null +++ b/SMSD/lan/powerStepSim/PowerStepSimulator.py @@ -0,0 +1,35 @@ +from .Clock import Clock, Clockable +from .gpio import GPIO +from .instructionProcessors import ProgramExecutor +from .motor import MotorController + + +class PowerStepSimulator(Clockable): + __slots__ = ("mode", "zeroPosition", "motorController", "gpio", "executor", "instructionProcessor", "clock") + + def __init__(self): + self.mode = None + self.zeroPosition = 0 # type: int + self.motorController = MotorController() # type: MotorController + self.gpio = GPIO() # type: GPIO + self.executor = ProgramExecutor() # type: InstructionProcessor + self.instructionProcessor = self.executor + self.clock = Clock((self.gpio, self.instructionProcessor, self.motorController)) # type: Clock + + def startTicking(self): + self.clock.startTicking() + + def startLoading(self): + self.instructionProcessor = self.executor.curProgram.startLoading() + + def commitLoading(self): + if isinstance(self.instructionProcessor, ProgramLoader): + self.instructionProcessor.commit() + + self.instructionProcessor = self.executor + + def emitMessage(self, typ, payload): + pass + + def tick(self): + self.clock.tick() diff --git a/SMSD/lan/powerStepSim/__init__.py b/SMSD/lan/powerStepSim/__init__.py new file mode 100644 index 0000000..77a4e67 --- /dev/null +++ b/SMSD/lan/powerStepSim/__init__.py @@ -0,0 +1,43 @@ +from ...kaitai.smsd_lan import SmsdLan +from ..serializers import * +from .instructionProcessors import * +from .instructions import * +from .motor import * +from .PowerStepSimulator import * + + +class PowerStepResponder: + __slots__ = ("ps",) + + def __init__(self): + self.ps = PowerStepSimulator() + + def __call__(self, cmd): + processor = getattr(Instructions, cmd.operation.name, None) + if processor: + arg = cmd.argument + if isinstance(arg, SmsdLan.PowerstepCommand.ZeroInt): + result = processor(self.ps, cmd.action) + else: + # print("arg", arg) + result = processor(self.ps, arg, cmd.action) + if isinstance(result, tuple): + if len(result) == 2: + respCode, respValue = result + else: + raise ValueError("Processor func should either return a tuple (code, result) or None") + else: + respCode = SmsdLan.Response.Code.success + respValue = 0 + + return SmsdLan.Type.response, self.serializeResponse(respCode, respValue) + else: + print(self.__class__.__name__ + ": No processor for ", cmd.operation.name) + + def serializeResponse(self, code, return_data: int): + ps = self.ps + m = ps.motorController + return serializeResponse(is_deenergized=m.driver.isDeenergized, is_ready=ps.instructionProcessor.isReady, is_sw_on=False, has_sw_event_happenned=False, is_rotating_direction_forward=m.isDirectionForward, accelerationStatus=m.accelerationStatus, is_command_error=False, code=code, return_data=return_data) + + def startTicking(self): + self.ps.startTicking() diff --git a/SMSD/lan/powerStepSim/gpio.py b/SMSD/lan/powerStepSim/gpio.py new file mode 100644 index 0000000..7c3a43b --- /dev/null +++ b/SMSD/lan/powerStepSim/gpio.py @@ -0,0 +1,125 @@ +from ...kaitai.smsd_lan import SmsdLan +from ..serializers import serializeEventStatus +from .Clock import Clockable + + +class GPIOStepState: + __slots__ = ("inputs", "events") + + def __init__(self, parent: "GPIO"): + self.inputs = [False] * parent.inputsCount + self.events = [False] * parent.inputsCount + + +class GPIO(Clockable): + __slots__ = ("inputsCount", "inputState", "externallySetInputs", "prevInputState", "masks", "events", "outputs", "relay", "cont") + + def __init__(self, inputsCount: int = 8, outs: int = 2): + self.inputsCount = inputsCount # type: int + self.inputState = self.spawnState() # type: GPIOStepState + self.prevInputState = self.inputState # type: GPIOStepState + + self.externallySetInputs = [None] * inputsCount + self.masks = [True] * inputsCount + + self.cont = False # type: bool + + self.outputs = [False] * outs + self.relay = False # type: bool + + def spawnState(self) -> GPIOStepState: + return GPIOStepState(self) + + def tick(self) -> None: + prevInputState = self.inputState + + newInputState = self.spawnState() + self.mutateInputState(newInputState) + + for i, (prev, cur) in enumerate(zip(prevInputState.inputs, newInputState.inputs)): + if self.masks[i] and not prev and cur: + newInputState.events[i] = True + + self.prevInputState = prevInputState + self.inputState = newInputState + + def mutateInputState(self, newInputState): + """Currently just copies the prev state if not externally set""" + + for i, el in enumerate(self.prevInputState.inputs): + ext = self.externallySetInputs[i] + if ext is not None: + newInputState.inputs[i] = ext + self.externallySetInputs[i] = None + else: + newInputState.inputs[i] = el + + +class GPIOInstructions: + def relay_get(ps: "PowerStepSimulator", action: bool = False): + """read a current state of the controller relay""" + # print("relay_get", action) + + return SmsdLan.Response.Code.relay_set if ps.gpio.relay else SmsdLan.Response.Code.relay_clear, 0 + + CMD_PowerSTEP01_GET_RELE = relay_get + + def relay_on(ps: "PowerStepSimulator", action: bool = False): + """turn on the controller relay""" + print("relay_on", action) + ps.gpio.relay = True + return SmsdLan.Response.Code.relay_set, 0 + + CMD_PowerSTEP01_SET_RELE = relay_on + + def relay_off(ps: "PowerStepSimulator", action: bool = False): + """turn off the controller relay""" + print("relay_off", action) + ps.gpio.relay = False + return SmsdLan.Response.Code.relay_clear, 0 + + CMD_PowerSTEP01_CLR_RELE = relay_off + + ######## + + def events_status_get(ps: "PowerStepSimulator", action: bool = False): + """reads information about current signals inputs state: + * whether events happenned, + * if they are masked + * if we are waiting for them""" + + # print("events_status_get", action) # -> events_status + g = ps.gpio + return SmsdLan.Response.Code.events_status, serializeEventStatus(g.inputState.inputs, g.masks, g.inputState.events) + + CMD_PowerSTEP01_STATUS_IN_EVENT = events_status_get + + def event_mask_set(ps: "PowerStepSimulator", argument: "EventMaskFromInt", action: bool = False): + """masks input signals. If the input signal MASK value = 1 – the Controller handles the signal state at the physical input. If the signal MASK is 0 – the controller doesn’t take a care the physical input state.""" + bits = [el.value for el in argument.is_enabled] + print("event_mask_set", action, bits) + g = ps.gpio + for i, bit in enumerate(bits): + g.masks[i] = bit + + CMD_PowerSTEP01_SET_MASK_EVENT = event_mask_set + + ################# + + def wait_for_in0(ps: "PowerStepSimulator", action: bool = False): + """wait until receiving a signal to the input IN0""" + # print("wait_for_in0", action) + + CMD_PowerSTEP01_WAIT_IN0 = wait_for_in0 + + def wait_for_in1(ps: "PowerStepSimulator", action: bool = False): + """wait until receiving a signal to the input IN1""" + # print("wait_for_in1", action) + + CMD_PowerSTEP01_WAIT_IN1 = wait_for_in1 + + def wait_for_continue(ps: "PowerStepSimulator", action: bool = False): + """waits for synchronization signal at the input CONTINUE, which is used for synchronization of executing programs in different controllers. This command is valid for 2d version of communication protocol only.""" + # print("wait_for_continue", action) + + CMD_PowerSTEP01_WAIT_CONTINUE = wait_for_continue diff --git a/SMSD/lan/powerStepSim/instructionProcessors.py b/SMSD/lan/powerStepSim/instructionProcessors.py new file mode 100644 index 0000000..8f9b45a --- /dev/null +++ b/SMSD/lan/powerStepSim/instructionProcessors.py @@ -0,0 +1,154 @@ +from ...kaitai.smsd_lan import SmsdLan +from ..serializers import serializeInstructionPointer +from .Clock import Clockable + + +class InstructionProcessor(Clockable): + __slots__ = () + + def __call__(self, instruction): + raise NotImplementedError + + @property + def isReady(self) -> bool: + return True + + +class ProgramLoader(InstructionProcessor): + __slots__ = ("parent", "instructions") + + def __init__(self, parent): + self.parent = parent + self.instructions = [] + + def commit(self): + self.parent.instructions = self.instructions + self.instructions = None + + def __call__(self, instruction): + self.instructions.append(instruction) + + def tick(self) -> None: + pass + + +class Program: + __slots__ = ( + "instructions", + "_instructionPointer", + ) + + def __init__(self): + self.instructions = [] + self._instructionPointer = 0 + + def executeNextInstr(self): + self._instructionPointer += 1 + instr = self.instructions + + @property + def instructionPointer(self): + return self._instructionPointer + + @instructionPointer.setter + def instructionPointer(self, v): + self._instructionPointer = v + + def startLoading(self): + return ProgramLoader(self) + + +class ProgramExecutor(InstructionProcessor): + __slots__ = ("programIdx", "programs") + + def __init__(self, countOfPrograms: int = 4): + self.programIdx = 0 + self.programs = [Program() for i in range(countOfPrograms)] + + @property + def curProgram(self): + return self.programs[self.programIdx] + + @property + def curProgramInstrPointer(self): + return self.curProgram.instructionPointer + + @property + def fullInstructionPointer(self): + return self.programIdx, self.curProgram.instructionPointer + + def __call__(self, instruction): + instruction() + + def tick(self) -> None: + pass + + +class InstructionProcessorInstructions: + def ret(ps: "PowerStepSimulator", action: bool = False): + """specifies the end of a subprogram and to return back to the main program. If previously the command CMD_PowerSTEP01_CALL_PROGRAM was not called, the executing of CMD_PowerSTEP01_RETURN_PROGRAM will call an error.""" + print("ret", action) + + CMD_PowerSTEP01_RETURN_PROGRAM = ret + + def program_start_mem0(ps: "PowerStepSimulator", action: bool = False): + """&program_start_mem_doc starts executing a program from the Controller memory area.""" + + CMD_PowerSTEP01_START_PROGRAM_MEM0 = program_start_mem0 + + def program_start_mem1(ps: "PowerStepSimulator", action: bool = False): + """*program_start_mem_doc""" + print("program_start_mem1", action) + + CMD_PowerSTEP01_START_PROGRAM_MEM1 = program_start_mem1 + + def program_start_mem2(ps: "PowerStepSimulator", action: bool = False): + """*program_start_mem_doc""" + print("program_start_mem2", action) + + CMD_PowerSTEP01_START_PROGRAM_MEM2 = program_start_mem2 + + def program_start_mem3(ps: "PowerStepSimulator", action: bool = False): + """*program_start_mem_doc""" + print("program_start_mem3", action) + + CMD_PowerSTEP01_START_PROGRAM_MEM3 = program_start_mem3 + + def halt(ps: "PowerStepSimulator", action: bool = False): + """stops executing a program""" + print("halt", action) + + CMD_PowerSTEP01_STOP_PROGRAM_MEM = halt + + def end(ps: "PowerStepSimulator", action: bool = False): + """marks the end of executing program""" + print("end", action) + + CMD_PowerSTEP01_END = end + + def jump(ps: "PowerStepSimulator", argument: "InstructionPointer", action: bool = False): + """unconditional branch – jumps to a specified command number in a specified program number. The DATA field contains the information about a program memory number and a command sequence number:""" + + print("jump", action) + + CMD_PowerSTEP01_GOTO_PROGRAM = jump + + def call(ps: "PowerStepSimulator", argument: "InstructionPointer", action: bool = False): + """calls a subprogram. The DATA field contains the information about a program memory number and a command sequence number, which starts a subprogram: For returning back to the main program, the subprogram should contain a RETURN command - CMD_PowerSTEP01_RETURN_PROGRAM. The subprogram is executed until the CMD_PowerSTEP01_RETURN_PROGRAM and after that returns to the next command of the main program after CMD_PowerSTEP01_CALL_PROGRAM.""" + + print("call", action) + + CMD_PowerSTEP01_CALL_PROGRAM = call + + def instruction_pointer_get(ps: "PowerStepSimulator", action: bool = False): + """reads information about current executing command number and program number from the controller.""" + # print("instruction_pointer_get", action) # -> jump_target(argument_raw) + return SmsdLan.Response.Code.instruction_pointer, serializeInstructionPointer(*ps.executor.fullInstructionPointer) + + CMD_PowerSTEP01_GET_STACK = instruction_pointer_get + + def loop(ps: "PowerStepSimulator", argument: "Loop", action: bool = False): + """loop – the Controller repeats specified times specified number of commands (start from the first command after this command. The DATA field contains the information about commands number and cycles number: bits 0..9 of the DATA field contain the commands number, bits 10..19 of the DATA field contain the cycles number.""" + print("loop", action) + + CMD_PowerSTEP01_LOOP_PROGRAM = loop diff --git a/SMSD/lan/powerStepSim/instructions.py b/SMSD/lan/powerStepSim/instructions.py new file mode 100644 index 0000000..040a72d --- /dev/null +++ b/SMSD/lan/powerStepSim/instructions.py @@ -0,0 +1,224 @@ +from ...kaitai.smsd_lan import SmsdLan +from .gpio import GPIOInstructions +from .instructionProcessors import InstructionProcessorInstructions +from .motor import MotorInstructions + + +class Instructions(MotorInstructions, GPIOInstructions, InstructionProcessorInstructions): + def zero_set(ps: "PowerStepSimulator", action: bool = False): + """sets ZERO position (to clear internal steps counter and specify a current position as a ZERO position)""" + print("zero_set", action) + + CMD_PowerSTEP01_RESET_POS = zero_set + + def reset_powerstep01(ps: "PowerStepSimulator", action: bool = False): + """hardware and software reset of the PowerSTEP01 stepper motor control module, but not of the whole Controller.""" + print("reset_powerstep01", action) + ps.__init__() + + CMD_PowerSTEP01_RESET_POWERSTEP01 = reset_powerstep01 + + def control_mode_set_en_step_dir(ps: "PowerStepSimulator", action: bool = False): + """changes the control mode to pulse control using external input signals EN, STEP, DIR.""" + print("control_mode_set_en_step_dir", action) + + CMD_PowerSTEP01_STEP_CLOCK = control_mode_set_en_step_dir + + def usb_stop(ps: "PowerStepSimulator", action: bool = False): + """stops data transfer via USB interface""" + print("usb_stop", action) + + CMD_PowerSTEP01_STOP_USB = usb_stop + + def status_and_clear_errors_get(ps: "PowerStepSimulator", action: bool = False): + """reads the current state of the controller, and the Controller clears all error flags.""" + print("status_and_clear_errors_get", action) + + CMD_PowerSTEP01_GET_STATUS_AND_CLR = status_and_clear_errors_get + + ########### + + def speed_forward(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """starts motor rotation in forward direction at designated speed. The DATA field should contain the final rotation speed value.""" + print("speed_forward", argument.speed, action) + ps.motorController + + CMD_PowerSTEP01_RUN_F = speed_forward + + def speed_reverse(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """starts motor rotation in backward direction at designated speed. The DATA field should contain the final rotation speed value.""" + print("speed_reverse", argument.speed, action) + ps.motorController + + CMD_PowerSTEP01_RUN_R = speed_reverse + + def stop_soft_hold(ps: "PowerStepSimulator", action: bool = False): + """smooth decelerating of the stepper motor and stop. After that the motor holds the current position (with preset holding current).""" + print("stop_soft_hold", action) + + CMD_PowerSTEP01_SOFT_STOP = stop_soft_hold + + def stop_hard_hold(ps: "PowerStepSimulator", action: bool = False): + """sudden stop of the stepper motor and holding the current position (with preset holding current).""" + print("stop_hard_hold", action) + + CMD_PowerSTEP01_HARD_STOP = stop_hard_hold + + def stop_soft_deenergize(ps: "PowerStepSimulator", action: bool = False): + """smooth decelerating of the stepper motor and stop. After that the motor phases are deenergized""" + print("stop_soft_deenergize", action) + + CMD_PowerSTEP01_SOFT_HI_Z = stop_soft_deenergize + + def stop_hard_deenergize(ps: "PowerStepSimulator", action: bool = False): + """sudden stop of the stepper motor and deenergizing the stepper motor""" + print("stop_hard_deenergize", action) + + CMD_PowerSTEP01_HARD_HI_Z = stop_hard_deenergize + + ############ + + def sleep(ps: "PowerStepSimulator", argument: "TimeVerifyRange", action: bool = False): + """sets pause. The DATA field contains the waiting time measured as ms. Allowed value range 0 – 3600000 ms""" + print("sleep", action) + + CMD_PowerSTEP01_SET_WAIT = sleep + + def sleep_interruptible(ps: "PowerStepSimulator", argument: "TimeVerifyRange", action: bool = False): + """sets a pause. The DATA field contains the waiting time measured as ms. Allowed value range 0 – 3600000 ms. Unlike with the similar command CMD_PowerSTEP01_SET_WAIT, executing of this command can be interrupted by input signals IN0, IN1 or SET_ZERO. This command is valid for 2d version of communication protocol only.""" + print("sleep_interruptible", action) + + CMD_PowerSTEP01_SET_WAIT_2 = sleep_interruptible + + def jump_if_in0(ps: "PowerStepSimulator", argument: "InstructionPointer", action: bool = False): + """conditional branch – jumps to a specified command number in a specified program number if there is a signal at the input IN0. The DATA field contains the information about a program memory number and a command sequence number:""" + + CMD_PowerSTEP01_GOTO_PROGRAM_IF_IN0 = jump_if_in0 + + def jump_if_in1(ps: "PowerStepSimulator", argument: "InstructionPointer", action: bool = False): + """conditional branch – jumps to a specified command number in a specified program number if there is a signal at the input IN1. The DATA field contains the information about a program memory number and a command sequence number:""" + + print("jump_if_in1", action) + + CMD_PowerSTEP01_GOTO_PROGRAM_IF_IN1 = jump_if_in1 + + def jump_if_at_zero(ps: "PowerStepSimulator", argument: "InstructionPointer", action: bool = False): + """conditional branch – jumps to a specified command number in a specified program number if the current position value is 0. The DATA field contains the information about a program memory number and a command sequence number:""" + + print("jump_if_at_zero", action) + + CMD_PowerSTEP01_GOTO_PROGRAM_IF_ZERO = jump_if_at_zero + + def jump_if_zero(ps: "PowerStepSimulator", argument: "InstructionPointer", action: bool = False): + """conditional branch – jumps to a specified command number in a specified program number if there is a signal at the input SET_ZERO. The DATA field contains the information about a program memory number and a command sequence number: This command is valid for 2d version of communication protocol only.""" + + print("jump_if_zero", action) + + ################################################ + + # absolute positions + def move_to_position_forward(ps: "PowerStepSimulator", argument: "Microsteps", action: bool = False): + """for motor displacement to the specified position in forward direction. The DATA field should contain the position value. The motion speed is determined by specified minimum and maximum speed and acceleration value.""" + + print("move_to_position_forward", action) + + CMD_PowerSTEP01_GO_TO_F = move_to_position_forward + + def move_to_position_reverse(ps: "PowerStepSimulator", argument: "Microsteps", action: bool = False): + """for motor displacement to the specified position in backward direction. The DATA field should contain the position value. The motion speed is determined by specified minimum and maximum speed and acceleration value.""" + + print("move_to_position_reverse", action) + + CMD_PowerSTEP01_GO_TO_R = move_to_position_reverse + + def move_to_position(ps: "PowerStepSimulator", argument: "Microsteps", action: bool = False): + """shortest movement to the specified position""" + print("move_to_position", action) + + CMD_PowerSTEP01_GO_TO = move_to_position + + def move_to_recorded_zero(ps: "PowerStepSimulator", action: bool = False): + """movement to the ZERO position (remembered using move_untill_zero_*_set_zero ?)""" + print("move_to_recorded_zero", action) + + CMD_PowerSTEP01_GO_ZERO = move_to_recorded_zero + + def move_to_recorded_label(ps: "PowerStepSimulator", action: bool = False): + """movement to the LABEL position (remembered using move_untill_in1_*_set_label ?)""" + print("move_to_recorded_label", action) + + CMD_PowerSTEP01_GO_LABEL = move_to_recorded_label + + # the ones for which limits are NOT documented, but we assume they are the same + def move_untill_zero_forward_set_zero(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """searches for zero position in a forward direction. The movement continues until signal to SET_ZERO input received. The DATA field determines the motion speed during searching the zero position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_zero_forward_set_zero", action) + + CMD_PowerSTEP01_SCAN_ZERO_F = move_untill_zero_forward_set_zero + + def move_untill_zero_reverse_set_zero(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """for searching zero position in a backward direction. The movement continues until signal to SET_ZERO input received. The DATA field determines the motion speed during searching the zero position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_zero_reverse_set_zero", action) + + CMD_PowerSTEP01_SCAN_ZERO_R = move_untill_zero_reverse_set_zero + + def move_untill_in1_forward_set_label(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """for searching LABEL position in a forward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_in1_forward_set_label", action) + + CMD_PowerSTEP01_SCAN_LABEL_F = move_untill_in1_forward_set_label + + def move_untill_in1_reverse_set_label(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """for searching LABEL position in a backward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_in1_reverse_set_label", action) + + CMD_PowerSTEP01_SCAN_LABEL_R = move_untill_in1_reverse_set_label + + def move_untill_in1_forward_set_mark(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """searches for LABEL position in a forward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. The motor stops according the deceleration value, current position is set as “Mark” position. Attention: the speed commands are always set as full steps per second. This command is valid for 2d version of communication protocol only.""" + + print("move_untill_in1_forward_set_mark", action) + + CMD_PowerSTEP01_SCAN_MARK2_F = move_untill_in1_forward_set_mark + + def move_untill_in1_reverse_set_mark(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """searches for LABEL position in backward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. The motor stops according the deceleration value, current position is set as “Mark” position. Attention: the speed commands are always set as full steps per second. This command is valid for 2d version of communication protocol only.""" + + print("move_untill_in1_reverse_set_mark", action) + + CMD_PowerSTEP01_SCAN_MARK2_R = move_untill_in1_reverse_set_mark + + # relative positions + def move_steps_forward(ps: "PowerStepSimulator", argument: "Microsteps", action: bool = False): + """for motor displacement in forward direction. The DATA field should contain the displacement value. The motion speed is determined by specified minimum and maximum speed and acceleration value. The motor should be stopped before executing this command (field Mot_Status of the powerSTEP_STATUS_Type structure = 0).""" + + print("move_steps_forward", action) + + CMD_PowerSTEP01_MOVE_F = move_steps_forward + + def move_steps_reverse(ps: "PowerStepSimulator", argument: "Microsteps", action: bool = False): + """for motor displacement in backward direction. The DATA field should contain the displacement value. The motion speed is determined by specified minimum and maximum speed and acceleration value. The motor should be stopped before executing this command (field Mot_Status of the powerSTEP_STATUS_Type structure = 0).""" + + print("move_steps_reverse", action) + + CMD_PowerSTEP01_MOVE_R = move_steps_reverse + + def move_untill_sw_forward(ps: "PowerStepSimulator", argument: "SignalVerifyRange", action: bool = False): + """motor forward motion at the maximum speed until receiving a signal at the input SW (taking into account the signal masking). After that the motor decelerates and stops. The MASK state of the signal can be changed by the executing command CMD_PowerSTEP01_SET_MASK_EVENT""" + print("move_untill_sw_forward", action) + + CMD_PowerSTEP01_GO_UNTIL_F = move_untill_sw_forward + + def move_untill_sw_reverse(ps: "PowerStepSimulator", argument: "SignalVerifyRange", action: bool = False): + """motor backward motion at the maximum speed until receiving a signal at the input SW (taking into account the signal masking). After that the motor decelerates and stops. The MASK state of the signal can be changed by the executing command CMD_PowerSTEP01_SET_MASK_EVENT""" + print("move_untill_sw_reverse", action) + + CMD_PowerSTEP01_GO_UNTIL_R = move_untill_sw_reverse diff --git a/SMSD/lan/powerStepSim/microcode.py b/SMSD/lan/powerStepSim/microcode.py new file mode 100644 index 0000000..4e4c988 --- /dev/null +++ b/SMSD/lan/powerStepSim/microcode.py @@ -0,0 +1,201 @@ +import typing +from dataclasses import dataclass + +from . import nanoops as nO +from .vm import * + +from motorAccelerationPlanner import ArbitraryPositionChangePlan +from motorAccelerationPlanner import * + + + +makeStepsSingleEpoch = nO.repeat(nO.makeStep(), RegID.currentSpeed) + +moveEthernally = nO.repeat_ethernally(makeStepsSingleEpoch) +hardStop = nO.set_immed(RegID.currentSpeed, 0) # and don't emit make steps commands + + +def initAcceleration(accel: int) -> nO.set_immed: + return nO.set_immed(RegID.currentAcceleration, accel) + + +doAcceleration = ( + nO.incr(RegID.currentSpeed, RegID.currentAcceleration), + makeStepsSingleEpoch, +) + + +def accelerateWithAccelerationSingleEpoch(accel: int): + return (initAcceleration(accel), *doAcceleration) + + +def speedChangePlan2Microcode(plan: SpeedChangePlan): + res = [] + if plan.accelerationEpochs > 1: + res.extend( + ( + nO.group( + ( + nO.set_immed(RegID.countOfSpeedSteps, plan.accelerationEpochs), + accelerateWithAccelerationSingleEpoch(plan.acceleration), + ) + ), + nO.repeat( + nO.group( + ( + nO.incr(RegID.currentSpeed, RegID.currentAcceleration), + makeStepsSingleEpoch, + ) + ), + RegID.countOfSpeedSteps, + ), + ) + ) + elif plan.accelerationEpochs == 1: + res.extend(accelerateWithAccelerationSingleEpoch(plan.acceleration)) + if plan.residualEpoch: + res.extend(accelerateWithAccelerationSingleEpoch(plan.residualEpoch)) + return res + + +def changeSpeedArbitrarily(currentSpeed: int, setSpeed: int): + """Changes speed from `currentSpeed` to `setSpeed` taking into account acceleration limits""" + uops = [] + + if areTheSameSign(setSpeed, currentSpeed): + uops.extend(speedChangePlan2Microcode(SpeedChangePlan.compute(currentSpeed, setSpeed, deccelLimit, accelLimit))) + else: + uops.extend(speedChangePlan2Microcode(SpeedChangePlan.compute(currentSpeed, 0, deccelLimit, accelLimit))) + uops.extend(speedChangePlan2Microcode(SpeedChangePlan.compute(0, setSpeed, deccelLimit, accelLimit))) + return uops + + +class Microcode: + def _changeSpeed(cpu: CPU, setSpeed: int): + return changeSpeedArbitrarily(cpu[RegID.currentSpeed], targetSpeed) + + def _moveEthernallyWithSpeed(cpu: CPU, setSpeed: int): + return (*_changeSpeed(cpu, setSpeed), moveEthernally) + + def speed_forward(cpu: CPU, setSpeed: int, action: bool = False): + return _moveEthernallyWithSpeed(ps, setSpeed) + + def speed_reverse(cpu: CPU, setSpeed: int, action: bool = False): + return _moveEthernallyWithSpeed(ps, setSpeed) + + def stop_soft_hold(cpu: CPU, action: bool = False): + return (_changeSpeed(ps, 0), energize()) + + def stop_hard_hold(cpu: CPU, action: bool = False): + return (hardStop, energize()) + + def stop_soft_deenergize(cpu: CPU, action: bool = False): + return (_changeSpeed(ps, 0), deenergize()) + + def stop_hard_deenergize(cpu: CPU, action: bool = False): + return (hardStop, deenergize()) + + def sleep(cpu: CPU, realTime: int, action: bool = False): + return (sleep(realTime),) + + def sleep_interruptible(cpu: CPU, realTime: int, action: bool = False): + return (sleep_interruptible(realTime),) + + def _conditionalJump(newIP: "InstructionPointer"): + return cond_true( + group( + ( + set_immed(RegID.program, newIP.program), + set_immed(RegID.ip, newIP.ip), + ) + ) + ) + + def _jumpIfInput(flagID: FlagID, newIP: "InstructionPointer"): + return (getFlag(flagID), _conditionalJump(newIP)) + + def jump_if_in0(cpu: CPU, newIP: "InstructionPointer", action: bool = False): + return cls._jumpIfInput(FlagID.IN0, newIP) + + def jump_if_in1(cpu: CPU, newIP: "InstructionPointer", action: bool = False): + return cls._jumpIfInput(FlagID.IN1, newIP) + + def jump_if_at_zero(cpu: CPU, newIP: "InstructionPointer", action: bool = False): + return (eq(RegID.zero, RegID.position), _conditionalJump(newIP)) + + def jump_if_zero(cpu: CPU, newIP: "InstructionPointer", action: bool = False): + return cls._jumpIfInput(FlagID.SET_ZERO, newIP) + + def _moveToPosition(cpu: CPU, position: int): + sched = ArbitraryPositionChangePlan.compute() + ... # ???? ToDO??? + + # absolute positions + def move_to_position_forward(cpu: CPU, position: int, action: bool = False): + _moveToPosition(position) + + def move_to_position_reverse(cpu: CPU, position: int, action: bool = False): + _moveToPosition(position) + + def move_to_position(cpu: CPU, position: int, action: bool = False): + _moveToPosition(position) + + def move_to_recorded_zero(cpu: CPU, action: bool = False): + _moveToPosition(ZERO) + + def move_to_recorded_label(cpu: CPU, action: bool = False): + _moveToPosition(LABEL) + + # the ones for which limits are NOT documented, but we assume they are the same + def move_untill_zero_forward_set_zero(cpu: CPU, setSpeed: int, action: bool = False): + """searches for zero position in a forward direction. The movement continues until signal to SET_ZERO input received. The DATA field determines the motion speed during searching the zero position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_zero_forward_set_zero", action) + + def move_untill_zero_reverse_set_zero(cpu: CPU, setSpeed: int, action: bool = False): + """for searching zero position in a backward direction. The movement continues until signal to SET_ZERO input received. The DATA field determines the motion speed during searching the zero position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_zero_reverse_set_zero", action) + + def move_untill_in1_forward_set_label(cpu: CPU, setSpeed: int, action: bool = False): + """for searching LABEL position in a forward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_in1_forward_set_label", action) + + def move_untill_in1_reverse_set_label(cpu: CPU, setSpeed: int, action: bool = False): + """for searching LABEL position in a backward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. + Attention: the speed commands are always set as full steps per second.""" + + print("move_untill_in1_reverse_set_label", action) + + def move_untill_in1_forward_set_mark(cpu: CPU, setSpeed: int, action: bool = False): + """searches for LABEL position in a forward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. The motor stops according the deceleration value, current position is set as “Mark” position. Attention: the speed commands are always set as full steps per second. This command is valid for 2d version of communication protocol only.""" + + print("move_untill_in1_forward_set_mark", action) + + def move_untill_in1_reverse_set_mark(cpu: CPU, setSpeed: int, action: bool = False): + """searches for LABEL position in backward direction. The movement continues until signal to IN1 input received. The DATA field determines the motion speed during searching the LABEL position. The motor stops according the deceleration value, current position is set as “Mark” position. Attention: the speed commands are always set as full steps per second. This command is valid for 2d version of communication protocol only.""" + + print("move_untill_in1_reverse_set_mark", action) + + # relative positions + def move_steps_forward(cpu: CPU, position: int, action: bool = False): + """for motor displacement in forward direction. The DATA field should contain the displacement value. The motion speed is determined by specified minimum and maximum speed and acceleration value. The motor should be stopped before executing this command (field Mot_Status of the powerSTEP_STATUS_Type structure = 0).""" + + print("move_steps_forward", action) + + def move_steps_reverse(cpu: CPU, position: int, action: bool = False): + """for motor displacement in backward direction. The DATA field should contain the displacement value. The motion speed is determined by specified minimum and maximum speed and acceleration value. The motor should be stopped before executing this command (field Mot_Status of the powerSTEP_STATUS_Type structure = 0).""" + + print("move_steps_reverse", action) + + def move_untill_sw_forward(cpu: CPU, argument: "SignalVerifyRange", action: bool = False): + """motor forward motion at the maximum speed until receiving a signal at the input SW (taking into account the signal masking). After that the motor decelerates and stops. The MASK state of the signal can be changed by the executing command CMD_PowerSTEP01_SET_MASK_EVENT""" + print("move_untill_sw_forward", action) + + def move_untill_sw_reverse(cpu: CPU, argument: "SignalVerifyRange", action: bool = False): + """motor backward motion at the maximum speed until receiving a signal at the input SW (taking into account the signal masking). After that the motor decelerates and stops. The MASK state of the signal can be changed by the executing command CMD_PowerSTEP01_SET_MASK_EVENT""" + print("move_untill_sw_reverse", action) diff --git a/SMSD/lan/powerStepSim/motor.py b/SMSD/lan/powerStepSim/motor.py new file mode 100644 index 0000000..a041917 --- /dev/null +++ b/SMSD/lan/powerStepSim/motor.py @@ -0,0 +1,188 @@ +from ...kaitai.smsd_lan import SmsdLan +from ..serializers import serializeMotorMode +from .Clock import Clockable + +AccelerationStatus = SmsdLan.PowerstepStatus.AccelerationStatus + + +class MotorDriver(Clockable): + __slots__ = ("windings", "currentHold", "currentWork", "model", "isInCurrentMode") + + def __init__(self, countOfWindings: int = 2): + self.windings = [False] * countOfWindings + self.currentHold = 1.0 # type: float + self.currentWork = 0.1 # type: float + self.model = 0 # type: int + self.isInCurrentMode = False # type: bool + + @property + def isDeenergized(self) -> bool: + return not any(self.windings) + + def tick(self) -> None: + pass + + +class MotorController(Clockable): + ELECTRICAL_MICROSTEPS_LOG = 7 + + def __init__(self): + self.microstepLog = 0 # type: int + + self._microsteps = 0 # type: int + self.accelerationCurrent = 0 # type: int + self.accelerationSet = 0 # type: int + self.decellerationSet = 0 # type: int + self.speedCurrent = 0 # type: int + self.speedTarget = 0 # type: int + self.speedMin = 0 # type: int + self.speedMax = 0 # type: int + self.speedFullStep = 0 # type: int + + self.driver = MotorDriver() # type: MotorDriver + + @property + def microstepsMask(self) -> int: + return (1 << self.microstepLog) - 1 + + @property + def fullSteps(self) -> int: + return self._microsteps >> self.microstepLog + + @property + def microsteps(self) -> int: + return self._microsteps & self.microstepsMask + + @property + def electricalMicrosteps(self) -> int: + return self._microsteps << (self.__class__.ELECTRICAL_MICROSTEPS_LOG - self.microstepLog) + + @property + def isDirectionForward(self) -> bool: + return self.speedCurrent >= 0 + + @property + def accelerationStatus(self) -> AccelerationStatus: + if self.accelerationCurrent == 0: + if self.speedCurrent == 0: + return AccelerationStatus.stop + else: + return AccelerationStatus.constant_speed + else: + if self.accelerationCurrent > 0: + return AccelerationStatus.accelerates + else: + return AccelerationStatus.decelerates + + def tick(self) -> None: + #self.motor.speedCurrent += self.accelerationCurrent + self.driver.tick() + + +class MotorInstructions: + def position_absolute_get(ps: "PowerStepSimulator", action: bool = False): + """reads the current motor position""" + # print("position_absolute_get", action) # -> current_electrical_step_microstep + return SmsdLan.Response.Code.position_absolute, ps.motorController.microsteps + + CMD_PowerSTEP01_GET_ABS_POS = position_absolute_get + + def position_microstepping_electrical_get(ps: "PowerStepSimulator", action: bool = False): + """reads the current motor electrical microstepping position""" + # print("position_microstepping_electrical_get", action) # -> current_electrical_step_microstep + + return SmsdLan.Response.Code.position_microstepping_electrical, ps.motorController.electricalMicrosteps + + CMD_PowerSTEP01_GET_EL_POS = position_microstepping_electrical_get + + def speed_current_get(ps: "PowerStepSimulator", action: bool = False): + """reads the current motor speed. + The important notice: for the correct response to the CMD_PowerSTEP01_GET_SPEED command the minimum speed should be set = 0x00 by command CMD_PowerSTEP01_SET_MIN_SPEED before sending the command CMD_PowerSTEP01_GET_SPEED. Otherwise the result could be wrong for low speed movement and stops""" + + # print("speed_get", action) # -> speed_verify_range(argument_raw, 15, 15600) + + return SmsdLan.Response.Code.speed_current, ps.motorController.speedCurrent + + CMD_PowerSTEP01_GET_SPEED = speed_current_get + + # the ones for which limits are documented + def speed_min_set(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """sets the motor minimum speed. The DATA field should contain the speed value in range 0 – 950 steps/sec. + Attention: the speed commands are always set as full steps per second.""" + print("speed_min_set", argument.speed, action) + ps.motorController.speedMin = argument.speed + + CMD_PowerSTEP01_SET_MIN_SPEED = speed_min_set + + def speed_min_get(ps: "PowerStepSimulator", action: bool = False): + """reads the current set minimum motor speed""" + # print("speed_min_get", action) # -> speed_verify_range(argument_raw, 0, 950) + return SmsdLan.Response.Code.speed_min, ps.motorController.speedMin + + CMD_PowerSTEP01_GET_MIN_SPEED = speed_min_get + + def speed_max_set(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """sets the motor maximum speed. The DATA field should contain the speed value in range 16 – 15600 steps/sec. + Attention: the speed commands are always set as full steps per second.""" + print("speed_max_set", argument.speed, action) + ps.motorController.speedMax = argument.speed + + CMD_PowerSTEP01_SET_MAX_SPEED = speed_max_set + + def speed_max_get(ps: "PowerStepSimulator", action: bool = False): + """reads the current set maximum motor speed""" + print("speed_max_get", action) # -> speed_verify_range(argument_raw, 16, 15600) + return SmsdLan.Response.Code.speed_max, ps.motorController.speedMax + + CMD_PowerSTEP01_GET_MAX_SPEED = speed_max_get + + def speed_full_step_set(ps: "PowerStepSimulator", argument: "SpeedVerifyRange", action: bool = False): + """sets the running speed, when the motor switches to a full step mode. The DATA field should contain the speed value in range 15 – 15600 steps/sec. + Attention: the speed commands are always set as full steps per second.""" + print("speed_full_step_set", argument.speed, action) + ps.motorController.speedFullStep = argument.speed + + CMD_PowerSTEP01_SET_FS_SPEED = speed_full_step_set + + def acceleration_set(ps: "PowerStepSimulator", argument: "AccelerationVerifyRange", action: bool = False): + """sets the motor acceleration to getting the motor maximum speed. The DATA field should contain the acceleration value in range 15 – 59000 steps/sec2.""" + print("acceleration_set", argument.acceleration, action) + ps.motorController.accelerationSet = argument.acceleration + + CMD_PowerSTEP01_SET_ACC = acceleration_set + + def decelleration_set(ps: "PowerStepSimulator", argument: "AccelerationVerifyRange", action: bool = False): + """sets the motor deceleration. The DATA field should contain the DECELERATION value in range 15 – 59000 steps/sec2.""" + print("decelleration_set", argument.acceleration, action) + ps.motorController.decellerationSet = argument.acceleration + + CMD_PowerSTEP01_SET_DEC = decelleration_set + + def motor_mode_set(ps: "PowerStepSimulator", argument: "MotorMode", action: bool = False): + """ + sets motor and control parameters: + * current or voltage + * motor model (determines motor analog parameters: max. current per phase, resistance per phase, inductance per phase, Step angle) + * microstepping mode + * operating current + * holding current""" + print("mode_set", argument, action) + mc = ps.motorController + d = mc.driver + d.currentHold = argument.hold_current + d.currentWork = argument.work_current + mc.microstepLog = argument.microstepping_nlog + d.model = argument.motor_model + d.isInCurrentMode = argument.is_in_current_mode + + CMD_PowerSTEP01_SET_MODE = motor_mode_set + + def motor_mode_get(ps: "PowerStepSimulator", action: bool = False): + """reads motor control parameters from the controller + those in CMD_PowerSTEP01_SET_MODE + number of program, which is available to be started by external signals""" + print("mode_get", action) # -> motor_mode + mc = ps.motorController + d = mc.driver + SmsdLan.Response.Code.mode, serializeMotorMode(program_n=0, hold_current=d.currentHold, work_current=d.currentWork, microstepping_nlog=mc.microstepLog, motor_model=d.model, is_in_current_mode=d.isInCurrentMode) + + CMD_PowerSTEP01_GET_MODE = motor_mode_get diff --git a/SMSD/lan/powerStepSim/motorsData/__init__.py b/SMSD/lan/powerStepSim/motorsData/__init__.py new file mode 100644 index 0000000..82241c9 --- /dev/null +++ b/SMSD/lan/powerStepSim/motorsData/__init__.py @@ -0,0 +1,94 @@ +from pathlib import Path + +thisDir = Path(__file__).parent + + +def tryIntoNum(v: str): + try: + return int(v) + except ValueError: + try: + return float(v) + except ValueError: + return v + + +def numericDict(d): + return {k: tryIntoNum(v) for k, v in d.items()} + + +def loadTSV(p: Path): + return [numericDict(el) for el in csv.DictReader(p.read_text().splitlines(), dialect=csv.excel_tab)] + + +def loadProprietaryNames(): + proprietary_names = loadTSV(thisDir / "proprietary_names.tsv") + + idx = {} + revIdx = {} + + for el in proprietary_names: + print(el) + idx[el["name_smsd"]] = el["name"] + revIdx[el["name"]] = el["name_smsd"] + + return idx, revIdx + + +proprietaryNameToName, nameToProprietaryName = loadProprietaryNames() + + +def loadUnknownMotorsSpecs(): + unknown_motors = loadTSV(thisDir / "unknown_motors.tsv") + + res = {} + for el in unknown_motors: + iD = el["SMSD-8.0LAN"] - 1 + del el["SMSD-8.0LAN"] + res[iD] = el + return res + + +unknown_motors = loadUnknownMotorsSpecs() + + +def loadMotorNames(): + motorNames = loadTSV(thisDir / "motors_names.tsv") + nameToEight = {} + eightToName = [None] * len(motorNames) + + fourToEight = [None] * len(motorNames) + eightToFour = [None] * len(motorNames) + + for el in motorNames: + eightNo = el["SMSD-8.0LAN"] - 1 + connT = el["connection_type"] == "p" + if el["name"]: + nameToEight[(el["name"], connT)] = eightNo + eightToName[eightNo] = (el["name"], connT) + + if el["SMSD-4.2LAN"]: + fourNo = el["SMSD-4.2LAN"] - 1 + fourToEight[fourNo] = eightNo + eightToFour[eightNo] = fourNo + + return nameToEight, eightToName, fourToEight, eightToFour + + +nameToEight, eightToName, fourToEight, eightToFour = loadMotorNames() + + +def getMotorIdByNameAndType(name: str, isParallel: bool = False, version: int = 8): + eightNum = nameToEight[name, isParallel] + 1 + if version == 4: + return eightToFour[eightNum] + return eightNum + + +def motorIdIntoNameAndConnectionType(motorId: int, version: int = 8): + motorId -= 1 + + if version == 4: + motorId = fourToEight[motorId] + + return eightToName[motorId] diff --git a/SMSD/lan/powerStepSim/motorsData/motors_names.tsv b/SMSD/lan/powerStepSim/motorsData/motors_names.tsv new file mode 100644 index 0000000..e577306 --- /dev/null +++ b/SMSD/lan/powerStepSim/motorsData/motors_names.tsv @@ -0,0 +1,55 @@ +name SMSD-4.2LAN SMSD-8.0LAN connection_type searched_by_specs +42STH33-1334 1 1 s 0 +42STH33-1334 2 2 s 0 +42STH38-1206 3 3 s 0 +42STH38-1684 4 4 s 0 +42STH38-1684 5 5 s 0 +42STH47-1206 6 6 s 0 +42STH47-1684 7 7 s 0 +42STH47-1684 8 8 s 0 +42STH60-1206 9 9 s 0 +42STH60-1206 10 10 s 0 +57ST41-1564 11 11 s 0 +57ST76-1006 12 12 s 0 +57ST76-1506 13 13 s 0 +57STH41-1006 14 14 s 0 +57STH41-1006 15 15 s 0 +57STH41-2804 16 16 s 0 +57STH41-2804 17 17 s 0 +57STH51-1006 18 18 s 0 +57STH51-2804 19 19 s 0 +57STH51-2804 20 20 s 0 +57STH56-1006 21 21 s 0 +57STH56-2006 22 22 s 0 +57STH56-2804 23 23 s 0 +57STH76-1006 24 24 s 0 +57STH76-2804 25 25 s 0 +57STH76-2804 26 26 p 0 +60STH65-2008 27 27 s 0 +60STH65-2008 28 28 p 0 +60STH86-2008 29 29 s 0 +60STH86-2008 30 30 p 0 +86STH65-2808 31 31 s 0 +86STH65-2808 32 32 p 0 +86STH80-4208 33 33 p 0 +86STH80-4208 34 34 s 0 +86STH118-4208 35 35 p 0 +86STH118-4208 36 36 s 0 +86STH156-4208 38 38 s 0 +86STH118-6004 39 s 1 +86STH156-6204 40 s 1 +110STH99-5504 41 s 1 +110STH150-6504 42 s 1 +110STH201-8004 43 s 1 + 37 37 s 0 + 39 44 s 0 + 40 45 s 0 + 41 46 s 0 + 42 47 s 0 + 43 48 s 0 + 44 49 s 0 + 45 50 s 0 + 51 s 0 + 52 s 0 + 53 s 0 + 54 s 0 diff --git a/SMSD/lan/powerStepSim/motorsData/proprietary_names.tsv b/SMSD/lan/powerStepSim/motorsData/proprietary_names.tsv new file mode 100644 index 0000000..fa55a2c --- /dev/null +++ b/SMSD/lan/powerStepSim/motorsData/proprietary_names.tsv @@ -0,0 +1,6 @@ +name name_smsd +42STH47-1684 SM4247 +57STH76-2804 SM5776 +86STH80-4208 SM8680 +86STH80-4208 SM8680 +110STH201-8004 SM110201 diff --git a/SMSD/lan/powerStepSim/motorsData/unknown_motors.tsv b/SMSD/lan/powerStepSim/motorsData/unknown_motors.tsv new file mode 100644 index 0000000..9ebef13 --- /dev/null +++ b/SMSD/lan/powerStepSim/motorsData/unknown_motors.tsv @@ -0,0 +1,13 @@ +current_per_phase resistance_per_phase inductance_per_phase angle_step SMSD-8.0LAN +4.2 0.625 8 1.8 37 +0.3 32 40 1.8 44 +0.67 8.5 7.5 1.8 45 +1.68 2.3 3.4 1.8 46 +3 1 3.4 1.8 47 +3 1.45 6.5 1.8 48 +3 1.2 6.4 1.8 49 +4.5 0.36 3 1.8 50 +6 0.6 5.7 1.8 51 +6.2 0.7 8.5 1.8 52 +8 0.8 16 1.8 53 +6 0.8 8.7 1.8 54 diff --git a/SMSD/lan/powerStepSim/nanoops.py b/SMSD/lan/powerStepSim/nanoops.py new file mode 100644 index 0000000..1775c10 --- /dev/null +++ b/SMSD/lan/powerStepSim/nanoops.py @@ -0,0 +1,133 @@ +import typing +from dataclasses import dataclass + +from .vm import CPU, FlagID, RegID + + +class NanoOp: + def __call__(self, cpu): + raise NotImplementedError + + +class nop(NanoOp): + def __call__(self, cpu): + pass + + +class energize(NanoOp): + def __call__(self, cpu): + cpu.motorController.driver.energize() + + +class deenergize(NanoOp): + def __call__(self, cpu): + cpu.motorController.driver.deenergize() + + +class makeStep(NanoOp): + def __call__(self, cpu): + motorController.makeStep() + + +class sleep(NanoOp): + amount: int + + def __call__(self, cpu): + # sleep(self.amount) in real hardware + pass + + +class sleep_interruptible(sleep): + pass + + +class getFlag(NanoOp): + flag: FlagID + + def __call__(self, cpu): + cpu.cmpRes = cpu.flags[flag] + + +@dataclass +class set_immed(NanoOp): + target: RegID + immed: int + + def __call__(self, cpu): + cpu.regs[self.target] = immed + + +@dataclass +class TwoRegOperandNanoOp(NanoOp): + a: RegID + b: RegID + + +class set(TwoRegOperandNanoOp): + def __call__(self, cpu): + cpu.regs[self.a] = cpu.regs[self.b] + + +class incr(TwoRegOperandNanoOp): + def __call__(self, cpu): + cpu.regs[self.a] += cpu.regs[self.b] + + +class eq(TwoRegOperandNanoOp): + def __call__(self, cpu): + cpu.cmpRes = cpu.regs[self.a] == cpu.regs[self.b] + + +class gt(TwoRegOperandNanoOp): + def __call__(self, cpu): + cpu.cmpRes = cpu.regs[self.a] > cpu.regs[self.b] + + +@dataclass +class NestedInstr(NanoOp): + op: NanoOp + + +class repeat_untill(NestedInstr): + def __call__(self, cpu): + while cpu.cmpRes and not cpu.interruptLoop: + self.op(cpu) + + +class repeat_ethernally(NestedInstr): + def __call__(self, cpu): + while not cpu.interruptLoop: + self.op(cpu) + + +@dataclass +class repeat(NestedInstr): + count: RegID + + def __call__(self, cpu): + for i in range(cpu.regs[self.count]): + if cpu.interruptLoop: + break + + self.op(cpu) + + +class cond_true(NestedInstr): + def __call__(self, cpu): + if cpu.cmpRes: + self.op(cpu) + + +class cond_false(NestedInstr): + def __call__(self, cpu): + if not cpu.cmpRes: + self.op(cpu) + + +@dataclass +class group(NanoOp): + ops: typing.Sequence[NanoOp] + + def __call__(self, cpu): + for op in self.ops: + op(cpu) diff --git a/SMSD/lan/powerStepSim/vm.py b/SMSD/lan/powerStepSim/vm.py new file mode 100644 index 0000000..f17702a --- /dev/null +++ b/SMSD/lan/powerStepSim/vm.py @@ -0,0 +1,33 @@ +from enum import IntEnum + + +class CPU: + def __init__(self): + self.regs = [0] * len(RegisterID) + self.cmpRes = False + self.interruptLoop = False + self.queue = [] + + +class RegID(IntEnum): + none = 0 + ip = 1 + program = 2 + uip = 3 + zero = 4 + label = 5 + mark = 6 + position = 7 + sleepCounter = 8 + currentSpeed = 9 + targetSpeed = 10 + currentAcceleration = 11 + targetAcceleration = 12 + countOfSpeedSteps = 13 + + +class FlagID(IntEnum): + none = 0 + SET_ZERO = 1 + IN0 = 2 + IN1 = 3 diff --git a/SMSD/lan/protocol.py b/SMSD/lan/protocol.py new file mode 100644 index 0000000..d76aeea --- /dev/null +++ b/SMSD/lan/protocol.py @@ -0,0 +1,217 @@ +import re +import typing +from enum import IntEnum +from io import BytesIO +from ipaddress import IPv4Address, IPv4Interface +from pathlib import Path + +from kaitaistruct import KaitaiStream, ValidationFailedError, ValidationExprError + +from ..kaitai.smsd_lan import SmsdLan +from ..kaitai.smsd_limits import SmsdLimits +from SaneIO.core import SIO1HiNLoMux, SIOTransport, UnsafelyMuxableSIO1HiNLoMux +from SaneIO.protocols.serial import AsyncSIO_UARTServer +from SaneIO.protocols.tcp import AsyncSIO_TCPServer +from .powerStepSim import PowerStepResponder +from .serializers import calcChecksum, netMaskIntoPrefixLengthFromBytes, serializeMessage, serializeNetworkConfig, serializeResponse, serializeVersionInfo +from .uart import SMSDLAN_SIOTransport_UART + +_DEFAULT_NETMASK = 16 + + +class NetworkConfig: + __slots__ = ("mac", "ip", "gateway", "dns", "port", "dhcpMode") + + def __init__(self, mac=bytes((0x00, 0xF8, 0xDC, 0x3F, 0x00, 0x00)), ip: IPv4Interface = IPv4Interface((bytes((192, 168, 1, 2)), _DEFAULT_NETMASK)), gateway: IPv4Interface = IPv4Interface((bytes((192, 168, 1, 1)), _DEFAULT_NETMASK)), dns: IPv4Address = IPv4Address(bytes((0, 0, 0, 0))), port: int = 5000, dhcpMode: int = 1): + self.mac = mac + self.ip = ip + self.gateway = gateway + self.dns = dns + self.port = port + self.dhcpMode = dhcpMode + + +smsdLimits = SmsdLimits(None) + + +class CommandParseException(Exception): + __slots__ = () + + @property + def cmd(self) -> SmsdLan: + return self.args[0] + + @property + def code(self) -> SmsdLan.Response.Code: + return self.args[1] + + @property + def src_path(self) -> str: + return self.args[2] + + +def parseCommand(commandRaw: bytes) -> SmsdLan: + with BytesIO(commandRaw) as f: + s = KaitaiStream(f) + cmd = SmsdLan(smsdLimits, s) + try: + cmd._read() + except ValidationExprError as ex: + if ex.src_path == "/seq/1": + raise CommandParseException(cmd, SmsdLan.Response.Code.wrong_checksum, ex.src_path) from ex + except ValidationFailedError as ex: + if ex.src_path == "/types/header/seq/4": + raise CommandParseException(cmd, SmsdLan.Response.Code.wrong_length, ex.src_path) from ex + else: + raise CommandParseException(cmd, SmsdLan.Response.Code.out_of_range, ex.src_path) from ex + return cmd + + +class Responder(SIOTransport): + __slots__ = ("server", "powerStepResponder", "commands") + + VERSION_INFO = ((0x0, 0x0), (0x0, 0x0), 2) + + DEBUG = False + + def __init__(self, server, powerStepResponder: PowerStepResponder = None): + super().__init__() + self.server = server + + if powerStepResponder is None: + powerStepResponder = PowerStepResponder() + + self.powerStepResponder = powerStepResponder + self.commands = [] + + def __call__(self, proto, cmd): + processor = getattr(self, cmd.header.type.name, None) + if processor: + whatToRespond = processor(proto, cmd) + if whatToRespond is None: + pass + elif isinstance(whatToRespond, tuple) and len(whatToRespond) == 2: + resType, resPayload = whatToRespond + if resType is None: + resType = cmd.header.type + resp = serializeMessage(typ=resType, iD=cmd.header.id, payload=resPayload) + self.sendCommand(proto, resp) + else: + raise ValueError(self.__class__.__name__ + ": Responder method has returned invalid stuff. It must either be None or (type, payload) tuple", whatToRespond) + + else: + print(self.__class__.__name__ + ": No processor for ", cmd.header.type) + self.sendCommand(proto, serializeMessage(typ=SmsdLan.Type.response, iD=cmd.header.id, payload=self.powerStepResponder.serializeResponse(SmsdLan.Response.Code.wrong_command, 0))) + + def password(self, proto, cmd): + print("Password", cmd.data) + return SmsdLan.Type.response, self.powerStepResponder.serializeResponse(SmsdLan.Response.Code.auth_success, 0) + + def version_data(self, proto, cmd): + VERSION_INFO = self.__class__.VERSION_INFO + return None, serializeVersionInfo(VERSION_INFO[0][0], VERSION_INFO[0][1], VERSION_INFO[1][0], VERSION_INFO[1][1], VERSION_INFO[2]) + + def power_step(self, proto, cmd): + return self.powerStepResponder(cmd.data) + + def network_config_set(self, proto, cmd): + cfg = cmd.data + maskPrefixLength = netMaskIntoPrefixLengthFromBytes(cfg.subnet_mask.ipv4) + self.server.networkConfig = NetworkConfig(mac=cfg.mac.mac, ip=IPv4Interface((cfg.my_ip.ipv4, maskPrefixLength)), gateway=IPv4Interface((cfg.gateway.ipv4, maskPrefixLength)), dns=IPv4Address(cfg.dns.ipv4), port=cfg.port, dhcpMode=cfg.dhcp_mode) + + def network_config_get(self, proto, cmd): + cfg = self.server.networkConfig + return None, serializeNetworkConfig(cfg.mac, cfg.ip.packed, cfg.ip.netmask.packed, cfg.gateway.packed, cfg.dns.packed, cfg.port, cfg.dhcpMode) + + def sendCommand(self, proto, cmd: bytes): + return proto.sendBytes(cmd) + + # SIOTransport interface + + def onReceive(self, proto, commandRaw: bytes): + if self.__class__.DEBUG: + print("Received", commandRaw) + + try: + cmd = parseCommand(commandRaw) + ok = True + except CommandParseException as ex: + cmd = ex.cmd + if ex.code == SmsdLan.Response.Code.wrong_length: + print("Length specified in the header has exceeded the specified upper limit:", commandRaw) + elif ex.code == SmsdLan.Response.Code.wrong_checksum: + print("Wrong checksum for the command:", commandRaw) + else: + print("A value has failed validation: ", ex) + self.sendCommand(proto, self.powerStepResponder.serializeResponse(ex.code, 0)) + ok = False + + # print("cmd", cmd, cmd.header.type) + self.commands.append(cmd) + if ok: + self(proto, cmd) + + def checkChecksum(self, commandRaw: bytes): + ourChecksum = calcChecksum(commandRaw[1:]) + return commandRaw[0] == ourChecksum + + def ifThisResponder(self, data: bytes): + return self.checkChecksum(data) + + +class Server(SIOTransport): + __slots__ = ( + "upper", + "networkConfig", + "rawMux", + "uartFramerAndEscaper", + "tcpServer", + "uartTCPServer", + "uartEscapedMux", + "uartServer" + ) + + def __init__(self): + super().__init__() + + resp = Responder(self) + self.bindHigher(resp) + + self.networkConfig = NetworkConfig(ip=IPv4Interface(("127.0.0.1", 32))) + + self.rawMux = SIO1HiNLoMux() + self.rawMux.bindHigher(self) + + self.uartFramerAndEscaper = SMSDLAN_SIOTransport_UART() + self.uartEscapedMux = UnsafelyMuxableSIO1HiNLoMux() + self.uartEscapedMux.bindHigher(self.uartFramerAndEscaper) + + self.uartFramerAndEscaper.bindHigher(self.rawMux) + + self.tcpServer = None + self.uartTCPServer = None + self.uartServer = None + + async def _startUARTServer(self, port: typing.Union[Path, str]): + self.uartServer = await AsyncSIO_UARTServer.create(port) + self.uartServer.bindHigher(self.uartEscapedMux) + return self.uartServer + + def startUARTServer(self, port: typing.Union[Path, str]): + if port not in self.uartEscapedMux: + return self._startUARTServer(port) + else: + raise RuntimeError("Server already running on this port!", port) + + async def startTCPServer(self): + self.tcpServer = await AsyncSIO_TCPServer.create(self.rawMux, host=str(self.networkConfig.ip.ip), port=self.networkConfig.port) + return self.tcpServer + + async def startUARTTCPServer(self, port: int = 14379): + """The same as TCP, but the data is segmented and escaped like in UART protocol. + Intended to be used to be connected by VirtualBox virtual COM port in TCP mode""" + self.uartTCPServer = await AsyncSIO_TCPServer.create(self.uartEscapedMux, host=str(self.networkConfig.ip.ip), port=port) + return self.uartTCPServer + + +# SMC-Program-LAN-v.7.0.6 requires the response to be sent within a strict time window (50 ms by default). It can be extended by editing the config files manually. diff --git a/SMSD/lan/serializers.py b/SMSD/lan/serializers.py new file mode 100644 index 0000000..ddfaf52 --- /dev/null +++ b/SMSD/lan/serializers.py @@ -0,0 +1,135 @@ +import typing +from struct import Struct + + +def calcChecksum(data) -> int: + v = 0xFF + for el in data: + v = (v + el) & 0xFF + return v ^ 0xFF + + +singleU4LE = Struct(" int: + return (cycles << 10) | commands + + +def serializeMicrosteps(microsteps: int) -> int: + fullMask = (1 << 22) - 1 + return microsteps & fullMask + + +powerstepCommandStruct = Struct(" bytes: + return powerstepCommandStruct.pack((argument_raw << 10) | (command << 4) | (action << 3) | reserved) + + +def serializeMac(mac): + return bytes(mac.mac) + + +def serializeIPv4(ipv4): + return bytes(ipv4.ipv4) + + +def serializeLanConfig(cfg): + return serializeMac(cfg.mac) + serializeIPv4(cfg.my_ip) + erializeIPv4(cfg.subnet_mask) + erializeIPv4(cfg.gateway) + erializeIPv4(cfg.dns) + struct.pack(" int: + return (int(is_deenergized) << 0) | (int(is_ready) << 1) | (int(is_sw_on) << 2) | (int(has_sw_event_happenned) << 3) | (int(is_rotating_direction_forward) << 4) | (int(accelerationStatus) << 5) | (int(is_command_error) << 7) + + +def serializePowerStepStatus(is_deenergized: bool, is_ready: bool, is_sw_on: bool, has_sw_event_happenned: bool, is_rotating_direction_forward: bool, accelerationStatus: "AccelerationStatus", is_command_error: bool, reserved: int = 0) -> bytes: + return powerStepStatusStruct.pack(serializePowerStepStatusFirstByte(is_deenergized, is_ready, is_sw_on, has_sw_event_happenned, is_rotating_direction_forward, accelerationStatus, is_command_error), reserved) + + +responseStruct = Struct(" bytes: + return serializePowerStepStatus(is_deenergized, is_ready, is_sw_on, has_sw_event_happenned, is_rotating_direction_forward, accelerationStatus, is_command_error) + responseStruct.pack(code, return_data) + + +versionInfoStruct = Struct(" bytes: + return headerStruct.pack(xor_sum, version, typ, iD, length) + + +currentProtocolVersion = 2 + + +def serializeMessage(typ, iD, payload): + hdr = serializeHeader(xor_sum=0, version=currentProtocolVersion, typ=typ, iD=iD, length=len(payload)) + summed = (hdr + payload)[1:] + return bytes((calcChecksum(summed),)) + summed + + +def packBits(bools: typing.Collection[bool]) -> int: + res = 0 + for i, b in enumerate(bools): + res |= int(b) << i + return res + + +def serializeEventStatus(inputs, masks, events) -> int: + assert len(inputs) == len(masks) == len(events) == 8 + return packBits(inputs + masks + events) + + +def serializeInstructionPointer(program, command) -> int: + return (program << 8) | command + + +def serializeMotorMode(program_n: int, hold_current: float, work_current: float, microstepping_nlog: int, motor_model: "MotorModel", is_in_current_mode: bool): + if hold_current > 1: + raise ValueError("Hold current cannot be more than 1") + hold_current -= 0.25 + if hold_current < 0: + raise ValueError("Hold current cannot be less than 0.25") + hold_current_raw = int(round(hold_current / 0.25)) + + work_current_raw = int(round(work_current / 0.1)) + + res = (program_n << 20) | (hold_current_raw << 17) | (work_current_raw << 10) | (microstepping_nlog << 7) | (motor_model << 1) | int(is_in_current_mode) + return res + + +networkConfigTail = Struct(" int: + return singleU4LE.unpack(b"\xff\xff\x00\x00")[0].bit_length() diff --git a/SMSD/lan/uart.py b/SMSD/lan/uart.py new file mode 100644 index 0000000..b90fadd --- /dev/null +++ b/SMSD/lan/uart.py @@ -0,0 +1,89 @@ +import re +from queue import Queue + +from SaneIO.core import SIOTransport, StreamingHalfDuplexInBandSignalling_SIOTransport + +__all__ = ("SMSDLANUART_SIOTransport",) + +START_MARKER = 0xFA +END_MARKER = 0xFB +ESCAPE_MARKER = 0xFE + +START_MARKER_BYTES = bytes((START_MARKER,)) +END_MARKER_BYTES = bytes((END_MARKER,)) +ESCAPE_MARKER_BYTES = bytes((ESCAPE_MARKER,)) + +escapedBytes = bytes((START_MARKER, END_MARKER, ESCAPE_MARKER)) + + +def _perm(b: int) -> int: + return b ^ 0x80 + + +deUartRx = re.compile(ESCAPE_MARKER_BYTES + b"([" + bytes(_perm(el) for el in escapedBytes) + b"])") + + +def deUARTReplacer(m: re.Match): + return bytes((_perm(m.group(1)[0]),)) + + +uartRx = re.compile(b"[" + escapedBytes + b"]") + + +def UARTReplacer(m: re.Match): + return bytes( + ( + ESCAPE_MARKER, + _perm(m.group(0)[0]), + ) + ) + + +def escapeForUART(data: bytes) -> bytes: + return START_MARKER_BYTES + uartRx.subn(UARTReplacer, data)[0] + END_MARKER_BYTES + + +def unescapeForUART(data: bytes) -> bytes: + if data[0] == START_MARKER and data[-1] == END_MARKER: + data = data[1:-1] + + return deUartRx.subn(deUARTReplacer, data)[0] + + raise ValueError("Invalid SMSD LAN UART packet") + + +class SMSDLAN_SIOTransport_UART(StreamingHalfDuplexInBandSignalling_SIOTransport): + """Sans-IO-style protocol for decoding UART SMSD LAN protocol into raw LAN protocol""" + + __slots__ = ( + "buffer", + "inCommand", + ) + + def __init__(self): + super().__init__() + self.buffer = bytearray() + self.inCommand = False + + def canSend(self) -> bool: + return not self.inCommand + + def receiveByte(self, b: int): + if not self.inCommand: + if b == START_MARKER: + self.inCommand = True + self.buffer.append(b) + else: + self.buffer.append(b) + if b == END_MARKER: + self.inCommand = False + deuarted = unescapeForUART(self.buffer) + self.buffer = type(self.buffer)() + self.higher.onReceive(self, deuarted) + self.sendCommandsInCertainStates() + + def filterSentBytes(self, b: bytes) -> bytes: + return escapeForUART(b) + + def ifThisResponder(self, data: bytes): + return self.inCommand or data[0] == START_MARKER diff --git a/SMSD/text/__init__.py b/SMSD/text/__init__.py new file mode 100644 index 0000000..4c63012 --- /dev/null +++ b/SMSD/text/__init__.py @@ -0,0 +1,2 @@ +from .commands import * +#from .serial import SMSD diff --git a/SMSD/text/asio.py b/SMSD/text/asio.py new file mode 100644 index 0000000..a4aebd1 --- /dev/null +++ b/SMSD/text/asio.py @@ -0,0 +1,71 @@ +import asyncio +from queue import Queue + +import serial_asyncio + +from enum import Enum + + +class SMSDProtocol(asyncio.Protocol): + __slots__ = ("commands", "buffer", "transport", "currentBatch", "tx", "isEndOfBatch", "expectedEcho") + + def __init__(self): + self.buffer = bytearray() + self.commands = [] + self.tx = Queue() + self.currentBatch = [] + self.transport = None + self.isEndOfBatch = True + self.expectedEcho = None + + def connection_made(self, transport): + self.transport = transport + transport.serial.rts = False # You can manipulate Serial object via transport + transport.serial.read() + + def sendCommand(self, cmd): + if isinstance(cmd, Enum): + cmdStr = cmd.value + else: + cmdStr = cmd + + self.tx.put(cmdStr) + self.sendCommandInCertainStates() + + def sendCommandInCertainStates(self): + if self.isEndOfBatch: + if not self.tx.empty(): + commandB = self.tx.get() + self.expectedEcho = commandB + self.transport.write(commandB.encode("ascii") + b"*") # Write serial data via transport + self.tx.task_done() + self.isEndOfBatch = False + + def data_received(self, data): + for c in data: + self.isEndOfBatch = c == 0x07 + if c == ord("*"): + cmd = self.buffer.decode("ascii") + if self.expectedEcho is None: + self.currentBatch.append(cmd) + self.buffer = type(self.buffer)() + print("Command received", repr(self.currentBatch[-1])) + else: + assert self.expectedEcho == cmd + self.expectedEcho = None + elif self.isEndOfBatch: + self.commands.append(self.currentBatch) + self.currentBatch = type(self.currentBatch)() + print("Batch received", repr(self.commands[-1])) + else: + self.buffer.append(c) + self.sendCommandInCertainStates() + + def connection_lost(self, exc): + self.transport.loop.stop() + + def pause_writing(self): + pass + + def resume_writing(self): + pass diff --git a/SMSD/text/commands.py b/SMSD/text/commands.py new file mode 100644 index 0000000..9f987e0 --- /dev/null +++ b/SMSD/text/commands.py @@ -0,0 +1,214 @@ +from enum import Enum + + +class CommandWithArgs: + __slots__ = ("cmd", "arg") + + def __init__(self, cmd, arg): + self.cmd = cmd + self.arg = arg + + def __str__(self): + return self.cmd.value + str(self.arg) + + def __repr__(self): + return self.__class__.__name__ + "(" + repr(self.cmd) + ", " + repr(self.arg) + ")" + + +class MotorPower(Enum): + motorPowerON = enable = EN = "EN" + motorPowerOFF = disable = DS = "DS" + + +MotorPower.motorPowerOFF.__doc__ = "Remove power off the motor." +MotorPower.motorPowerON.__doc__ = "Put power on the motor." + + +class Direction(Enum): + left = forward = DL = "DL" + right = backward = DR = "DR" + reverse = change = RS = "RS" + + +Direction.left.__doc__ = "Set direction to left/forward" +Direction.right.__doc__ = "Set direction to right/backward" +Direction.change.__doc__ = "Change direction" + + +class Movement(Enum): + move = MV = "MV" + move_til_highest = continuous_til_input2 = MH = "MH" + move_til_lowest = continuous_til_input1 = ML = "ML" + continuous_til_input0 = HM = "HM" + + +Movement.continuous_til_input0.__doc__ = "Move till input signal on the input dedicated for zero limit switch" +Movement.continuous_til_input1.__doc__ = 'Move till input signal on the "lowest" input' +Movement.continuous_til_input2.__doc__ = 'Move till input signal on the "highest" input' +Movement.move.__doc__ = "Move the motor, either endlessly, or for n steps" + + +class ControlFlow(Enum): + loop = "SB" + setLabel = LL = "LL" + label_loop = jump_loop = JP = "JP" + + +ControlFlow.setLabel.__doc__ = "Set jump label" +ControlFlow.label_loop.__doc__ = "Repeat the program starting from the label" +ControlFlow.loop.__doc__ = "Repeat the whole program." + + +class Parameters(Enum): + acceleration = accel = "AL" + speed = SD = "SD" + startSpeed = SS = "SS" + + +Parameters.acceleration.__doc__ = "Acceleration" +Parameters.speed.__doc__ = "Speed to rotate the motor" +Parameters.startSpeed.__doc__ = "Initial speed, is subject to the acceleration" + + +class Command(Enum): + finishLoading = ED = "ED" + begin = BG = "BG" + sleep = SP = "SP" + + +Command.begin.__doc__ = "Marks the start of the program. Begins transaction?" +Command.sleep.__doc__ = "Pause/sllep program for n seconds" +Command.finishLoading.__doc__ = "Marks the end of the program. Ends transaction?" + + +class StandaloneCommand(Enum): + """Commands that can be executed in standalone mode only""" + + loadToEEPROM = LD = "LD" + readFromEEPROM = RD = "RD" + + +StandaloneCommand.loadToEEPROM.__doc__ = "Upload commands from PC to EEPROM buffer, moves controller into loading mode. Burns EEPROM, don't use." +StandaloneCommand.readFromEEPROM.__doc__ = "Output contents of the EEPROM buffer for programs." + + +class DirectCommand(Enum): + """Commands that can be executed in Direct Control mode only""" + + loadToRAM = LB = "LB" + readFromRAM = RB = "RB" + + +DirectCommand.loadToRAM.__doc__ = "Upload commands from PC to in-memory buffer. When this command is received while driving, the motor is stopped and turned off." +DirectCommand.readFromRAM.__doc__ = "Output contents of the in-memory buffer. When this command is received while driving, the motor is stopped and turned off." + + +class ExecutionCommands(Enum): + pause = PS = "PS" # takes channel name + startOrStop = ST = "ST" # takes channel name + + +ExecutionCommands.startOrStop.__doc__ = "Start or stop program execution of a program" +ExecutionCommands.pause.__doc__ = "Pause program execution" + + +class RelayCommand(Enum): + on = set_flag = SF = "SF" + off = clear_flag = CF = "CF" + + +RelayCommand.on.__doc__ = "Output high" +RelayCommand.off.__doc__ = "Output low" + + +class WaitForInterruptCommand(Enum): + wait_lowest = BX1 = IN1 = WL = "WL" + wait_highest = BX2 = IN2 = WH = "WH" + + +WaitForInterruptCommand.WL.__doc__ = 'Wait for signal on input considered "lowest" (#1)' +WaitForInterruptCommand.WH.__doc__ = 'Wait for signal on input considered "highest" (#2)' + + +class MicrosteppingCommand(Enum): + on = micro = "ON" + off = whole = "OF" + + +MicrosteppingCommand.micro.__doc__ = "Enable microstepping" +MicrosteppingCommand.whole.__doc__ = "Disable microstepping" + + +class CommandEncoder: + __slots__ = () + + def loadToEEPROM(self, channel=1): + return CommandWithArgs(StandaloneCommand.loadToEEPROM, channel) + + def readFromEEPROM(self, channel=1): + return CommandWithArgs(StandaloneCommand.readFromEEPROM, channel) + + def loadToRAM(self, channel=1): + return CommandWithArgs(StandaloneCommand.loadToRAM, channel) + + def readFromRAM(self, channel=1): + return CommandWithArgs(StandaloneCommand.readFromRAM, channel) + + def pause(self, channel=1): + return CommandWithArgs(StandaloneCommand.pause, channel) + + def startOrStop(self, channel=1): + return CommandWithArgs(StandaloneCommand.startOrStop, channel) + + def loop(ddd: int): + assert 1 <= ddd <= 255 + return CommandWithArgs(Command.loop, ddd) + + SB = loop + + def acceleration(ddd: int): + assert -1000 <= ddd <= 1000 + "Acceleration" + return CommandWithArgs(Parameters.acceleration, ddd) + + AL = accel = acceleration + + def speed(ddd: int): + assert 1 <= ddd <= 10000 + return CommandWithArgs(Parameters.speed, ddd) + + SD = speed + + def startSpeed(ddd: int): + assert 1 <= ddd <= 2000 + return CommandWithArgs(Parameters.startSpeed, ddd) + + SS = startSpeed + + def steps(ddd: int): + assert 1 <= ddd <= 10000000 + return CommandWithArgs(Movement.steps, ddd) + + move = MV = steps + + def label_loop(ddd: int): + assert 1 <= ddd <= 255 + return "JP" + str(ddd) + return CommandWithArgs(ControlFlow.label_loop, ddd) + + jump_loop = JP = label_loop + + def sleep(ddd: int): + assert 1 <= ddd <= 100000000 + return CommandWithArgs(Command.sleep, ddd) + + SP = sleep + + +class ErrorCode(Enum): + ok = "E10" + error = "E13" + completed = "E14" + communication_err = "E15" + cmd_err = "E16" + cmd_arg_err = "E19" diff --git a/SMSD/text/formats.py b/SMSD/text/formats.py new file mode 100644 index 0000000..2389571 --- /dev/null +++ b/SMSD/text/formats.py @@ -0,0 +1,116 @@ +from dataclasses import dataclass +import re +import typing +from struct import pack, unpack +from enum import Enum + +from .commands import Command, CommandWithArgs, ControlFlow, DirectCommand, Direction, ExecutionCommands, MicrosteppingCommand, MotorPower, Movement, Parameters, RelayCommand, StandaloneCommand, WaitForInterruptCommand +from .hash import hashtableLookup + + +def derot(el, key=0x7E): + return bytes(((c - key) & 0xFF) for c in el) + + +def rot(el, key=0x7E): + return derot(el, -key) + + +ENCODING = "cp1251" + + +def decodeSmcStream(d: bytes) -> typing.Iterable[str]: + res = derot(d).decode(ENCODING).split("\r\n") + if not res[-1]: + res = res[:-1] + return res + + +rawHashTable = { + 'начало программы': Command.begin, + 'движение влево': Direction.forward, + 'движение вправо': Direction.backward, + 'реверс': Direction.change, + 'установить сигнал "разрешение"': MotorPower.motorPowerON, + 'снять сигнал "разрешение"': MotorPower.motorPowerOFF, + 'режим дробления шага': MicrosteppingCommand.on, + 'режим целого шага': MicrosteppingCommand.off, + 'скорость': Parameters.speed, + 'начальная скорость': Parameters.startSpeed, + 'остановка на': Command.sleep, + 'ускорение': Parameters.acceleration, + 'выполнить': Movement.move, + 'установить флаг': RelayCommand.on, + 'ждать младший флаг': WaitForInterruptCommand.wait_lowest, + 'ждать старший флаг': WaitForInterruptCommand.wait_highest, + 'снять флаг': RelayCommand.off, + 'установить метку': ControlFlow.setLabel, + 'выполнить от метки': ControlFlow.label_loop, + 'начать загрузку:': StandaloneCommand.loadToEEPROM, + 'завершить загрузку': Command.finishLoading, + 'начать/закончить работу:': ExecutionCommands.startOrStop, + 'поставить/снять паузу:': ExecutionCommands.pause, + + 'program begin': Command.begin, + 'forward motion': Direction.forward, + 'backward motion': Direction.backward, + 'reverse': Direction.change, + 'set "disable"': MotorPower.motorPowerOFF, + 'set "enable"': MotorPower.motorPowerON, + 'acceleration': Parameters.acceleration, + 'speed': Parameters.speed, + 'start speed': Parameters.startSpeed, + 'move till zero limit switch': Movement.continuous_til_input0, + 'move till input signal ml': Movement.move_til_lowest, + 'move till input signal mh': Movement.move_til_highest, + 'move': Movement.move, + 'pause for': Command.sleep, + 'set signal to output': RelayCommand.on, + 'clear signal from output': RelayCommand.off, + 'wait signal wl to input': WaitForInterruptCommand.wait_lowest, + 'wait signal wh to input': WaitForInterruptCommand.wait_highest, + 'set label': ControlFlow.setLabel, + 'repeat from label': ControlFlow.label_loop, + 'record commands to buffer': DirectCommand.loadToRAM, + 'repeat buffer': ControlFlow.loop, + 'start commands recording:': StandaloneCommand.loadToEEPROM, + 'end commands recording': Command.finishLoading, + 'start/stop program executing:': ExecutionCommands.startOrStop, + 'set/take off pause:': ExecutionCommands.pause, +} + + +argRe = re.compile("(-?\d+)") + +#def hashtableLookup(s: str) -> typing.Optional[Enum]: +# return rawHashTable.get(s, None) + + +def decodeKnownCommandFromHumanText(rawString: str): + rawString = rawString.lower().strip() + + splitted = argRe.split(rawString, 1) + + if len(splitted) == 3: + cmd, arg, rest = splitted + cmd = cmd.strip() + arg = int(arg) + rest = rest.strip() + else: + cmd = rawString + arg = None + rest = None + + cmd = hashtableLookup(cmd) + if cmd: + return CommandWithArgs(cmd, arg) + + +def _decode_smcStream(d: bytes): + for el in decodeSmcStream(d): + res = decodeKnownCommandFromHumanText(el) + yield res + + +def decode_smcStream(d: bytes): + return list(_decode_smcStream(d)) diff --git a/SMSD/text/hash.py b/SMSD/text/hash.py new file mode 100644 index 0000000..04a1310 --- /dev/null +++ b/SMSD/text/hash.py @@ -0,0 +1,124 @@ +import typing +from hashlib import blake2s +from struct import Struct +from .commands import (Command, ControlFlow, DirectCommand, Direction, ExecutionCommands, MicrosteppingCommand, MotorPower, Movement, Parameters, RelayCommand, StandaloneCommand, WaitForInterruptCommand) +from enum import Enum + + +class uint8_t(int): + __slots__ = () + + +class uint16_t(int): + __slots__ = () + + +twoShorts = Struct(" uint16_t: + (a, b) = twoShorts.unpack(full) + return a ^ b + + +def hashtableLookup(s: typing.Any) -> typing.Optional[typing.Any]: + hC = gHT.c.h + t = gHT.t + (reduced, half, full) = singleByteHashStrings(gHT.c.p, hC.o2b(s)) + idx = perfectHashRemap(hC, reduced) + (control, res) = t[idx] + if control != half: + raise KeyError(s) + return res + + +def singleByteHashStrings(powCfg: "POWConfig", d: bytes): + h = blake2s(key=powCfg.nonce, digest_size=4) + h.update(d) + full = h.digest() + half = halfIntHash(full) + reduced = powCfg.reducer(*full) + return (reduced, half, full) + + +def perfectHashRemap(hashConfig: "HashConfig", i: int) -> int: + i -= hashConfig.offset + return i % hashConfig.t + hashConfig.r[i // hashConfig.t] + + +nN = (None, None) +hashedHashTable = ( + (57952, WaitForInterruptCommand.wait_lowest), + (54793, RelayCommand.off), + (35936, Direction.backward), + (10882, Parameters.startSpeed), + (53071, MotorPower.motorPowerOFF), + (18448, Parameters.speed), + (64860, ControlFlow.label_loop), + (56943, Direction.forward), + (44099, Direction.forward), + (44460, ExecutionCommands.pause), + (48393, Direction.change), + (42157, ControlFlow.label_loop), + (13627, ControlFlow.setLabel), + (17603, Movement.move_til_lowest), + (37643, ControlFlow.loop), + (20960, ExecutionCommands.startOrStop), + (19349, Parameters.acceleration), + (48516, Direction.change), + (24401, ExecutionCommands.pause), + (43127, WaitForInterruptCommand.wait_lowest), + (52627, MotorPower.motorPowerON), + (43607, Movement.move), + (18733, RelayCommand.on), + (39443, Movement.continuous_til_input0), + (63409, Movement.move), + (8821, MicrosteppingCommand.on), + nN, + (4315, MotorPower.motorPowerON), + (55745, Command.begin), + (20850, Direction.backward), + (34089, Command.sleep), + nN, + (57617, Parameters.speed), + (35824, StandaloneCommand.loadToEEPROM), + (30825, Parameters.startSpeed), + nN, + (51076, StandaloneCommand.loadToEEPROM), + (58900, DirectCommand.loadToRAM), + (48899, Command.begin), + (52044, ControlFlow.setLabel), + (49985, RelayCommand.off), + (29915, Movement.move_til_highest), + (9818, WaitForInterruptCommand.wait_highest), + (14636, ExecutionCommands.startOrStop), + (36013, Command.finishLoading), + (44971, WaitForInterruptCommand.wait_highest), + (62243, MicrosteppingCommand.off), + (50960, Parameters.acceleration), + (23084, MotorPower.motorPowerOFF), + (17252, Command.sleep), + (63829, RelayCommand.on), + (47972, Command.finishLoading), +) + + +class gHT: + class c: + class p: + nonce = b"\xb8`Mu" + + def reducer(d: int, c: int, b: int, a: int) -> uint8_t: + return a - b + c - d & 255 + + class h: + t = 13 + r = (30, 39, 4, -1, 10, 0, 40, 13, 16, 25, -1, 37, 33) + offset = 84 + + def o2b(s: str) -> bytes: + if not isinstance(s, bytes): + s = s.encode("cp1251") + return s + + t = hashedHashTable diff --git a/SMSD/text/serial.py b/SMSD/text/serial.py new file mode 100644 index 0000000..fb429c4 --- /dev/null +++ b/SMSD/text/serial.py @@ -0,0 +1,19 @@ +import serial +import serial_asyncio + +from .asio import SMSDProtocol + + +async def SMSD(eventLoop, port: str): + coro = serial_asyncio.create_serial_connection( + eventLoop, + SMSDProtocol, + port, + baudrate=9600, + bytesize=8, # CS8 + parity=serial.PARITY_EVEN, # PARENB + stopbits=1, + ) + transport, protocol = await coro + return protocol + diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..efb9808 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/genHashTable.py b/genHashTable.py new file mode 100755 index 0000000..04320b7 --- /dev/null +++ b/genHashTable.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import ast + +from pathlib import Path + +from SMSD.formats import rawHashTable +from PerfectPrecomputedHashtable import POWConfig, genPerfectHashtable +from PerfectPrecomputedHashtable.codegen import genCode, GenCfg, takeFromFile + +def o2b(s: str) -> bytes: + if not isinstance(s, bytes): + s = s.encode("cp1251") + return s + +def valueASTGen(cmd): + enumName = cmd.__class__.__name__ + valueName = cmd.name + return ast.Attribute( + value=ast.Name(id=enumName, ctx=ast.Load()), + attr=valueName, ctx=ast.Load() + ) + +fromThisFile = takeFromFile(Path(__file__), {o2b.__name__}) + +powCfg = POWConfig.fromDict({"nonce": 0x754d60b8, "span": 0xf1222157, "reducer": 8, "reduced_span": 168}) + +""" +Checked all the nonces up to 0x90000000. The most remarkable results are: +{"nonce": 0x259eebf1, "span": 0xfbe6997f, "reducer": 13, "reduced_span": 171} utf-8 +{"nonce": 0x2eebe99e, "span": 0xf9a8a317, "reducer": 0, "reduced_span": 174} cp1251 +{"nonce": 0x431dfcac, "span": 0xecab1070, "reducer": 15, "reduced_span": 174} cp1251 +{"nonce": 0x52fafdf9, "span": 0xf946c235, "reducer": 16, "reduced_span": 169} cp1251 +{"nonce": 0x754d60b8, "span": 0xf1222157, "reducer": 8, "reduced_span": 168} cp1251 + +""" + +if __name__ == "__main__": + g = genPerfectHashtable(powCfg, rawHashTable, o2b) + + print(genCode(g, GenCfg( + valueASTGen=valueASTGen, + preamble = [ + ast.ImportFrom(module='commands', names=[ast.alias(name='*')], level=1), + ast.ImportFrom(module='enum', names=[ast.alias(name='Enum')], level=0), + ], + o2b = fromThisFile["o2b"] + ))) diff --git a/monotonic_ns_timings.py b/monotonic_ns_timings.py new file mode 100644 index 0000000..185f602 --- /dev/null +++ b/monotonic_ns_timings.py @@ -0,0 +1,19 @@ +# this is an approximation of joint distribution of times `time.monotonic_ns` call takes. +# Method of computation: +# 0. create an accumulator array. index is time, value by the index is frequency +# 1. call `time.monotonic_ns` multiple times +# 2. compute deltas +# 3. it turns out (after the experiment with averaging one call) the distribution is multimodal, a mixture of multiple distributions. So check if all the deltas belong to the same subdistribution by comparing them to local mimima of PDFs, separating the unimodal subdistributions. If the values belong to different distributions, do nothing, try again. +# 4. use arythm average of the deltas as the key: the last time minus the first time, divided by count of deltas. Use only odd number of deltas to avoid rounding errors. +# 5. when we increase count of consecutive evaluations of `time.monotonic_ns`, the resulting distribution tends to shift into the area of lower times. It seems the overhead becomes lower. Since Python version used was not JITted ... IDK origin of this. + +import numpy +import scipy.stats + +d1 = scipy.stats.norm(loc=100.88800333333336, scale=1.186251833851963) +d2 = scipy.stats.norm(loc=129.43312242240475, scale=2.4933842584502934) +d3 = scipy.stats.norm(loc=142.0452, scale=4.552126641472099) +weights = np.array((0.2761394101876676, 0.4557640750670241, 0.2680965147453083)) +def jointPDF(x): + return d1.pdf(x) * weights[0] + d2.pdf(x) * weights[1] + d3.pdf(x) * weights[2] + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d1b1a61 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +requires = ["setuptools>=61.2.0", "wheel", "setuptools_scm[toml]>=3.4.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "SMSD" +readme = "ReadMe.md" +description = "A library implementing protocols for Smart Motor Devices SMSD controlling drivers for stepper motors." +authors = [{name = "KOLANICH"}] +license = {text = "Unlicense"} +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Other Environment", + "Intended Audience :: Developers", + "License :: Public Domain", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", +] +keywords = ["SMSD"] +urls = {Homepage = "https://github.com/KOLANICH-libs/SMSD.py"} +requires-python = ">=3.4" +dependencies = [ + "motorAccelerationPlanner", # git+https://github.com/KOLANICH-physics/motorAccelerationPlanner.py.git + "SaneIO", # @ git+https://github.com/KOLANICH-libs/SaneIO.py.git +] +dynamic = ["version"] + +[project.scripts] +SMSD = "SMSD.__main__:CLI.run" + +[project.optional-dependencies] +uart = [ + "serial_asyncio", # git+https://github.com/pyserial/pyserial-asyncio.git +] +lan = [ + "kaitaistruct", # git+https://github.com/kaitai-io/kaitai_struct_python_runtime.git +] + +[tool.setuptools] +zip-safe = true + +[tool.setuptools.packages.find] +include = ["SMSD", "SMSD.*"] + +[tool.setuptools_scm] + +[tool.kaitai.repos."local"."smsd"] +update = false +localPath = "specs" +outputDir = "SMSD/kaitai" + +[tool.kaitai.repos."local"."smsd".formats.smsd_lan] +flags = { autoRead = false } +postprocess = {fixEnums = []} +path = "smsd_lan" + +[tool.kaitai.repos."local"."smsd".formats.smsd_modbus] +postprocess = {fixEnums = []} +path = "smsd_modbus" diff --git a/smsd.cpp b/smsd.cpp new file mode 100644 index 0000000..1c7abf2 --- /dev/null +++ b/smsd.cpp @@ -0,0 +1,19 @@ +#include "../ctre.hpp" +#include + +auto matcher = ctre::match<"^(LD)(\\d+)$">; + +void parseCommand(std::string cmdStr){ + auto [whole, command, num] = matcher(cmdStr); + /*Serial.print("whole "); + Serial.println(whole.data()); + Serial.print("command "); + Serial.println(command.data()); + Serial.print("num "); + Serial.println(num.data());*/ + std::cout << "whole " << whole << " command " << command << " num " << num.data() << std::endl; +} + +int main(){ + parseCommand("LD1"); +} diff --git a/smsd.yug b/smsd.yug new file mode 100644 index 0000000..d69cbf5 --- /dev/null +++ b/smsd.yug @@ -0,0 +1,92 @@ +meta: + id: smsd + title: SMSD command + license: Unlicense + class: regular +doc: A grammar for SMSD text serial commands +tests: + - model: lines + files: + - "tests.txt" + +chars: + - id: digit + wellknown: digits + - id: plus + lit: + + - id: minus + lit: '-' + - id: sign + alt: + - ref: plus + - ref: minus +keywords: + - id: optional_arg_verb + alt: + - lit: LD + - lit: MV + - lit: ST + - lit: PS + - id: no_arg_verb + alt: + - lit: BG + - lit: EN + - lit: DL + - lit: DR + - lit: RS + - lit: EN + - lit: DS + - lit: ON + - lit: OF + - lit: SF + - lit: WL + - lit: WH + - lit: CF + - lit: LL + - lit: ED + - id: unsigned_arg_verb + alt: + - lit: SD + - lit: SS + - lit: SP + - lit: JP + - id: signed_arg_verb + lit: AL +tokens: + - id: unsigned_number + ref: digit + min: 1 + - id: signed_number + seq: + - opt: + ref: sign + - ref: unsigned_number + +prods: + - id: command + alt: + - ref: opt_arg_command + - ref: unsigned_command + - ref: signed_command + + - id: signed_command + seq: + - ref: signed_arg_verb + cap: verb + - ref: signed_number + cap: arg + - id: unsigned_command + seq: + - ref: unsigned_arg_verb + cap: verb + - ref: unsigned_number + cap: arg + - id: opt_arg + opt: + ref: unsigned_number + - id: opt_arg_command + seq: + - ref: optional_arg_verb + cap: verb + - ref: opt_arg + cap: arg diff --git a/specs/checksum_simple_additive_u1.ksy b/specs/checksum_simple_additive_u1.ksy new file mode 100644 index 0000000..a5d2d16 --- /dev/null +++ b/specs/checksum_simple_additive_u1.ksy @@ -0,0 +1,39 @@ +meta: + id: checksum_simple_additive_u1 + license: Unlicense + title: Computes a simple additive checksum + endian: le +doc: | + assert calcChecksum(0xFF, b"abcde") == 238 + assert calcChecksum(0xFF, b"abcdef") == 84 + assert calcChecksum(0xFF, b"abcdefgh") == 35 + assert calcChecksum(0xFF, bytes(range(256))) == 127 + + assert calcChecksum(0, b"abcde") == 239 + assert calcChecksum(0, b"abcdef") == 85 + assert calcChecksum(0, b"abcdefgh") == 36 + assert calcChecksum(0, bytes(range(256))) == 128 +params: + - id: initial + type: u1 + - id: data + type: bytes +instances: + reduction: + pos: 0 + size: 0 + type: iteration(_index) + repeat: expr + repeat-expr: data.size + value: + value: reduction[data.size - 1].res & 0xFF +types: + iteration: + params: + - id: idx + type: u4 + instances: + prev: + value: "idx == 0 ? _root.initial : (_parent.reduction[idx - 1].as.res).as" + res: + value: prev + _parent.data[idx] diff --git a/specs/smsd_lan.ksy b/specs/smsd_lan.ksy new file mode 100644 index 0000000..02ad01e --- /dev/null +++ b/specs/smsd_lan.ksy @@ -0,0 +1,1177 @@ +meta: + id: smsd_lan + title: Smart Motor Devices SMSD-4.2LAN and SMSD-8.0LAN protocol v04 + endian: le + bit-endian: le + imports: + - ./smsd_limits + - ./checksum_simple_additive_u1 + +doc-ref: + - https://smd.ee/manuals/smsd-lan-communication-protocol.pdf + - https://electroprivod.ru/pdf/smsd-4.2lan-communication-protocol.pdf + +doc: | + It is possible to connect to devices either via TCP or via a virtual COM port through USB. + It is required to transfer data as whole information packets, every packet conforms the structure, described in this manual. Every packet contains only one data transmission command. It is not possible to transfer more than one data transmission command inside one information packet. Every information packet should be continuously transferred, without interruptions. After receiving an information packet, the controller handles it and sends a response, the response is sent the same physical line as the command was received. A sequence of bytes in the information packets is inverted – “little-endian”, (Intel). + These parameters can be changed afterwards by commands sent through a USB or Ethernet connection. + RS-232 parameters (USB connection): + · Baud rate - 115200 + · Data bits - 8 + · Parity – none + · Stop bits – 1 + +-orig-id: LAN_COMMAND_Type + +params: + - id: limits + type: smsd_limits + +seq: + - id: header + type: header + - id: check_checksum + size: 0 + valid: + expr: _.size == 0 and recomputed_checksum.value == header.checksum + - id: data + -orig-id: DATA + size: header.len + type: + switch-on: header.type + cases: + type::response: response + #type::password: bytes + #type::password_set: bytes + type::power_step: powerstep_command + type::powerstem_write_mem_0: powerstep_commands + type::powerstem_write_mem_1: powerstep_commands + type::powerstem_write_mem_2: powerstep_commands + type::powerstem_write_mem_3: powerstep_commands + type::powerstem_read_mem_0: empty + type::powerstem_read_mem_1: empty + type::powerstem_read_mem_2: empty + type::powerstem_read_mem_3: empty + type::network_config_set: network_config + type::network_config_get: empty + type::error_counters: error_counters + type::version_data: version_data + if: header.len + doc: the data portion of the packet, length of the data portion is LENGTH_DATA bytes. +instances: + checksummed_bytes: + pos: header.checksum._sizeof + size: sizeof
- header.checksum._sizeof + header.len + recomputed_checksum: + pos: 0 + size: 0 + type: smsd_checksum(checksummed_bytes) + +types: + smsd_checksum: + params: + - id: data + type: bytes + seq: + - id: original_checksum + type: checksum_simple_additive_u1(0xFF, data) + instances: + value: + value: (original_checksum.value ^ 0xFF) & 0xFF + header: + seq: + - id: checksum + -orig-id: XOR_SUM + type: u1 + - id: version + -orig-id: Ver + type: u1 + doc: | + communication protocol version. + The current version of the data communication protocol - 0x02 (applicable for controllers since 19/04/2018). + - id: type + -orig-id: CMD_TYPE + type: u1 + enum: type + doc: type of the data transmission command + - id: id + -orig-id: CMD_IDENTIFICATION + type: u1 + doc: unique identifier of the data transfer packet. The same identifier is sent inside the response information packet from the controller. The identifier uniquely associates a transferred command and received response. + - id: len + -orig-id: LENGTH_DATA + type: u2 + valid: + max: 1024 + empty: + seq: + - id: must_have_zero_size + -affected-by: | + self._unnamed0 = self._io.read_bytes_full() + _ = self.unnamed_0 + size-eos: true + valid: + expr: _.size == 0 + + instruction_pointer: + params: + - id: raw + type: u2 + instances: + program: + value: (raw >> 8) & 0b11 + command: + value: raw & 0xFF + + event_mask_from_int: + params: + - id: raw + type: u2 + instances: + is_enabled: + pos: 0 + size: 0 + type: bit(_index, raw) + #value: (raw >> _index) & 1 == 1 + repeat: expr + repeat-expr: 8 + types: + bit: + params: + - id: idx + type: u1 + - id: raw + type: u1 + instances: + value: + value: (raw >> idx) & 1 == 1 + + speed_verify_range: + params: + - id: speed + type: u2 + doc: always set as full steps per second! + -unit: full step / second + - id: min_allowed + type: u2 + -unit: full step / second + - id: max_allowed + type: u2 + -unit: full step / second + seq: + - id: hack + size: 0 + valid: + expr: min_allowed <= speed and speed <= max_allowed and _.size == 0 + + response: + -orig-id: COMMANDS_RETURN_DATA_Type + #to-string: | + # "PowerStep.Response(" + + # powerstep_status.to_s + ", " + + # code.to_s + ", " + + # ")" + seq: + - id: powerstep_status + -orig-id: STATUS_POWERSTEP01 + type: powerstep_status + - id: code + -orig-id: ERROR_OR_COMMAND + type: u1 + enum: code + - id: return_data + -orig-id: RETURN_DATA + size: 4 + type: + switch-on: code + cases: + code::position_microstepping_electrical: current_electrical_step_microstep + code::events_status: events_status + + code::speed_min: decode_from_int(code) + code::speed_max: decode_from_int(code) + code::speed_current: decode_from_int(code) + code::instruction_pointer: decode_from_int(code) + + enums: + code: + 0: + id: success + -orig-id: OK + doc: command accepted without errors + 1: + id: auth_success + -orig-id: OK_ACCESS + doc: correct password + 2: + id: auth_failure + -orig-id: ERROR_ACCESS + doc: incorrect password. You can retry in 1 second. + 3: + id: rate_limited + -orig-id: ERROR_ACCESS_TIMEOUT + doc: to deter bruteforce + 4: + id: wrong_checksum + -orig-id: ERROR_XOR + 5: + id: wrong_command + -orig-id: ERROR_NO_COMMAND + doc: the command does not exist + 6: + id: wrong_length + -orig-id: ERROR_LEN + doc: the packet length error + 7: + id: out_of_range + -orig-id: ERROR_RANGE + doc: exceeding values limits + 8: + id: fail_write + -orig-id: ERROR_WRITE + doc: writing error + 9: + id: fail_read + -orig-id: ERROR_READ + doc: reading error + 10: + id: fail_program + -orig-id: ERROR_PROGRAMS + doc: program error + 11: + id: fail_write_setup + -orig-id: ERROR_WRITE_SETUP + 12: + id: no_next + -orig-id: NO_NEXT + doc: no next command + 13: + id: end_programs + -orig-id: END_PROGRAMS + doc: end of program + 14: + id: events_status + -orig-id: COMMAND_GET_STATUS_IN_EVENT + -response-to: CMD_PowerSTEP01_STATUS_IN_EVENT + doc: the field RETURN_DATA contains the bit map of input signals + 15: + id: mode + -orig-id: COMMAND_GET_MODE + -response-to: CMD_PowerSTEP01_GET_MODE + doc: the field RETURN_DATA contains the bit map of the Controller parameters + 16: + id: position_absolute + -orig-id: COMMAND_GET_ABS_POS + -response-to: CMD_PowerSTEP01_GET_ABS_POS + doc: the field RETURN_DATA contains the current position of the stepper motor (measured as steps) + 17: + id: position_microstepping_electrical + -orig-id: COMMAND_GET_EL_POS + -response-to: CMD_PowerSTEP01_GET_EL_POS + doc: the field RETURN_DATA contains the current electrical position of the rotor + 18: + id: speed_current + -orig-id: COMMAND_GET_SPEED + -response-to: CMD_PowerSTEP01_GET_SPEED + doc: the field RETURN_DATA contains the current motor speed + 19: + id: speed_min + -orig-id: COMMAND_GET_MIN_SPEED + -response-to: CMD_PowerSTEP01_GET_MIN_SPEED + doc: the field RETURN_DATA contains the current set minimum motor speed + 20: + id: speed_max + -orig-id: COMMAND_GET_MAX_SPEED + -response-to: CMD_PowerSTEP01_GET_MAX_SPEED + doc: the field RETURN_DATA contains the current set maximum motor speed + 21: + id: instruction_pointer + -orig-id: COMMAND_GET_STACK + -response-to: CMD_PowerSTEP01_GET_STACK + doc: the field RETURN_DATA contains information about executing program number and command number + 22: + id: relay_set + -orig-id: STATUS_RELE_SET + -response-to: + - CMD_PowerSTEP01_GET_RELE + - CMD_PowerSTEP01_SET_RELE + doc: relay is turned ON + 23: + id: relay_clear + -orig-id: STATUS_RELE_CLR + -response-to: + - CMD_PowerSTEP01_GET_RELE + - CMD_PowerSTEP01_CLR_RELE + doc: relay is turned OFF + + types: + current_electrical_step_microstep: + seq: + - id: raw + type: u4 + instances: + full_step: + value: (raw >> 7) & 0b11 + microstep: + value: raw & 0b1111111 + -unit: 1/128 full step + doc: current microstep inside the current full step (measured as 1/128 of the full step) + + events_status: + seq: + - id: has_happenned + type: u1 + - id: is_masking_raw + type: u1 + - id: is_waiting + type: u1 + instances: + is_masking: + pos: 0 + size: 0 + type: event_mask_from_int(is_masking_raw) + + decode_from_int: + params: + - id: code + type: u1 + enum: code + seq: + - id: raw + type: u4 + instances: + value: + pos: 0 + size: 0 + type: + switch-on: code + cases: + code::speed_min: speed_verify_range(raw, 0, _root.limits.speed_min_max_limit) + code::speed_max: speed_verify_range(raw, _root.limits.speed_max_min_limit, _root.limits.speed_max_max_limit) + code::current_speed: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) # feels like bullshit + code::instruction_pointer: instruction_pointer(raw) + + powerstep_commands: + seq: + - id: commands + type: powerstep_command + repeat: eos + + network_config: + -orig-id: SMSD_LAN_Config_Type + seq: + - id: mac + type: mac_addr + - id: my_ip + type: ipv4 + - id: subnet_mask + type: ipv4 + doc: Why not prefix length instead? + - id: gateway + type: ipv4 + - id: dns + type: ipv4 + - id: port + type: u2 + - id: dhcp_mode + type: u1 + types: + mac_addr: + seq: + - id: mac + size: 6 + ipv4: + seq: + - id: ipv4 + size: 4 + + powerstep_status: + -orig-id: powerSTEP_STATUS_TypeDef + to-string: | #↺↻ ↠↞⇔⏯⏸⏴ + "PowerStepStatus<" + + (is_deenergized ? "Z": "⚡") + ", " + + (is_ready ? "💤": "⏳") + ", " + + (is_sw_on ? "SW on, ": "") + + (has_sw_event_happenned ? "🔊": "🔇") + ", " + + (is_rotating_direction_forward ? "→": "←") + ", " + + (acceleration_status == acceleration_status::stop?"⏹":(acceleration_status == acceleration_status::accelerates?"⏩":(acceleration_status == acceleration_status::decelerates?"⏪":"⏵"))) + ", " + + (is_command_error ? "🗙": "🗸") + ", " + + (reserved != 0x00 ? reserved.to_s : "") + + + ">" + seq: + - id: is_deenergized + -orig-id: HiZ 0x01 + type: b1 + doc: whether phases are in Z (disconnected, high-impedance) state + - id: is_ready + -orig-id: BUSY 0x02 + type: b1 + - id: is_sw_on + -orig-id: SW_F 0x04 + type: b1 + - id: has_sw_event_happenned + -orig-id: SW_EVN 0x08 + type: b1 + - id: is_rotating_direction_forward + -orig-id: DIR 0x10 + type: b1 + doc: rotation direction + - id: acceleration_status + -orig-id: MOT_STATUS 0x20 0x40 + type: b2 + enum: acceleration_status + doc: motor running state + - id: is_command_error + -orig-id: CMD_ERROR 0x80 + type: b1 + doc: command executing error + - id: reserved + -orig-id: RESERVE 0xFF + type: u1 + enums: + acceleration_status: + 0: stop + 1: accelerates + 2: decelerates + 3: constant_speed + + motor_mode: + to-string: | + "MotorMode<" + + (is_in_current_mode?"A":"V") + ", " + + "model=" + motor_model.to_i.to_s + ", " + + "1/" + microstepping_denominator.to_s + ", " + + (work_current_raw / 10).to_s + " A, " + + (hold_current_percent).to_s + "%, " + + program_n.to_s + ", " + + (reserved != 0?reserved.to_s:"") + + ">" + params: + - id: raw + type: u2 + seq: + - id: hack + size: 0 + valid: + expr: | + 1 <= work_current_raw + and + work_current_raw <= 80 + and + _.size == 0 + instances: + reserved: + value: raw >> 21 + program_n: + -orig-id: PROGRAM_N + value: (raw >> 19) & 0b11 + hold_current_raw: + -orig-id: STOP_CURRENT + value: (raw >> 17) & 0b11 + hold_current_percent: + value: (hold_current_raw + 1) * 25 + hold_current: + value: hold_current_percent / 100. + doc: share of an operating current + work_current_raw: + -orig-id: WORK_CURRENT + value: (raw >> 10) & 0b1111111 + work_current: + value: work_current_raw / 10. + -unit: Ampere + doc: operating current for the current control mode. + microstepping_nlog: + -orig-id: MICROSTEPPING + value: (raw >> 7) & 0b111 + microstepping_denominator: + value: 1 << microstepping_nlog + motor_model: + -orig-id: MOTOR_TYPE + value: (raw >> 1) & 0b111111 + enum: motor_model + doc: motor type for the voltage control mode. See the table in the docs for the limitation values + is_in_current_mode: + -orig-id: CURRENT_OR_VOLTAGE + value: raw & 0b1 == 1 + doc: | + motor control mode + false: voltage + true: current + enums: + motor_model: + 7: sm4247 + 25: sm5776 + 33: sm8680_parallel + 34: sm8680_serial + 43: sm110201 + + powerstep_command: + -orig-id: SMSD_CMD_Type + doc: | + The structure SMSD_CMD_Type is used in data transmission packets. + seq: + - id: raw + type: u4 + instances: + reserved: + -orig-id: RESERVE + value: raw & 0b111 + action: + -orig-id: ACTION + value: (raw >> 3) & 0b1 == 1 + doc: for internal use, send as 0 + operation: + -orig-id: COMMAND + value: (raw >> 4) & 0b111111 + doc: the executing command code + enum: opcode + argument_raw: + -orig-id: DATA + value: raw >> 10 + doc: Zero if not needed. + argument: + # hack + pos: 0 + size: 0 + type: + switch-on: operation + cases: + opcode::move_to_recorded_zero: zero_int(argument_raw) + opcode::move_to_recorded_label: zero_int(argument_raw) + opcode::zero_set: zero_int(argument_raw) + opcode::reset_powerstep01: zero_int(argument_raw) + opcode::stop_soft_hold: zero_int(argument_raw) + opcode::stop_hard_hold: zero_int(argument_raw) + opcode::stop_soft_deenergize: zero_int(argument_raw) + opcode::stop_hard_deenergize: zero_int(argument_raw) + opcode::relay_on: zero_int(argument_raw) + opcode::relay_off: zero_int(argument_raw) + opcode::relay_get: zero_int(argument_raw) + opcode::ret: zero_int(argument_raw) + opcode::program_start_mem0: zero_int(argument_raw) + opcode::program_start_mem1: zero_int(argument_raw) + opcode::program_start_mem2: zero_int(argument_raw) + opcode::program_start_mem3: zero_int(argument_raw) + opcode::halt: zero_int(argument_raw) + opcode::control_mode_set_en_step_dir: zero_int(argument_raw) + opcode::usb_stop: zero_int(argument_raw) + opcode::end: zero_int(argument_raw) + opcode::wait_for_in0: zero_int(argument_raw) + opcode::wait_for_in1: zero_int(argument_raw) + opcode::wait_for_continue: zero_int(argument_raw) + opcode::status_and_clear_errors_get: zero_int(argument_raw) + + opcode::position_absolute_get: zero_int(argument_raw) # -> current_electrical_step_microstep + opcode::position_microstepping_electrical_get: zero_int(argument_raw) # -> current_electrical_step_microstep + opcode::speed_current_get: zero_int(argument_raw) # -> speed_verify_range(argument_raw, 15, 15600) + + opcode::speed_min_set: speed_verify_range(raw, 0, _root.limits.speed_min_max_limit) + opcode::speed_min_get: zero_int(argument_raw) # -> speed_verify_range(raw, 0, _root.limits.speed_min_max_limit) + + opcode::speed_max_set: speed_verify_range(raw, _root.limits.speed_max_min_limit, _root.limits.speed_max_max_limit) + opcode::speed_max_get: zero_int(argument_raw) # -> speed_verify_range(raw, _root.limits.speed_max_min_limit, _root.limits.speed_max_max_limit) + + opcode::speed_full_step_set: speed_verify_range(argument_raw, 15, 15600) + opcode::speed_forward: speed_verify_range(argument_raw, 15, 15600) + opcode::speed_reverse: speed_verify_range(argument_raw, 15, 15600) + + # the ones for which limits are NOT documented, but we assume they are the same + opcode::move_untill_zero_forward_set_zero: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) + opcode::move_untill_zero_reverse_set_zero: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) + opcode::move_untill_in1_forward_set_label: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) + opcode::move_untill_in1_reverse_set_label: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) + opcode::move_untill_in1_forward_set_mark: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) + opcode::move_untill_in1_reverse_set_mark: speed_verify_range(raw, _root.limits.speed_max_min_limit - 1, _root.limits.speed_max_max_limit) + + opcode::sleep: time_verify_range(argument_raw) + opcode::sleep_interruptible: time_verify_range(argument_raw) + + opcode::acceleration_set: acceleration_verify_range(argument_raw) + opcode::decelleration_set: acceleration_verify_range(argument_raw) + + # absolute positions + opcode::move_to_position_forward: microsteps(argument_raw) + opcode::move_to_position_reverse: microsteps(argument_raw) + opcode::move_to_position: microsteps(argument_raw) + + # relative positions + opcode::move_steps_forward: microsteps(argument_raw) + opcode::move_steps_reverse: microsteps(argument_raw) + + opcode::move_untill_sw_forward: signal_verify_range(argument_raw) + opcode::move_untill_sw_reverse: signal_verify_range(argument_raw) + + opcode::jump: instruction_pointer(argument_raw) + opcode::jump_if_in0: instruction_pointer(argument_raw) + opcode::jump_if_in1: instruction_pointer(argument_raw) + opcode::call: instruction_pointer(argument_raw) + opcode::jump_if_at_zero: instruction_pointer(argument_raw) + opcode::jump_if_zero: instruction_pointer(argument_raw) + opcode::instruction_pointer_get: zero_int(argument_raw) # -> instruction_pointer(argument_raw) + + opcode::loop: loop(argument_raw) + + opcode::motor_mode_set: motor_mode(argument_raw) + opcode::motor_mode_get: zero_int(argument_raw) # -> motor_mode + + opcode::events_status_get: zero_int(argument_raw) # -> events_status + opcode::event_mask_set: event_mask_from_int(argument_raw) + + types: + zero_int: + params: + - id: input + type: u2 + seq: + - id: hack + size: 0 + valid: + expr: input == 0 and _.size == 0 + + time_verify_range: + params: + - id: time + type: u2 + -unit: millisecond + seq: + - id: hack + size: 0 + valid: + expr: time <= 3600000 and _.size == 0 + + signal_verify_range: + params: + - id: signal + type: u2 + seq: + - id: hack + size: 0 + valid: + expr: signal <= 7 and _.size == 0 + + acceleration_verify_range: + params: + - id: acceleration + type: u2 + -unit: step/second^2 + seq: + - id: hack + size: 0 + valid: + expr: 15 <= acceleration and acceleration <= 59000 and _.size == 0 + + microsteps: + params: + - id: raw + type: u2 + instances: + weird: + value: 1 << 21 + modulus_mask: + value: weird - 1 + microsteps: + value: 'raw >= weird ? (raw & modulus_mask) - weird : raw' + -unit: microstep + doc: The motion commands are always set as microstepping measured displacements. + + loop: + params: + - id: raw + type: u2 + instances: + cycles: + value: (raw >> 10) & ((1 << 10) - 1) + commands: + value: raw & ((1 << 10) - 1) + + enums: + opcode: + 0x00: + id: end + -orig-id: CMD_PowerSTEP01_END + doc: marks the end of executing program + 0x01: + id: speed_current_get + -orig-id: CMD_PowerSTEP01_GET_SPEED + doc: | + reads the current motor speed. + The important notice: for the correct response the minimum speed should be set to 0 by command `speed_min_set` before sending this command. Otherwise the result could be wrong for low speed movement and stops" + 0x02: + id: events_status_get + -orig-id: CMD_PowerSTEP01_STATUS_IN_EVENT + doc: | + reads information about current signals inputs state: + * whether events happenned, + * if they are masked + * if we are waiting for them + 0x03: + id: motor_mode_set + -orig-id: CMD_PowerSTEP01_SET_MODE + doc: | + sets motor and control parameters: + * current or voltage + * motor model (determines motor analog parameters: max. current per phase, resistance per phase, inductance per phase, Step angle) + * microstepping mode + * operating current + * holding current + 0x04: + id: motor_mode_get + -orig-id: CMD_PowerSTEP01_GET_MODE + doc: | + reads motor control parameters (those ones that are in `motor_mode_set` + number of program, which is available to be started by external signals) + 0x05: + id: speed_min_set + -orig-id: CMD_PowerSTEP01_SET_MIN_SPEED + doc: sets the motor minimum speed in full steps per second. + 0x06: + id: speed_max_set + -orig-id: CMD_PowerSTEP01_SET_MAX_SPEED + doc: | + sets the motor maximum speed in full steps per second. + 0x07: + id: acceleration_set + -orig-id: CMD_PowerSTEP01_SET_ACC + doc: sets the motor acceleration in full steps/sec^2. + 0x08: + id: decelleration_set + -orig-id: CMD_PowerSTEP01_SET_DEC + doc: sets the motor deceleration in full steps/sec^2. + + 0x09: + id: speed_full_step_set + -orig-id: CMD_PowerSTEP01_SET_FS_SPEED + doc: | + sets the running speed in full steps per second, when the motor switches to a full step mode. + 0x0A: + id: event_mask_set + -orig-id: CMD_PowerSTEP01_SET_MASK_EVENT + doc: masks input signals. Mask is ANDed to the signals. + 0x0B: + id: position_absolute_get + -orig-id: CMD_PowerSTEP01_GET_ABS_POS + doc: reads the current motor position + 0x0C: + id: position_microstepping_electrical_get + -orig-id: CMD_PowerSTEP01_GET_EL_POS + doc: reads the current motor electrical microstepping position + 0x0D: + id: status_and_clear_errors_get + -orig-id: CMD_PowerSTEP01_GET_STATUS_AND_CLR + doc: reads the current state of the controller, and clears all error flags + 0x0E: + id: speed_forward + -orig-id: CMD_PowerSTEP01_RUN_F + doc: starts motor rotation in forward direction at designated speed + 0x0F: + id: speed_reverse + -orig-id: CMD_PowerSTEP01_RUN_R + doc: starts motor rotation in backward direction at designated speed. + 0x10: + id: move_steps_forward + -orig-id: CMD_PowerSTEP01_MOVE_F + doc: | + starts motor rotation in forward direction for specified displacement, accelerating and decelerating. The motor must be stopped before executing this command (field Mot_Status of the powerSTEP_STATUS_Type structure = 0). + 0x11: + id: move_steps_reverse + -orig-id: CMD_PowerSTEP01_MOVE_R + doc: | + starts motor rotation in backward direction for specified displacement, accelerating and decelerating. The motor must be stopped before executing this command (field Mot_Status of the powerSTEP_STATUS_Type structure = 0). + + # WTF?! Why do we have 2 commands to move to position? + 0x12: + id: move_to_position_forward + -orig-id: CMD_PowerSTEP01_GO_TO_F + doc: | + starts motor rotation in forward direction for specified position, accelerating and decelerating. The DATA field should contain the position value. + . + 0x13: + id: move_to_position_reverse + -orig-id: CMD_PowerSTEP01_GO_TO_R + doc: | + starts motor rotation in backward direction for specified position, accelerating and decelerating. The DATA field should contain the position value. + + 0x14: + id: move_untill_sw_forward + -orig-id: CMD_PowerSTEP01_GO_UNTIL_F + doc: starts motor rotation in forward direction at the maximum speed until receiving a signal at the input SW (taking into account the signal masking, CMD_PowerSTEP01_SET_MASK_EVENT). After that the motor decelerates and stops. + 0x15: + id: move_untill_sw_reverse + -orig-id: CMD_PowerSTEP01_GO_UNTIL_R + doc: starts motor rotation in backward direction at the maximum speed until receiving a signal at the input SW (taking into account the signal masking, CMD_PowerSTEP01_SET_MASK_EVENT). After that the motor decelerates and stops. + + 0x16: + id: move_untill_zero_forward_set_zero + -orig-id: CMD_PowerSTEP01_SCAN_ZERO_F + doc: starts motor rotation in forward direction at the set speed in full steps per second until receiving a signal at the input SET_ZERO and remembers that position as ZERO. + 0x17: + id: move_untill_zero_reverse_set_zero + -orig-id: CMD_PowerSTEP01_SCAN_ZERO_R + doc: starts motor rotation in forward direction at the set speed in full steps per second until receiving a signal at the input SET_ZERO and remembers that position as ZERO. + + 0x18: + id: move_untill_in1_forward_set_label + -orig-id: CMD_PowerSTEP01_SCAN_LABEL_F + doc: | + starts motor rotation in forward direction at the set speed in full steps per second until receiving a signal at the input IN1 and remembers that position as LABEL. + + 0x19: + id: move_untill_in1_reverse_set_label + -orig-id: CMD_PowerSTEP01_SCAN_LABEL_R + doc: | + starts motor rotation in backward direction at the set speed in full steps per second until receiving a signal at the input IN1 and remembers that position as LABEL. + + 0x1A: + id: move_to_recorded_zero + -orig-id: CMD_PowerSTEP01_GO_ZERO + doc: movement to the ZERO position (remembered using move_untill_zero_*_set_zero) + 0x1B: + id: move_to_recorded_label + -orig-id: CMD_PowerSTEP01_GO_LABEL + doc: movement to the LABEL position (remembered using move_untill_in1_*_set_label) + + 0x1C: + id: move_to_position + -orig-id: CMD_PowerSTEP01_GO_TO + doc: shortest movement to the specified position + + 0x1D: + id: zero_set + -orig-id: CMD_PowerSTEP01_RESET_POS + doc: sets ZERO position (to clear internal steps counter and specify a current position as a ZERO position) + 0x1E: + id: reset_powerstep01 + -orig-id: CMD_PowerSTEP01_RESET_POWERSTEP01 + doc: hardware and software reset of the PowerSTEP01 stepper motor control module, but not of the whole Controller. + 0x1F: + id: stop_soft_hold + -orig-id: CMD_PowerSTEP01_SOFT_STOP + doc: smooth decelerating of the stepper motor and stop. After that the motor holds the current position (with preset holding current). + 0x20: + id: stop_hard_hold + -orig-id: CMD_PowerSTEP01_HARD_STOP + doc: sudden stop of the stepper motor and holding the current position (with preset holding current). + 0x21: + id: stop_soft_deenergize + -orig-id: CMD_PowerSTEP01_SOFT_HI_Z + doc: smooth decelerating of the stepper motor and stop. After that the motor phases are deenergized + 0x22: + id: stop_hard_deenergize + -orig-id: CMD_PowerSTEP01_HARD_HI_Z + doc: sudden stop of the stepper motor and deenergizing the stepper motor + + 0x23: + id: sleep + -orig-id: CMD_PowerSTEP01_SET_WAIT + doc: sets pause. The DATA field contains the waiting time measured as ms. Allowed value range 0 – 3600000 ms + + 0x24: + id: relay_on + -orig-id: CMD_PowerSTEP01_SET_RELE + doc: turn on the controller relay + 0x25: + id: relay_off + -orig-id: CMD_PowerSTEP01_CLR_RELE + doc: turn off the controller relay + 0x26: + id: relay_get + -orig-id: CMD_PowerSTEP01_GET_RELE + doc: read a current state of the controller relay + + 0x27: + id: wait_for_in0 + -orig-id: CMD_PowerSTEP01_WAIT_IN0 + doc: wait until receiving a signal to the input IN0 + 0x28: + id: wait_for_in1 + -orig-id: CMD_PowerSTEP01_WAIT_IN1 + doc: wait until receiving a signal to the input IN1 + + 0x29: + id: jump + -orig-id: CMD_PowerSTEP01_GOTO_PROGRAM + doc: | + unconditionally jumps to a specified command number in a specified program number. The DATA field contains the information about a program memory number and a command sequence number: + + 0x2A: + id: jump_if_in0 + -orig-id: CMD_PowerSTEP01_GOTO_PROGRAM_IF_IN0 + doc: | + conditionally jumps to a specified command number in a specified program number if there is a signal at the input IN0. The DATA field contains the information about a program memory number and a command sequence number: + 0x2B: + id: jump_if_in1 + -orig-id: CMD_PowerSTEP01_GOTO_PROGRAM_IF_IN1 + doc: | + conditionally jumps to a specified command number in a specified program number if there is a signal at the input IN1. The DATA field contains the information about a program memory number and a command sequence number: + 0x2C: + id: loop + -orig-id: CMD_PowerSTEP01_LOOP_PROGRAM + doc: | + loop – the Controller repeats specified times specified number of commands (start from the first command after this command. The DATA field contains the information about commands number and cycles number: bits 0..9 of the DATA field contain the commands number, bits 10..19 of the DATA field contain the cycles number. + + 0x2D: + id: call + -orig-id: CMD_PowerSTEP01_CALL_PROGRAM + doc: | + calls a subprogram. The subprogram is executed until the `ret` and after that returns to the next command of the main program after `call`. + 0x2E: + id: ret + -orig-id: CMD_PowerSTEP01_RETURN_PROGRAM + doc: specifies the end of a subprogram and to return back to the main program. + + 0x2F: + id: program_start_mem0 + -orig-id: CMD_PowerSTEP01_START_PROGRAM_MEM0 + doc: &program_start_mem_doc starts executing a program from the Controller memory area. + 0x30: + id: program_start_mem1 + -orig-id: CMD_PowerSTEP01_START_PROGRAM_MEM1 + doc: *program_start_mem_doc + 0x31: + id: program_start_mem2 + -orig-id: CMD_PowerSTEP01_START_PROGRAM_MEM2 + doc: *program_start_mem_doc + 0x32: + id: program_start_mem3 + -orig-id: CMD_PowerSTEP01_START_PROGRAM_MEM3 + doc: *program_start_mem_doc + + 0x33: + id: halt + -orig-id: CMD_PowerSTEP01_STOP_PROGRAM_MEM + doc: stops executing a program + + 0x34: + id: control_mode_set_en_step_dir + -orig-id: CMD_PowerSTEP01_STEP_CLOCK + doc: changes the control mode to pulse control using external input signals EN, STEP, DIR. + 0x35: + id: usb_stop + -orig-id: CMD_PowerSTEP01_STOP_USB + doc: stops data transfer via USB interface + + 0x36: + id: speed_min_get + -orig-id: CMD_PowerSTEP01_GET_MIN_SPEED + doc: reads the current set minimum motor speed + 0x37: + id: speed_max_get + -orig-id: CMD_PowerSTEP01_GET_MAX_SPEED + doc: reads the current set maximum motor speed + 0x38: + id: instruction_pointer_get + -orig-id: CMD_PowerSTEP01_GET_STACK + doc: reads information about current executing command number and program number from the controller. + + 0x39: + id: jump_if_at_zero + -orig-id: CMD_PowerSTEP01_GOTO_PROGRAM_IF_ZERO + doc: | + conditionally jumps to a specified command number in a specified program number if the current position value is 0. The DATA field contains the information about a program memory number and a command sequence number: + 0x3A: + id: jump_if_zero + -orig-id: CMD_PowerSTEP01_GOTO_PROGRAM_IF_IN_ZERO + doc: | + conditionally jumps to a specified command number in a specified program number if there is a signal at the input SET_ZERO. The DATA field contains the information about a program memory number and a command sequence number: This command is valid for 2d version of communication protocol only. + + 0x3B: + id: wait_for_continue + -orig-id: CMD_PowerSTEP01_WAIT_CONTINUE + doc: waits for synchronization signal at the input CONTINUE, which is used for synchronization of executing programs in different controllers. This command is valid for 2d version of communication protocol only. + 0x3C: + id: sleep_interruptible + -orig-id: CMD_PowerSTEP01_SET_WAIT_2 + doc: sets a pause. The DATA field contains the waiting time measured as ms. Allowed value range 0 – 3600000 ms. Unlike with the similar command CMD_PowerSTEP01_SET_WAIT, executing of this command can be interrupted by input signals IN0, IN1 or SET_ZERO. This command is valid for 2d version of communication protocol only. + 0x3D: + id: move_untill_in1_forward_set_mark + -orig-id: CMD_PowerSTEP01_SCAN_MARK2_F + doc: | + starts motor rotation in forward direction at the set speed in full steps per second until receiving a signal at the input IN1 and remembers that position as `Mark`. The motor stops according the deceleration value. + This command is valid for 2d version of communication protocol only. + + 0x3E: + id: move_untill_in1_reverse_set_mark + -orig-id: CMD_PowerSTEP01_SCAN_MARK2_R + doc: | + starts motor rotation in backward direction at the set speed in full steps per second until receiving a signal at the input IN1 and remembers that position as `Mark`. The motor stops according the deceleration value. + This command is valid for 2d version of communication protocol only. + + error_counters: + seq: + - id: starts + -orig-id: N_STARTS + type: u4 + doc: counter of stepper motor phases energizing + - id: xt + -orig-id: ERROR_XT + type: u4 + doc: quantity of internal errors of clock enables + - id: timeouts + -orig-id: ERROR_TIME_OUT + type: u4 + doc: quantity of timeout errors of the main process executing + - id: chip_powerstep01_init + -orig-id: ERROR_INIT_POWERSTEP01 + type: u4 + doc: quantity of chip PowerSTEP01 initialization failures + - id: chip_w5500_init + -orig-id: ERROR_INIT_WIZNET + type: u4 + doc: quantity of chip W5500 initialization failures + - id: fram_init + -orig-id: ERROR_INIT_FRAM + type: u4 + doc: quantity of memory chip FRAM initialization failures + - id: lan + -orig-id: ERROR_SOCKET + type: u4 + doc: quantity of LAN connection errors + - id: fram_exchange + -orig-id: ERROR_FRAM + type: u4 + doc: quantity of errors of data exchange with the memory chip FRAM. + - id: interrupts + -orig-id: ERROR_INTERRUPT + type: u4 + doc: quantity of interrupt handling errors + - id: overcurrents + -orig-id: ERROR_EXTERN_5V + type: u4 + doc: quantity of current overloads of the internal 5VDC power source + - id: overvoltages + -orig-id: ERROR_EXTERN_VDD + type: u4 + doc: quantity of exceeding the limits of power supply voltage + - id: overheatings_chip_powerstep01 + -orig-id: ERROR_THERMAL_POWERSTEP01 + type: u4 + doc: quantity of chip PowerSTEP01 overheatings + - id: overheatings_brake + -orig-id: ERROR_THERMAL_BRAKE + type: u4 + doc: quantity of the brake resistor overheatings + - id: chip_powerstep01_command_transfer + -orig-id: ERROR_COMMAND_POWERSTEP01 + type: u4 + doc: quantity of errors during commands transfer to the chip PowerSTEP01 + - id: unkn_uvlo_powerstep + -orig-id: ERROR_UVLO_POWERSTEP01 + type: u4 + doc: for internal use + - id: unkn_stall_powerstep + -orig-id: ERROR_STALL_POWERSTEP01 + type: u4 + doc: for internal use + - id: program_errors + -orig-id: ERROR_WORK_PROGRAM + type: u4 + doc: quantity of program executing errors + + version_data: + doc: undocumented type. But in the manual for another controller something is found. + doc-ref: https://smd.ee/manuals/BLSD-20Modbus_PS.pdf 6.8. Identification registers + seq: + - id: hardware + type: version + doc: | + major: Driver type + minor: version + - id: firmware + type: version + doc: | + major: indentifier + minor: version + - id: protocol + type: u1 + types: + version: + to-string: '"Version(" + major.to_s + ", " + minor.to_s + ")"' + seq: + - id: major + type: u2 + -orig-id: HW_MAJOR, FW_MAJOR + - id: minor + -orig-id: HW_MINOR, FW_MINOR + type: u2 + +enums: + type: + 0: + id: password + -orig-id: CODE_CMD_REQUEST + doc: | + authentication (the DATA field of the packet contains authentification information) + The data transmission command CODE_CMD_REQUEST is used for authorizing purpose. The data transfer packet with CODE_CMD_REQUEST code is sent from the controller to the user as a response to a LAN connection event (only for LAN connection, not used for USB connection). + After receiving of the packet with CODE_CMD_REQUEST command, the User should send a data transfer packet, which contains authentication password (8 bytes). + The default password is 0x01 0x23 0x45 0x67 0x89 0xAB 0xCD 0xEF. + The controller doesn’t check version of the communication protocol (field VER) in this data packet. + This password can be changed using data transmission command CODE_CMD_PASSWORD_SET. + The controller checks received password and sends a response, which contains a result. CMD_TYPE of the response is CODE_CMD_RESPONSE, the data field of the response contains COMMANDS_RETURN_DATA structure. Please, learn the COMMANDS_RETURN_DATA structure below in this manual. + 1: + id: response + -orig-id: CODE_CMD_RESPONSE + doc: | + confirmation (the entry of the DATA field depends on a sent data transmission command) + The data transfer packet with CODE_CMD_RESPONSE code is sent from the controller to the user as a response to some data transmission commands - CODE_CMD_POWERSTEP01, CODE_CMD_CONFIG_SET, CODE_CMD_ID_SET, CODE_CMD_POWERSTEP01_W_MEM, and in case of errors occur. The data field of the packet contains COMMANDS_RETURN_DATA structure. Please, learn the COMMANDS_RETURN_DATA structure below in this manual. + 2: + id: power_step + -orig-id: CODE_CMD_POWERSTEP01 + doc: motor control (the DATA field of the packet contains POWERSTEP01 commands - SMSD_CMD_Type type) + 3: + id: powerstem_write_mem_0 + -orig-id: CODE_CMD_POWERSTEP01_W_MEM0 + doc: writing of an executing program into the controller memory + 4: + id: powerstem_write_mem_1 + -orig-id: CODE_CMD_POWERSTEP01_W_MEM1 + doc: writing of an executing program into the controller memory + 5: + id: powerstem_write_mem_2 + -orig-id: CODE_CMD_POWERSTEP01_W_MEM2 + doc: writing of an executing program into the controller memory + 6: + id: powerstem_write_mem_3 + -orig-id: CODE_CMD_POWERSTEP01_W_MEM3 + doc: writing of an executing program into the controller memory + 7: + id: powerstem_read_mem_0 + -orig-id: CODE_CMD_POWERSTEP01_R_MEM0 + doc: reading of an executing program from the controller memory + 8: + id: powerstem_read_mem_1 + -orig-id: CODE_CMD_POWERSTEP01_R_MEM1 + doc: reading of an executing program from the controller memory + 9: + id: powerstem_read_mem_2 + -orig-id: CODE_CMD_POWERSTEP01_R_MEM2 + doc: reading o an executing program from the controller memory + 10: + id: powerstem_read_mem_3 + -orig-id: CODE_CMD_POWERSTEP01_R_MEM3 + doc: reading of an executing program from the controller memory + + 11: + id: network_config_set + -orig-id: CODE_CMD_CONFIG_SET + doc: writing of LAN parameters + 12: + id: network_config_get + -orig-id: CODE_CMD_CONFIG_GET + doc: reading of LAN parameters + 13: + id: password_set + -orig-id: CODE_CMD_PASSWORD_SET + doc: changing of authentication password + 14: + id: error_counters + -orig-id: CODE_CMD_ERROR_GET + doc: reading of information about number of operation mode starts and error statistics. + + 15: + id: unknown_15 + doc: if 16 exists, it is likely 15 exists too + + 16: + id: version_data + doc: | + Sent by the SMC Program LAN 7.0.7 after authentication + Seems to be we must respond with version_data diff --git a/specs/smsd_limits.ksy b/specs/smsd_limits.ksy new file mode 100644 index 0000000..ebc66f3 --- /dev/null +++ b/specs/smsd_limits.ksy @@ -0,0 +1,13 @@ +meta: + id: smsd_limits + title: Limits imposed by SMSD LAN protocol specs on the values of quantities and variables + +instances: + speed_max_max_limit: + value: 15600 + speed_max_min_limit: + value: 16 + speed_min_max_limit: + value: 950 + speed_movement_min_limit: + value: speed_max_min_limit - 1 diff --git a/specs/smsd_modbus.ksy b/specs/smsd_modbus.ksy new file mode 100644 index 0000000..c296009 --- /dev/null +++ b/specs/smsd_modbus.ksy @@ -0,0 +1,215 @@ +meta: + id: smsd_modbus + title: Modbus instruction for Smart Motor Devices BLSD-20Modbus controllers + endian: le + bit-endian: le + +doc-ref: + - https://smd.ee/manuals/BLSD-20Modbus_PS.pdf + - https://electroprivod.ru/pdf/drivers/BLSD-20Modbus_PS_2020_10_08.pdf + +types: + # the real layout is not clear for me from the docs + instruction: + seq: + - id: raw0 + type: u1 + - id: raw1 + type: u1 + - id: data + -orig-id: uData + size: 2 + type: + switch-on: command + cases: + command::register_system_set: register_system + command::register_modbus_write: register_modbus + command::delay: delay + command::jmp: jump(is_relative) + command::jeq: jump(is_relative) + command::jneq: jump(is_relative) + command::jgt: jump(is_relative) + command::jlt: jump(is_relative) + command::call: jump(false) + command::loop: loop + + instances: + command: + -orig-id: uCmd + value: raw0 & 0b1111111 + enum: command + is_relative: + -orig-id: bTypeJmp + value: (raw0 >> 7) & 0b1 == 1 + doc: displacement type for movement commands + register_address: + -orig-id: uAdrSysReg + value: raw1 & 0xF + doc: address of the system register AX_REG ... FX_REG with numbers 0 ... 5 + register_type: + -orig-id: uTypeModbusReg + value: (raw1 >> 4) & 0xF + doc: type of a Modbus register + enum: register_type + + enums: + register_type: + 0: discrete_inputs + 1: coils + 2: inputs + 3: holding_registers + command: + 0x00: + id: stop_program + -orig-id: CMD_STOP_PROGRAM + doc: | + stop executing of a user program, without exiting the user program execution mode. At the end of the program execution, all registers and state of the motor remain as they were before the command was executed (the motor continues to rotate if it was rotating before the command was executed). Before the next start of the program, you must send the CMD_FULL_STOP_PROGRAM command. + Important: when the execution of a user program is stopped by the command CMD_STOP_PROGRAM, the state of the motor and all registers remain the same as they were set during the program. For this reason, if the motor was rotating at the moment the CMD_STOP_PROGRAM command was executed, it will continue executing the last task, i.e. movement will continue when the program has finished. After the program has finished the drive rotation can be stopped by writing a register via Modbus (Coils 2001h or Coils 2002h). To prevent uncontrolled rotation, a motor stop command can be set before the program end command. + 0x01: + id: register_system_set + -orig-id: CMD_SET_SYSTEM_REG + doc: writing to the system register with the uAdrSysReg address, values from the uData data field. + 0x02: + id: register_modbus_write + -orig-id: CMD_WRITE_REG_MODBUS + doc: writing the contents from the uAdrSysReg system register to the ModBUS register space defined by the TypeModbusReg field and its address in the uData field. + 0x03: + id: read_reg_modbus + -orig-id: CMD_READ_REG_MODBUS + doc: reading the content from the ModBUS register space determined by the TypeModbusReg field and its address in the uData field into one of the uAdrSysReg system registers. + 0x04: + id: delay + -orig-id: CMD_DELAY + doc: pause, ms. + 0x05: + id: jmp + -orig-id: CMD_JMP + doc: jump to the address specified in the uData field. + 0x06: + id: jeq + -orig-id: CMD_JMP_AX_PARI_BX + doc: jump to the address specified in the uData field, if the value in the system register AX_REG is equal to the value in BX_REG. + 0x07: + id: jneq + -orig-id: CMD_JMP_AX_NOPARI_BX + doc: jump to the address specified in the uData field if the value in the system register AX_REG is not equal to the value in BX_REG. + 0x08: + id: jgt + -orig-id: CMD_JMP_AX_MORE_BX + doc: jump to the address specified in the uData field, if the value in the system register AX_REG is greater than the value in BX_REG. + 0x09: + id: jlt + -orig-id: CMD_JMP_AX_LESS_BX + doc: jump to the address specified in the uData field, if the value in the system register AX_REG is less than the value in BX_REG. + 0x0A: + id: call + -orig-id: CMD_CALL + doc: call a subroutine starting at the address specified in the uData field. + 0x0B: + id: ret + -orig-id: CMD_RETURN + doc: return from the subroutine. + 0x0C: + id: loop + -orig-id: CMD_FOR + doc: cyclic execution of a sequence of commands. + 0x0D: + id: full_stop_program + -orig-id: CMD_FULL_STOP_PROGRAM + doc: stopping the execution of the user program and exiting the program operation mode. At the end of the program execution, all registers and the state of the motor return to their original values, the motor stops. + types: + register_system: + seq: + - id: value + type: u2 + register_modbus: + seq: + - id: reg + type: u2 + delay: + seq: + - id: delay + type: u2 + doc: in milliseconds + -unit: ms + jump: + params: + - id: is_relative + type: bool + seq: + - id: addr + type: + switch-on: is_relative + cases: + true: relative + false: absolute + types: + relative: + seq: + - id: addr + type: s2 + valid: + min: -1024 + max: 1024 + absolute: + seq: + - id: addr + type: u2 + valid: + min: 0 + max: 1024 + loop: + seq: + - id: repeats + type: u1 + doc: The least significant byte of the uData field contains the number of repetitions. + - id: commands + type: u1 + doc: The high byte of the uData field contains the number of commands located after the CMD_FOR command that will be repeated in a cycle. + + error: + doc: | + 5023h register of modbus. + errors that occur during controller operation + seq: + - id: voltage_out_of_range + type: b1 + doc: out of the supply voltage range + - id: short_circuit_winding + type: b1 + doc: short circuit of the motor windings + - id: overheat_brake + type: b1 + doc: overheating of the brake circuit + - id: overheat_power + type: b1 + doc: overheating of the power circuit + - id: not_connected_hall + type: b1 + doc: Hall sensors connecting error + - id: emergency_stop + type: b1 + - id: overheat_mcu + type: b1 + doc: MCU overheating + - id: test_control_program + type: b1 + - id: runtime + type: b1 + doc: user program execution error + - id: io_settings + type: b1 + doc: error reading or writing settings + - id: output_analog + type: b1 + doc: error in the operation of the output transistor switches + - id: warning_breakpoint + type: b1 + doc: warning about the impossibility of calculating the breakpoint + - id: register_out_of_range + type: b1 + doc: warning about an attempt to write to the register a value that is out of range + - id: parity + type: b1 + doc: RS-485 transmitting parity error + diff --git a/tests/build.ninja b/tests/build.ninja new file mode 100755 index 0000000..97ea4d4 --- /dev/null +++ b/tests/build.ninja @@ -0,0 +1,16 @@ +#!/usr/bin/env -S ninja -f + +cpp = clang++ + +rule cpp + command = $cpp -std=gnu++2b -O3 -MT $out -MF $out.d -o $out $in && strip -s $out + description = Compile $out + depfile = $out.d + deps = gcc + +rule gen_test_data + command = ./lan_proto_test + description = Generating test files + +build ./lan_proto_test: cpp ./lan_proto_test.cpp +build testData: gen_test_data ./lan_proto_test diff --git a/tests/lan_proto_from_pdf.hpp b/tests/lan_proto_from_pdf.hpp new file mode 100644 index 0000000..9a1da7b --- /dev/null +++ b/tests/lan_proto_from_pdf.hpp @@ -0,0 +1,334 @@ +#pragma once + +#include + +constexpr uint8_t xor_sum(uint8_t *data, uint16_t length) { + uint8_t xor_temp = 0xFF; + while(length--) { + xor_temp += *data; + data++; + } + return (xor_temp ^ 0xFF); +} + + +// modbus +enum class register_type_t : uint8_t { + DISCRETE_INPUTS = 0, + COILS = 1, + INPUTS = 2, + HOLDING_REGISTERS = 3 +}; + +enum class command_t : uint8_t { + STOP_PROGRAM = 0, + REGISTER_SYSTEM_SET = 1, + REGISTER_MODBUS_WRITE = 2, + READ_REG_MODBUS = 3, + DELAY = 4, + JMP = 5, + JEQ = 6, + JNEQ = 7, + JGT = 8, + JLT = 9, + CALL = 10, + RETURN = 11, + LOOP = 12, + FULL_STOP_PROGRAM = 13 +}; + +//lan + +enum class CODE_CMD : uint8_t{ + REQUEST, + RESPONSE, + POWERSTEP01, + POWERSTEP01_W_MEM0, + POWERSTEP01_W_MEM1, + POWERSTEP01_W_MEM2, + POWERSTEP01_W_MEM3, + POWERSTEP01_R_MEM0, + POWERSTEP01_R_MEM1, + POWERSTEP01_R_MEM2, + POWERSTEP01_R_MEM3, + CONFIG_SET, + CONFIG_GET, + PASSWORD_SET, + ERROR_GET, + + // Undocumented codes: + Unknown15 = 15, + VersionData = 16, +}; + +enum class CMD_PowerSTEP01 : uint32_t{ + END = 0x00, + GET_SPEED = 0x01, + STATUS_IN_EVENT = 0x02, + SET_MODE = 0x03, + GET_MODE = 0x04, + SET_MIN_SPEED = 0x05, + SET_MAX_SPEED = 0x06, + SET_ACC = 0x07, + SET_DEC = 0x08, + SET_FS_SPEED = 0x09, + SET_MASK_EVENT = 0x0A, + GET_ABS_POS = 0x0B, + GET_EL_POS = 0x0C, + GET_STATUS_AND_CLR = 0x0D, + RUN_F = 0x0E, + RUN_R = 0x0F, + MOVE_F = 0x10, + MOVE_R = 0x11, + GO_TO_F = 0x12, + GO_TO_R = 0x13, + GO_UNTIL_F = 0x14, + GO_UNTIL_R = 0x15, + SCAN_ZERO_F = 0x16, + SCAN_ZERO_R = 0x17, + SCAN_LABEL_F = 0x18, + SCAN_LABEL_R = 0x19, + GO_ZERO = 0x1A, + GO_LABEL = 0x1B, + GO_TO = 0x1C, + RESET_POS = 0x1D, + RESET_POWERSTEP01 = 0x1E, + SOFT_STOP = 0x1F, + HARD_STOP = 0x20, + SOFT_HI_Z = 0x21, + HARD_HI_Z = 0x22, + SET_WAIT = 0x23, + SET_RELE = 0x24, + CLR_RELE = 0x25, + GET_RELE = 0x26, + WAIT_IN0 = 0x27, + WAIT_IN1 = 0x28, + GOTO_PROGRAM = 0x29, + GOTO_PROGRAM_IF_IN0 = 0x2A, + GOTO_PROGRAM_IF_IN1 = 0x2B, + LOOP_PROGRAM = 0x2C, + CALL_PROGRAM = 0x2D, + RETURN_PROGRAM = 0x2E, + START_PROGRAM_MEM0 = 0x2F, + START_PROGRAM_MEM1 = 0x30, + START_PROGRAM_MEM2 = 0x31, + START_PROGRAM_MEM3 = 0x32, + STOP_PROGRAM_MEM = 0x33, + STEP_CLOCK = 0x34, + STOP_USB = 0x35, + GET_MIN_SPEED = 0x36, + GET_MAX_SPEED = 0x37, + GET_STACK = 0x38, + GOTO_PROGRAM_IF_ZERO = 0x39, + GOTO_PROGRAM_IF_IN_ZERO = 0x3A, + WAIT_CONTINUE = 0x3B, + SET_WAIT_2 = 0x3C, + SCAN_MARK2_F = 0x3D, + SCAN_MARK2_R = 0x3E, + + UNKNOWN = 0x3F, +}; + +enum class code_t : uint8_t{ + OK, //< command accepted without errors + OK_ACCESS, //< successful authentication (the User has got access to the Controller control) + ERROR_ACCESS, //< authentication error (the User has not got access to the Controller control) + ERROR_ACCESS_TIMEOUT, //< authentication timeout is not elapsed (authentication timeout is 1 sec) + ERROR_XOR, //< checksum error + ERROR_NO_COMMAND, //< the command does not exist + ERROR_LEN, //< the packet length error + ERROR_RANGE, //< exceeding values limits + ERROR_WRITE, //< writing error + ERROR_READ, //< reading error + ERROR_PROGRAMS, //< program error + ERROR_WRITE_SETUP, + NO_NEXT, //< no next command + END_PROGRAMS, //< end of program + COMMAND_GET_STATUS_IN_EVENT, //< the field RETURN_DATA contains the bit map of input signals + COMMAND_GET_MODE, //< the field RETURN_DATA contains the bit map of the Controller parameters + COMMAND_GET_ABS_POS, //< the field RETURN_DATA contains the current position of the stepper motor (measured as steps) + COMMAND_GET_EL_POS, //< the field RETURN_DATA contains the current electrical position of the rotor + COMMAND_GET_SPEED, //< the field RETURN_DATA contains the current motor speed + COMMAND_GET_MIN_SPEED, //< the field RETURN_DATA contains the current set minimum motor speed + COMMAND_GET_MAX_SPEED, //< the field RETURN_DATA contains the current set maximum motor speed + COMMAND_GET_STACK, //< the field RETURN_DATA contains information about executing program number and command number + STATUS_RELE_SET, //< relay is turned ON + STATUS_RELE_CLR, //< relay is turned OFF +}; + +struct [[gnu::packed]] powerSTEP_STATUS_TypeDef { + bool HiZ : 1 = false; + bool BUSY : 1 = false; + bool SW_F : 1 = false; + bool SW_EVN : 1 = false; + bool DIR : 1 = false; + uint8_t MOT_STATUS : 2 = false; + bool CMD_ERROR : 1 = false; + uint8_t RESERVE : 8; +}; + +struct [[gnu::packed]] COMMANDS_RETURN_DATA_Type { + powerSTEP_STATUS_TypeDef STATUS_POWERSTEP01; + code_t ERROR_OR_COMMAND; + uint32_t RETURN_DATA = 0; +}; + +struct [[gnu::packed]] SMSD_CMD_Type { + uint8_t RESERVE : 3 = 0; + bool ACTION : 1 = 0; + CMD_PowerSTEP01 COMMAND : 6 = CMD_PowerSTEP01::UNKNOWN; + uint32_t DATA : 22 = 0; +}; + +struct [[gnu::packed]] ErrorCounters { + uint32_t + N_STARTS=0, //< counter of stepper motor phases energizing + ERROR_XT=0, //< quantity of internal errors of clock enables + ERROR_TIME_OUT=0, //< quantity of timeout errors of the main process executing + ERROR_INIT_POWERSTEP01=0, //(1), +}; + +constexpr uint8_t CURRENT_PROTOCOL_VERSION = 0x02; + +struct [[gnu::packed]] LAN_COMMAND_Type_header { + uint8_t XOR_SUM = 0; + uint8_t Ver = CURRENT_PROTOCOL_VERSION; + CODE_CMD CMD_TYPE; + uint8_t CMD_IDENTIFICATION = 0x00; + uint16_t LENGTH_DATA = 0x00; +}; + +template +struct [[gnu::packed]] LAN_COMMAND_Password{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = type, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = passwordLength, + }; + uint8_t password[passwordLength]; +}; + +template +using LAN_COMMAND_REQUEST = LAN_COMMAND_Password; + +template +using LAN_COMMAND_PASSWORD_SET = LAN_COMMAND_Password; + +struct [[gnu::packed]] LAN_COMMAND_RESPONSE{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::RESPONSE, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = sizeof(COMMANDS_RETURN_DATA_Type), + }; + COMMANDS_RETURN_DATA_Type payload; +}; + +struct [[gnu::packed]] LAN_COMMAND_POWERSTEP01{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::POWERSTEP01, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = sizeof(SMSD_CMD_Type), + }; + SMSD_CMD_Type payload; +}; + +template +struct [[gnu::packed]] LAN_COMMAND_POWERSTEP01_MEM{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = type, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = sizeof(SMSD_CMD_Type) * count_of_commands, + }; + SMSD_CMD_Type payload[count_of_commands]; +}; + +template +using LAN_COMMAND_POWERSTEP01_R_MEM = LAN_COMMAND_POWERSTEP01_MEM(static_cast(CODE_CMD::POWERSTEP01_R_MEM0) + mem)>; + +template +using LAN_COMMAND_POWERSTEP01_W_MEM = LAN_COMMAND_POWERSTEP01_MEM(static_cast(CODE_CMD::POWERSTEP01_W_MEM0) + mem)>; + +template +struct [[gnu::packed]] LAN_COMMAND_Config{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = type, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = sizeof(SMSD_LAN_Config_Type), + }; + SMSD_LAN_Config_Type payload = defaultLanConfig; +}; + +using LAN_COMMAND_CONFIG_SET = LAN_COMMAND_Config; +using LAN_COMMAND_CONFIG_GET_RESPONSE = LAN_COMMAND_Config; + +struct [[gnu::packed]] LAN_COMMAND_CONFIG_GET{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::CONFIG_GET, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = 0, + }; +}; + +struct [[gnu::packed]] LAN_COMMAND_ERROR_GET{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::ERROR_GET, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = 0, + }; +}; + +struct [[gnu::packed]] LAN_COMMAND_ERROR_GET_RESPONSE{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::ERROR_GET, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = sizeof(ErrorCounters), + }; + ErrorCounters errorCounters; +}; + diff --git a/tests/lan_proto_test.cpp b/tests/lan_proto_test.cpp new file mode 100644 index 0000000..c7d177f --- /dev/null +++ b/tests/lan_proto_test.cpp @@ -0,0 +1,413 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "lan_proto_from_pdf.hpp" +#include "lan_proto_undocumented.hpp" + + +const std::filesystem::path prefix = "./testData/"; + +template +void dumpStructures(T *b, T *e) { + std::ofstream f; + uint16_t i = 0; + for(; b < e; ++b) { + auto &el = *b; + auto parentDir = prefix / nameof::nameof_short_type(); + std::filesystem::create_directories(parentDir); + std::stringstream s; + s << i << ".dat"; + std::filesystem::path fn = s.str(); + auto fpn = parentDir / fn; + std::cout << fpn << std::endl; + f.open(fpn); + f.write((const char *) &el, sizeof(el)); + f.close(); + ++i; + } +} + +void test_powerSTEP_STATUS_TypeDef() { + powerSTEP_STATUS_TypeDef a[11]; + memset(a, 0, sizeof(a)); + + a[0].HiZ = 1; // 01 00 + a[1].BUSY = 1; // 02 00 + a[2].SW_F = 1; // 04 00 + a[3].SW_EVN = 1; // 08 00 + a[4].DIR = 1; // 10 00 + a[5].MOT_STATUS = 1; // 20 00 + a[6].MOT_STATUS = 2; // 40 00 + a[7].MOT_STATUS = 3; // 60 00 + a[8].CMD_ERROR = 1; // 80 00 + a[9].RESERVE = 1; // 00 01 + a[10].RESERVE = 0xFF;// 00 ff + + dumpStructures(std::begin(a), std::end(a)); +} + +void test_COMMANDS_RETURN_DATA_Type() { + COMMANDS_RETURN_DATA_Type a[3]; + memset(a, 0, sizeof(a)); + + a[0].STATUS_POWERSTEP01 = powerSTEP_STATUS_TypeDef { + .HiZ = 1, + .BUSY = 1, + .SW_F = 1, + .SW_EVN = 1, + .DIR = 1, + .MOT_STATUS = 2, + .CMD_ERROR = 1, + .RESERVE = 0x77, + }; // df 77 00 00 00 00 00 + a[1].ERROR_OR_COMMAND = static_cast(0xAB);// 00 00 ab 00 00 00 00 + a[2].RETURN_DATA = 0xFFFFFFFF; // 00 00 00 ff ff ff ff + + dumpStructures(std::begin(a), std::end(a)); +} + +constexpr CMD_PowerSTEP01 speedCommands[]{ + CMD_PowerSTEP01::SET_MIN_SPEED, + CMD_PowerSTEP01::SET_MAX_SPEED, + CMD_PowerSTEP01::SET_FS_SPEED, + CMD_PowerSTEP01::RUN_F, + CMD_PowerSTEP01::RUN_R, + CMD_PowerSTEP01::MOVE_F, + CMD_PowerSTEP01::MOVE_R, + CMD_PowerSTEP01::SCAN_ZERO_F, + CMD_PowerSTEP01::SCAN_ZERO_R, + CMD_PowerSTEP01::SCAN_LABEL_F, + CMD_PowerSTEP01::SCAN_LABEL_R, + CMD_PowerSTEP01::SCAN_MARK2_F, + CMD_PowerSTEP01::SCAN_MARK2_R, +}; + +constexpr CMD_PowerSTEP01 accelerationCommands[]{ + CMD_PowerSTEP01::SET_ACC, + CMD_PowerSTEP01::SET_DEC, +}; + +constexpr CMD_PowerSTEP01 waitCommands[]{ + CMD_PowerSTEP01::SET_WAIT, + CMD_PowerSTEP01::SET_WAIT_2, +}; + +constexpr CMD_PowerSTEP01 positionCommands[]{ + CMD_PowerSTEP01::GO_TO_F, + CMD_PowerSTEP01::GO_TO_R, + CMD_PowerSTEP01::GO_TO, +}; + +void testSMSD_CMD_Type() { + SMSD_CMD_Type a[14]; + memset(a, 0, sizeof(a)); + + a[0].RESERVE = 1; //01 00 00 00 + a[1].RESERVE = 2; //02 00 00 00 + a[2].RESERVE = 4; //04 00 00 00 + a[3].ACTION = 1; //08 00 00 00 + a[4].COMMAND = static_cast(1); //10 00 00 00 + a[5].COMMAND = static_cast(2); //20 00 00 00 + a[6].COMMAND = static_cast(4); //40 00 00 00 + a[7].COMMAND = static_cast(8); //80 00 00 00 + a[8].COMMAND = static_cast(16);//00 01 00 00 + a[9].COMMAND = static_cast(32);//00 02 00 00 + + a[10].COMMAND = speedCommands[1]; + a[10].DATA = 0x0000FF; //60 fc 03 00 + a[11].COMMAND = a[12].COMMAND = a[13].COMMAND = positionCommands[0]; + a[11].DATA = 0x00FF00; //20 01 fc 03 + a[12].DATA = 0x0F0000; //00 00 00 3C + a[13].DATA = 0x300000; //00 00 00 C0 + + dumpStructures(std::begin(a), std::end(a)); +} + +void testSMSD_LAN_Config_Type() { + static_assert(sizeof(SMSD_LAN_Config_Type) == 4 * 4 + 6 + 2 + 1); + SMSD_LAN_Config_Type a[1]; + memset(a, 0, sizeof(a)); + + a[0] = defaultLanConfig; + /* + 00000000 00 f8 dc 3f 00 00 c0 a8 01 02 ff ff 00 00 c0 a8 |...?............| + 00000010 01 01 00 00 00 00 88 13 01 |.........| + */ + + dumpStructures(std::begin(a), std::end(a)); +} + +uint8_t calculateChecksum(LAN_COMMAND_Type_header &h){ + return xor_sum((uint8_t*) &(h.Ver), sizeof(LAN_COMMAND_Type_header) - sizeof(h.XOR_SUM) + h.LENGTH_DATA); +} + +void recalculateChecksum(LAN_COMMAND_Type_header &h){ + h.XOR_SUM = calculateChecksum(h); +} + +void testLAN_COMMAND_Type_header() { + LAN_COMMAND_Type_header a[1]; + memset(a, 0, sizeof(a)); + a[0] = LAN_COMMAND_Type_header { + .XOR_SUM = 0xf3, + .Ver = 0x02, + .CMD_TYPE = CODE_CMD::CONFIG_GET, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = 0, + }; + recalculateChecksum(a[0]); + //f3 02 0c ff 00 00 + + dumpStructures(std::begin(a), std::end(a)); +} + + +void dumpCommands(LAN_COMMAND_Type_header** b, LAN_COMMAND_Type_header** e) { + std::ofstream f; + uint16_t i = 0; + for(; b < e; ++b) { + auto &el = **b; + recalculateChecksum(el); + + auto parentDir = prefix / "commands"; + std::filesystem::create_directories(parentDir); + std::stringstream s; + + s << i << "_" << magic_enum::enum_name(el.CMD_TYPE); + + if(el.LENGTH_DATA){ + switch(el.CMD_TYPE){ + case CODE_CMD::RESPONSE: + { + auto &r = *reinterpret_cast(&el); + s << "_" << magic_enum::enum_name(r.payload.ERROR_OR_COMMAND); + } + break; + case CODE_CMD::POWERSTEP01: + { + auto &c = *reinterpret_cast(&el); + s << "_" << magic_enum::enum_name(c.payload.COMMAND); + } + break; + case CODE_CMD::POWERSTEP01_R_MEM0: + case CODE_CMD::POWERSTEP01_W_MEM0: + case CODE_CMD::POWERSTEP01_R_MEM1: + case CODE_CMD::POWERSTEP01_W_MEM1: + case CODE_CMD::POWERSTEP01_R_MEM2: + case CODE_CMD::POWERSTEP01_W_MEM2: + case CODE_CMD::POWERSTEP01_R_MEM3: + case CODE_CMD::POWERSTEP01_W_MEM3: + { + //auto &c = *reinterpret_cast(&el); + s << "_" << el.LENGTH_DATA / sizeof(SMSD_CMD_Type); + } + break; + } + } + + s << ".dat"; + std::filesystem::path fn = s.str(); + auto fpn = parentDir / fn; + std::cout << fpn << std::endl; + f.open(fpn); + f.write((const char *) &el, sizeof(el) + el.LENGTH_DATA); + f.close(); + ++i; + } +} + +void testLAN_COMMAND_Type() { + LAN_COMMAND_REQUEST<0> noPass{}; // ff 02 00 ff 00 00 + + LAN_COMMAND_REQUEST<8> defaultPass{ + .password{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF} + }; // 37 02 00 ff 08 00 01 23 45 67 89 ab cd ef + + LAN_COMMAND_PASSWORD_SET<8> setDefaultPass{ + .password{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF} + }; // 2a 02 0d ff 08 00 01 23 45 67 89 ab cd ef + + LAN_COMMAND_RESPONSE resp{ + .payload{ + .STATUS_POWERSTEP01{ + .HiZ = 1, + .BUSY = 1, + .SW_F = 1, + .SW_EVN = 1, + .DIR = 1, + .MOT_STATUS = 2, + .CMD_ERROR = 1, + .RESERVE = 0x77, + }, + .ERROR_OR_COMMAND = static_cast(0xAB), + .RETURN_DATA = 0x0 + } + }; // f6 02 01 ff 07 00 df 77 ab 00 00 00 00 + + LAN_COMMAND_POWERSTEP01 moveFInstr{ + .payload{ + .COMMAND = CMD_PowerSTEP01::MOVE_F, + .DATA = 0x000000, + } + }; // f8 02 02 ff 04 00 00 01 00 00 + + LAN_COMMAND_POWERSTEP01 getSpeedInstr{ + .payload{ + .COMMAND = CMD_PowerSTEP01::GET_SPEED, + .DATA = 0x000000 + } + }; // ea 02 02 ff 04 00 10 fc 03 00 + + LAN_COMMAND_RESPONSE resp1{ + .payload{ + .STATUS_POWERSTEP01 = powerSTEP_STATUS_TypeDef { + .HiZ = 1, + .BUSY = 1, + .SW_F = 1, + .SW_EVN = 1, + .DIR = 1, + .MOT_STATUS = 2, + .CMD_ERROR = 1, + .RESERVE = 0x00, + }, + .ERROR_OR_COMMAND = code_t::COMMAND_GET_SPEED, + .RETURN_DATA = 0x1234, + } + }; // c0 02 01 ff 07 00 df 00 12 34 12 00 00 + + LAN_COMMAND_POWERSTEP01_W_MEM<1> writeSingle{ + .payload{ + { + .RESERVE = 1, + .ACTION = 1, + .COMMAND = static_cast(1), + .DATA = 0x0000FF, + } + }, + }; // e0 02 03 ff 04 00 19 fc 03 00 + + LAN_COMMAND_POWERSTEP01_R_MEM<0> readRequest{}; // f8 02 07 ff 00 00 + + + LAN_COMMAND_POWERSTEP01_R_MEM<1> readResponse{ + .payload{ + { + .RESERVE = 1, + .ACTION = 1, + .COMMAND = static_cast(1), + .DATA = 0x0000FF, + } + } + }; // dc 02 07 ff 04 00 19 fc 03 00 + + LAN_COMMAND_CONFIG_SET setLanConfig; + /* + 00000000 59 02 0b ff 19 00 00 f8 dc 3f 00 00 c0 a8 01 02 |Y........?......| + 00000010 ff ff 00 00 c0 a8 01 01 00 00 00 00 88 13 01 |...............| + 0000001f + */ + + LAN_COMMAND_CONFIG_GET getLanConfig; // f3 02 0c ff 00 00 + LAN_COMMAND_CONFIG_GET_RESPONSE getLanConfigResponse; + /*00000000 58 02 0c ff 19 00 00 f8 dc 3f 00 00 c0 a8 01 02 |X........?......| + 00000010 ff ff 00 00 c0 a8 01 01 00 00 00 00 88 13 01 |...............| + 0000001f*/ + + LAN_COMMAND_ERROR_GET getError; // f1 02 0e ff 00 00 + + LAN_COMMAND_ERROR_GET_RESPONSE getErrorResponse; + /*00000000 ad 02 0e ff 44 00 00 00 00 00 00 00 00 00 00 00 |....D...........| + 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + * + 00000040 00 00 00 00 00 00 00 00 00 00 |..........| + 0000004a*/ + + LAN_COMMAND_VersionDataGet getVerData; + LAN_COMMAND_VersionDataResponse getVerDataResponse{ + .payload{ + Version hardware{ + .major=0x1234, + .minor=0x5678, + }, + firmware{ + .major=0xABCD, + .minor=0xEF01, + }, + } + }; + + LAN_COMMAND_Type_header* commandsPtrs[]{ + reinterpret_cast(&noPass), + reinterpret_cast(&defaultPass), + reinterpret_cast(&setDefaultPass), + reinterpret_cast(&resp), + reinterpret_cast(&moveFInstr), + reinterpret_cast(&getSpeedInstr), + reinterpret_cast(&resp1), + reinterpret_cast(&writeSingle), + reinterpret_cast(&readRequest), + reinterpret_cast(&readResponse), + reinterpret_cast(&setLanConfig), + reinterpret_cast(&getLanConfig), + reinterpret_cast(&getLanConfigResponse), + reinterpret_cast(&getError), + reinterpret_cast(&getErrorResponse) + reinterpret_cast(&getVerData), + reinterpret_cast(&getVerDataResponse) + }; + + dumpCommands(std::begin(commandsPtrs), std::end(commandsPtrs)); +} + +template +void printSizeof(){ + std::cout << "sizeof(" << nameof::nameof_short_type() << ") " << sizeof(T) << std::endl; +} + +voif printSizeofs(){ + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + printSizeof(); + + printSizeof>(); + printSizeof>(); + printSizeof>(); + printSizeof>(); + + +} + + +int main() { + printSizeofs(); + testSMSD_CMD_Type(); + test_powerSTEP_STATUS_TypeDef(); + test_COMMANDS_RETURN_DATA_Type(); + testSMSD_LAN_Config_Type(); + testLAN_COMMAND_Type_header(); + testLAN_COMMAND_Type(); + return 0; +} diff --git a/tests/lan_proto_undocumented.hpp b/tests/lan_proto_undocumented.hpp new file mode 100644 index 0000000..88c3289 --- /dev/null +++ b/tests/lan_proto_undocumented.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "lan_proto_from_pdf.hpp" + + +struct [[gnu::packed]] VersionData{ + struct [[gnu::packed]] Version{ + uint16_t major = 0; + uint16_t minor = 0; + }; + + Version hardware; + Version firmware; + uint8_t protocol = CURRENT_PROTOCOL_VERSION; +}; + +struct [[gnu::packed]] LAN_COMMAND_VersionDataGet{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::VersionData, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = 0, + }; +}; + +struct [[gnu::packed]] LAN_COMMAND_VersionDataResponse{ + LAN_COMMAND_Type_header header{ + .CMD_TYPE = CODE_CMD::VersionData, + .CMD_IDENTIFICATION = 0xFF, + .LENGTH_DATA = sizeof(VersionData), + }; + VersionData payload; +}; diff --git a/tests/testData/COMMANDS_RETURN_DATA_Type/0.dat b/tests/testData/COMMANDS_RETURN_DATA_Type/0.dat new file mode 100644 index 0000000000000000000000000000000000000000..01203da5fb0c79894c266c2578ae60d94307bb44 GIT binary patch literal 7 Lcmca#&Hw@c2 literal 0 HcmV?d00001 diff --git a/tests/testData/COMMANDS_RETURN_DATA_Type/1.dat b/tests/testData/COMMANDS_RETURN_DATA_Type/1.dat new file mode 100644 index 0000000000000000000000000000000000000000..cfdb8649b35cc45b7deec3edd7d16b76219f1252 GIT binary patch literal 7 McmZQzSj_+g00UkCtN;K2 literal 0 HcmV?d00001 diff --git a/tests/testData/COMMANDS_RETURN_DATA_Type/2.dat b/tests/testData/COMMANDS_RETURN_DATA_Type/2.dat new file mode 100644 index 0000000000000000000000000000000000000000..05edd7ff96c4ab2108a9e923f398c780be43c1bb GIT binary patch literal 7 McmZQzVE7LN015pA{Qv*} literal 0 HcmV?d00001 diff --git a/tests/testData/LAN_COMMAND_Type_header/0.dat b/tests/testData/LAN_COMMAND_Type_header/0.dat new file mode 100644 index 0000000000000000000000000000000000000000..e83860d5a20e854cb26fb9ada003bac384098ab6 GIT binary patch literal 6 Ncmey&#Pgql0RRZ^0s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/SMSD_CMD_Type/0.dat b/tests/testData/SMSD_CMD_Type/0.dat new file mode 100644 index 0000000000000000000000000000000000000000..f66c9cf4c9672fa2832bce76f4082fd97b823506 GIT binary patch literal 4 LcmZQ%U|;|M00;mA literal 0 HcmV?d00001 diff --git a/tests/testData/SMSD_CMD_Type/1.dat b/tests/testData/SMSD_CMD_Type/1.dat new file mode 100644 index 0000000000000000000000000000000000000000..2cf9f2099b716d1d48d504a8708bb43dafcff3f2 GIT binary patch literal 4 LcmZQ#U|;|M01N;F literal 0 HcmV?d00001 diff --git a/tests/testData/SMSD_CMD_Type/10.dat b/tests/testData/SMSD_CMD_Type/10.dat new file mode 100644 index 0000000000000000000000000000000000000000..b7ac7df328a539e1ad6c5166fc8eeb030ce2e020 GIT binary patch literal 4 LcmYfJ!^{8x1bzWv literal 0 HcmV?d00001 diff --git a/tests/testData/SMSD_CMD_Type/11.dat b/tests/testData/SMSD_CMD_Type/11.dat new file mode 100644 index 0000000..d230d3b --- /dev/null +++ b/tests/testData/SMSD_CMD_Type/11.dat @@ -0,0 +1 @@ +  \ No newline at end of file diff --git a/tests/testData/SMSD_CMD_Type/12.dat b/tests/testData/SMSD_CMD_Type/12.dat new file mode 100644 index 0000000000000000000000000000000000000000..d118943e3c4020b618c1c2f118e5f8b46fcc1abe GIT binary patch literal 4 LcmY#jWUv7M0K))Y literal 0 HcmV?d00001 diff --git a/tests/testData/SMSD_CMD_Type/13.dat b/tests/testData/SMSD_CMD_Type/13.dat new file mode 100644 index 0000000000000000000000000000000000000000..d21eb47498ace5bf0b2119a6afd9090b03f048ab GIT binary patch literal 4 LcmY#jWHk literal 0 HcmV?d00001 diff --git a/tests/testData/commands/0_REQUEST.dat b/tests/testData/commands/0_REQUEST.dat new file mode 100644 index 0000000000000000000000000000000000000000..da79d4fd66efc2d2885d29d41e0006aba3e42a2a GIT binary patch literal 6 Ncmey*#PFYi0RRaH0s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/10_CONFIG_SET.dat b/tests/testData/commands/10_CONFIG_SET.dat new file mode 100644 index 0000000000000000000000000000000000000000..1cc1e78b5599b31b2c7dbfdfa732cde277e85e0a GIT binary patch literal 31 icma!K;{Gqm!0_XaJp;pm6^u;(|AT2p1|aAVW&{AE8VLaa literal 0 HcmV?d00001 diff --git a/tests/testData/commands/11_CONFIG_GET.dat b/tests/testData/commands/11_CONFIG_GET.dat new file mode 100644 index 0000000000000000000000000000000000000000..e83860d5a20e854cb26fb9ada003bac384098ab6 GIT binary patch literal 6 Ncmey&#Pgql0RRZ^0s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/12_CONFIG_GET.dat b/tests/testData/commands/12_CONFIG_GET.dat new file mode 100644 index 0000000000000000000000000000000000000000..1f50413a9448051095f2fa776fda3fc16dd1e3e0 GIT binary patch literal 31 icma!G;`uMh!0_XaJp;pm6^u;(|AT2p1|aAVW&{AE7zqIY literal 0 HcmV?d00001 diff --git a/tests/testData/commands/13_ERROR_GET.dat b/tests/testData/commands/13_ERROR_GET.dat new file mode 100644 index 0000000000000000000000000000000000000000..720af519968a5da8efc6674ad0b97f5f40113674 GIT binary patch literal 6 Ncmey!#P^?p0RRZ=0s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/14_ERROR_GET.dat b/tests/testData/commands/14_ERROR_GET.dat new file mode 100644 index 0000000000000000000000000000000000000000..a3d233964a105e51f3e449d4ae7c43bd664caf80 GIT binary patch literal 74 PcmZ3>#P{EYff@h+kRJj8 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/1_REQUEST.dat b/tests/testData/commands/1_REQUEST.dat new file mode 100644 index 0000000000000000000000000000000000000000..f9268e77ccdf764d560c1a5144828065eb79f1e1 GIT binary patch literal 14 VcmXqKV))O&z^Lq+-nshhdjJ``1pxp6 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/2_PASSWORD_SET.dat b/tests/testData/commands/2_PASSWORD_SET.dat new file mode 100644 index 0000000000000000000000000000000000000000..30766945c30664ae62a4553cd2a395cc70363c7a GIT binary patch literal 14 VcmdO5;{DITz^Lq+-nshhdjJ`s1pxp6 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/3_RESPONSE_.dat b/tests/testData/commands/3_RESPONSE_.dat new file mode 100644 index 0000000000000000000000000000000000000000..48583c50acede07d571016a2b2bede03cd8016ff GIT binary patch literal 13 Tcmeyy#Q2|`;ePpQ1|R?cBOL?* literal 0 HcmV?d00001 diff --git a/tests/testData/commands/4_POWERSTEP01_MOVE_F.dat b/tests/testData/commands/4_POWERSTEP01_MOVE_F.dat new file mode 100644 index 0000000000000000000000000000000000000000..57941bd6cd4844c38746aa5cf2c74a24e38e49b9 GIT binary patch literal 10 Rcmeyt#Ppwqfq{{M0RRy20s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/5_POWERSTEP01_GET_SPEED.dat b/tests/testData/commands/5_POWERSTEP01_GET_SPEED.dat new file mode 100644 index 0000000000000000000000000000000000000000..b933bb41780a611866e0393bad1d5e24b1e2eb81 GIT binary patch literal 10 RcmaFK#PpwqL4bjQ0RRw}0s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/6_RESPONSE_COMMAND_GET_SPEED.dat b/tests/testData/commands/6_RESPONSE_COMMAND_GET_SPEED.dat new file mode 100644 index 0000000000000000000000000000000000000000..bc9991b5aded85f1e4c12ff94048dfe9f0793c75 GIT binary patch literal 13 UcmX@W#Q2|`;XZ?qi4X$=02`hI0RR91 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/7_POWERSTEP01_W_MEM0_1.dat b/tests/testData/commands/7_POWERSTEP01_W_MEM0_1.dat new file mode 100644 index 0000000000000000000000000000000000000000..3e13e049afe8c542b0a0d032dcdfb27d4cac2d56 GIT binary patch literal 10 RcmaFB#QdLyLGlkX0{|0a0|5X4 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/8_POWERSTEP01_R_MEM0.dat b/tests/testData/commands/8_POWERSTEP01_R_MEM0.dat new file mode 100644 index 0000000000000000000000000000000000000000..d736366e3ba4de27e7a7e3d7a3858af411710585 GIT binary patch literal 6 Ncmeyt#QvXw0RRa30s#O3 literal 0 HcmV?d00001 diff --git a/tests/testData/commands/9_POWERSTEP01_R_MEM0_1.dat b/tests/testData/commands/9_POWERSTEP01_R_MEM0_1.dat new file mode 100644 index 0000000000000000000000000000000000000000..49c71034bb0b9d5b08b97a778d4198f4574afc53 GIT binary patch literal 10 Rcmcb^#QvX!LGlkX0{|0S0|5X4 literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/0.dat b/tests/testData/powerSTEP_STATUS_TypeDef/0.dat new file mode 100644 index 0000000000000000000000000000000000000000..35a038769b15c0935bb3cd038f5cc1de7579f128 GIT binary patch literal 2 JcmZQ%0000400IC2 literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/1.dat b/tests/testData/powerSTEP_STATUS_TypeDef/1.dat new file mode 100644 index 0000000000000000000000000000000000000000..5407bf3ddf8b5ca61b411342fe54921a2bbb0ec2 GIT binary patch literal 2 JcmZQ#0000600RI3 literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/10.dat b/tests/testData/powerSTEP_STATUS_TypeDef/10.dat new file mode 100644 index 0000000000000000000000000000000000000000..ba01f6b05bdbb386b35f4d086e268c5d422cafb9 GIT binary patch literal 2 JcmZSh4*&rH0RR91 literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/2.dat b/tests/testData/powerSTEP_STATUS_TypeDef/2.dat new file mode 100644 index 0000000000000000000000000000000000000000..d825e1ad776558a390c09389f5b2ce26cd573be3 GIT binary patch literal 2 JcmZQ!0000A00jU5 literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/3.dat b/tests/testData/powerSTEP_STATUS_TypeDef/3.dat new file mode 100644 index 0000000000000000000000000000000000000000..268720a2e79c9095b9b38ddd2ab7495ab6474f1b GIT binary patch literal 2 Jcmd;J0000I00{s9 literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/4.dat b/tests/testData/powerSTEP_STATUS_TypeDef/4.dat new file mode 100644 index 0000000000000000000000000000000000000000..6a4f18607c79a88e0389dc87354e03c5a70114fe GIT binary patch literal 2 JcmWe&0000Y01*HH literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/5.dat b/tests/testData/powerSTEP_STATUS_TypeDef/5.dat new file mode 100644 index 0000000000000000000000000000000000000000..8f3e7dc26bbd27756dda579a638909ccd4152c27 GIT binary patch literal 2 JcmY#j0000&03iSX literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/6.dat b/tests/testData/powerSTEP_STATUS_TypeDef/6.dat new file mode 100644 index 0000000000000000000000000000000000000000..88f99805e3bfe92b02c2b74323748993ac63d6fa GIT binary patch literal 2 JcmZ=@0001j06_o% literal 0 HcmV?d00001 diff --git a/tests/testData/powerSTEP_STATUS_TypeDef/7.dat b/tests/testData/powerSTEP_STATUS_TypeDef/7.dat new file mode 100644 index 0000000000000000000000000000000000000000..0974091e000ccf7de355f997cc5b1ae87b7c02b4 GIT binary patch literal 2 JcmYdb0002O0AT