Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
KOLANICH committed Dec 21, 2022
0 parents commit cc2b985
Show file tree
Hide file tree
Showing 102 changed files with 6,846 additions and 0 deletions.
Empty file added .ci/aptPackagesToInstall.txt
Empty file.
4 changes: 4 additions & 0 deletions .ci/pythonPackagesToInstallFromGit.txt
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .github/.templateMarker
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KOLANICH/python_project_boilerplate.py
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-type: "all"
15 changes: 15 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -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 }}
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions Code_Of_Conduct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No codes of conduct!
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include UNLICENSE
include *.md
include tests
include .editorconfig
72 changes: 72 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
@@ -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<number>` 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 `<name>.smc` and `<name>._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.
* `<name>.smc` contains a sequence of commands
* `<name>._smc` contains human-readable description of what a command does.
Empty file added SMSD/__init__.py
Empty file.
68 changes: 68 additions & 0 deletions SMSD/__main__.py
Original file line number Diff line number Diff line change
@@ -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()
Empty file added SMSD/kaitai/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions SMSD/kaitai/checksum_simple_additive_u1.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit cc2b985

Please sign in to comment.